Spring Security实战一:登录注册

登录注册总是密不可分,就放在一起说吧。数据库中明文密码自然是不允许的。需要在项目级别拦截请求,来实现登录注册操作。

一、要解决的问题

  本篇要解决的问题

  • 项目级别统一拦截请求
  • 注册加密
  • 登录校验
  • 登录成功/失败返回自定义信息
  • 自定义用户信息

二、原理

  Spring Boot项目中引入Spring Security,通过WebSecurityConfigurerAdapter来实现请求的统一拦截,拦截到请求后,通过UserDetailsService来查询数据库中存储的用户信息,比对登录请求传输的信息,来确定登录成功与否。

三、实战

1.引入Spring Security

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.0.1.RELEASE</version>
        </dependency>

2.自定义WebSecurityConfigurerAdapter统一拦截请求

/**
 * @EnableWebSecurity:此注解会启用Spring Security
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 1)HttpSecurity支持cors。
     * 2)默认会启用CRSF,此处因为没有使用thymeleaf模板(会自动注入_csrf参数),
     * 要先禁用csrf,否则登录时需要_csrf参数,而导致登录失败。
     * 3)antMatchers:匹配 "/" 路径,不需要权限即可访问,匹配 "/user" 及其以下所有路径,
     *  都需要 "USER" 权限
     * 4)配置登录地址和退出地址
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/user/**").hasRole("USER")
                .and()
                .formLogin().loginPage("/login").defaultSuccessUrl("/hello")
                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/login");
    }
}

3.自定义UserDetailsService查询数据库中用户信息

@Service
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //从数据库查询用户信息
        UserInfoBean userInfo = userService.getUser(username);
        if (userInfo == null){
            throw new UsernameNotFoundException("用户不存在!");
        }
        //查询权限信息
        List<SimpleGrantedAuthority> simpleGrantedAuthorities = createAuthorities(userInfo.getRoles());
        //返回Spring Security框架提供的User或者自定义的MyUser(implements UserDetails)
        ////        return new MyUser(username, userInfo.getPassword(), simpleGrantedAuthorities);
        return new User(username, userInfo.getPassword(), simpleGrantedAuthorities);
    }

    /**
     * 权限字符串转化
     *
     * 如 "USER,ADMIN" -> SimpleGrantedAuthority("USER") + SimpleGrantedAuthority("ADMIN")
     *
     * @param roleStr 权限字符串
     */
    private List<SimpleGrantedAuthority> createAuthorities(String roleStr){
        String[] roles = roleStr.split(",");
        List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
        for (String role : roles) {
            simpleGrantedAuthorities.add(new SimpleGrantedAuthority(role));
        }
        return simpleGrantedAuthorities;
    }
}

4.密码加密

  密码加密简单说明下。密码加密分两个部分,注册时给存储到数据库的密码加密和登录验证时将拿到的密码加密与数据库中密码比对。Spring Security提供了几种加密方式,当然也可自定义,此处选用BCryptPasswordEncoder。
  BCryptPasswordEncoder相关知识:用户表的密码通常使用MD5等不可逆算法加密后存储,为防止彩虹表破解更会先使用一个特定的字符串(如域名)加密,然后再使用一个随机的salt(盐值)加密。特定字符串是程序代码中固定的,salt是每个密码单独随机,一般给用户表加一个字段单独存储,比较麻烦。BCrypt算法将salt随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理salt问题。

1)注册时给密码加密

注册接口中加密密码:

@Service
public class UserService {

    @Autowired
    private UserInfoMapper userInfoMapper;

    public boolean insert(UserInfoBean userInfo){
        encryptPassword(userInfo);
        if(userInfoMapper.insert(userInfo)==1)
            return true;
        else
            return false;
    };

    private void encryptPassword(UserInfoBean userInfo){
        String password = userInfo.getPassword();
        password = new BCryptPasswordEncoder().encode(password);
        userInfo.setPassword(password);
    }
}
2)登录时密码加密校验

只需在WebSecurityConfigurerAdapter的子类中指定密码的加密规则即可,Spring Security会自动将密码加密后与数据库比对。

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailService).passwordEncoder(passwordEncoder());
    }

    /**
     * 密码加密
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

5.自定义用户登录流程

  要实现当用户是html结尾的请求就跳转到默认的登录页面或者指定的登录页,用户是restFul请求时就返回json数据。备注:此处的校验逻辑是以html后缀来校验,如果集成其他模板引擎可根据需要修改。
1)先定义实现以上需求的controller逻辑

private RequestCache requestCache = new HttpSessionRequestCache();
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    @RequestMapping("/logintype")
    @ResponseBody
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public String requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (savedRequest != null) {
            String targetUrl =  savedRequest.getRedirectUrl();
            logger.info("引发跳转的请求是:" + targetUrl);
            if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
                redirectStrategy.sendRedirect(request, response, securityProperties.getBrower().getLoginPage());
            }
        }
        return "请登录!";
    }


    @GetMapping("/login_html")
    public String loginHtml(){
        return "login";
    }

    @PostMapping("/login")
    public void login(){
    }

2)定义不同登录页面的配置类

@Configuration
@ConfigurationProperties(prefix = "evolutionary.security")
public class SecurityProperties {

    private BrowerProperties brower = new BrowerProperties();

    public BrowerProperties getBrower() {
        return brower;
    }

    public void setBrower(BrowerProperties brower) {
        this.brower = brower;
    }
}
public class BrowerProperties {
    private String loginPage = "/login_html";//默认跳转的接口

//    private LoginInType loginInType = LoginInType.JSON;

    public String getLoginPage() {
        return loginPage;
    }

    public void setLoginPage(String loginPage) {
        this.loginPage = loginPage;
    }

//    public LoginInType getLoginInType() {
//        return loginInType;
//    }
//
//    public void setLoginInType(LoginInType loginInType) {
//        this.loginInType = loginInType;
//    }
}

可配置的登录页面

#配置登录页面接口
#evolutionary.security.brower.loginPage = /login_html
#evolutionary.security.brower.loginInType=REDIRECT

3)默认的登录页面login.html

<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>登录 | SpringForAll - Spring Security</title>
    <link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet">
</head>
<body style="background-color: #f1f1f1; padding-bottom: 0">
<div class="container" style="margin-top: 60px">
    <div class="row" style="margin-top: 100px">
        <div class="col-md-6 col-md-offset-3">
            <div class="panel panel-primary">
                <div class="panel-heading">
                    <h3 class="panel-title"><span class="glyphicon glyphicon-console"></span> Login</h3>
                </div>
                <div class="panel-body">
                    <form th:action="@{/login}" method="post">
                        <div class="form-group" style="margin-top: 30px">
                            <div class="input-group col-md-6 col-md-offset-3">
                                <div class="input-group-addon"><span class="glyphicon glyphicon-user"></span></div>
                                <input type="text" class="form-control" name="username" id="username" placeholder="账号">
                            </div>
                        </div>
                        <div class="form-group ">
                            <div class="input-group col-md-6 col-md-offset-3">
                                <div class="input-group-addon"><span class="glyphicon glyphicon-lock"></span></div>
                                <input type="password" class="form-control" name="password" id="password"
                                       placeholder="密码">
                            </div>
                        </div>
                        <br>
                        <div class="form-group">
                            <div class="input-group col-md-6 col-md-offset-3 col-xs-12 ">
                                <button type="submit" class="btn btn-primary btn-block">登录</button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

4)修改认证逻辑的代码见文末

6.自定义登录成功处理

默认情况下Spring Security登录成功后会跳到之前引发登录的请求。修改为登录成功返回json信息,只需实现AuthenticationSuccessHandler接口的onAuthenticationSuccess方法:

@Component("evolutionaryAuthenticationHandler")
public class EvolutionaryAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
 
    private Logger logger = LoggerFactory.getLogger(getClass());
 
    /**
     * spring MVC 启动的时候会为我们注册一个objectMapper
     */
    @Autowired
    private ObjectMapper objectMapper;
 
    @Autowired
    private SecurityProperties securityProperties;
 
    /**
     * 登录成功会调用该方法
     * @param request
     * @param response
     * @param authentication
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        logger.info("登录成功!");
        if (LoginInType.JSON.equals(securityProperties.getBrower().getLoginInType())) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        }else{
            super.onAuthenticationSuccess(request, response, authentication);
        }
 
    }
}
@Component("evolutionaryAuthenticationHandler")
public class EvolutionaryAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * spring MVC 启动的时候会为我们注册一个objectMapper
     */
    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    /**
     * 登录成功会调用该方法
     * @param request
     * @param response
     * @param authentication
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        logger.info("登录成功!");
        if (/*LoginInType.JSON.equals(securityProperties.getBrower().getLoginInType())*/1==1) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        }else{
            super.onAuthenticationSuccess(request, response, authentication);
        }

    }
}

再配置登录成功的处理方式:

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()  //表单登录
                //.loginPage("/evolutionary-loginIn.html")
                .loginPage("/logintype") //如果需要身份认证则跳转到这里
                .loginProcessingUrl("/login")
                .successHandler(evolutionaryAuthenticationHandler)
                .failureHandler(evolutionaryAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers("/logintype",securityProperties.getBrower().getLoginPage())//不校验我们配置的登录页面
                .permitAll()
                .anyRequest()
                .authenticated()
                .and().csrf().disable();
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容