Spring Security實(shí)戰(zhàn)一:登錄注冊

登錄注冊總是密不可分,就放在一起說吧。數(shù)據(jù)庫中明文密碼自然是不允許的。需要在項(xiàng)目級別攔截請求,來實(shí)現(xiàn)登錄注冊操作。

一、要解決的問題

??本篇要解決的問題

  • 項(xiàng)目級別統(tǒng)一攔截請求
  • 注冊加密
  • 登錄校驗(yàn)
  • 登錄成功/失敗返回自定義信息
  • 自定義用戶信息

二、原理

??Spring Boot項(xiàng)目中引入Spring Security,通過WebSecurityConfigurerAdapter來實(shí)現(xiàn)請求的統(tǒng)一攔截,攔截到請求后,通過UserDetailsService來查詢數(shù)據(jù)庫中存儲的用戶信息,比對登錄請求傳輸?shù)男畔?,來確定登錄成功與否。

三、實(shí)戰(zhàn)

1.引入Spring Security

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

2.自定義WebSecurityConfigurerAdapter統(tǒng)一攔截請求

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

    /**
     * 1)HttpSecurity支持cors。
     * 2)默認(rèn)會啟用CRSF,此處因?yàn)闆]有使用thymeleaf模板(會自動注入_csrf參數(shù)),
     * 要先禁用csrf,否則登錄時(shí)需要_csrf參數(shù),而導(dǎo)致登錄失敗。
     * 3)antMatchers:匹配 "/" 路徑,不需要權(quán)限即可訪問,匹配 "/user" 及其以下所有路徑,
     *  都需要 "USER" 權(quán)限
     * 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查詢數(shù)據(jù)庫中用戶信息

@Service
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private UserService userService;

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

        //從數(shù)據(jù)庫查詢用戶信息
        UserInfoBean userInfo = userService.getUser(username);
        if (userInfo == null){
            throw new UsernameNotFoundException("用戶不存在!");
        }
        //查詢權(quán)限信息
        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);
    }

    /**
     * 權(quán)限字符串轉(zhuǎn)化
     *
     * 如 "USER,ADMIN" -> SimpleGrantedAuthority("USER") + SimpleGrantedAuthority("ADMIN")
     *
     * @param roleStr 權(quán)限字符串
     */
    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.密碼加密

??密碼加密簡單說明下。密碼加密分兩個(gè)部分,注冊時(shí)給存儲到數(shù)據(jù)庫的密碼加密和登錄驗(yàn)證時(shí)將拿到的密碼加密與數(shù)據(jù)庫中密碼比對。Spring Security提供了幾種加密方式,當(dāng)然也可自定義,此處選用BCryptPasswordEncoder。
??BCryptPasswordEncoder相關(guān)知識:用戶表的密碼通常使用MD5等不可逆算法加密后存儲,為防止彩虹表破解更會先使用一個(gè)特定的字符串(如域名)加密,然后再使用一個(gè)隨機(jī)的salt(鹽值)加密。特定字符串是程序代碼中固定的,salt是每個(gè)密碼單獨(dú)隨機(jī),一般給用戶表加一個(gè)字段單獨(dú)存儲,比較麻煩。BCrypt算法將salt隨機(jī)并混入最終加密后的密碼,驗(yàn)證時(shí)也無需單獨(dú)提供之前的salt,從而無需單獨(dú)處理salt問題。

1)注冊時(shí)給密碼加密

注冊接口中加密密碼:

@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)登錄時(shí)密碼加密校驗(yàn)

只需在WebSecurityConfigurerAdapter的子類中指定密碼的加密規(guī)則即可,Spring Security會自動將密碼加密后與數(shù)據(jù)庫比對。

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

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

5.自定義用戶登錄流程

??要實(shí)現(xiàn)當(dāng)用戶是html結(jié)尾的請求就跳轉(zhuǎn)到默認(rèn)的登錄頁面或者指定的登錄頁,用戶是restFul請求時(shí)就返回json數(shù)據(jù)。備注:此處的校驗(yàn)邏輯是以html后綴來校驗(yàn),如果集成其他模板引擎可根據(jù)需要修改。
1)先定義實(shí)現(xiàn)以上需求的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("引發(fā)跳轉(zhuǎn)的請求是:" + 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";//默認(rèn)跳轉(zhuǎn)的接口

//    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)默認(rèn)的登錄頁面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  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)修改認(rèn)證邏輯的代碼見文末

6.自定義登錄成功處理

默認(rèn)情況下Spring Security登錄成功后會跳到之前引發(fā)登錄的請求。修改為登錄成功返回json信息,只需實(shí)現(xiàn)AuthenticationSuccessHandler接口的onAuthenticationSuccess方法:

@Component("evolutionaryAuthenticationHandler")
public class EvolutionaryAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
 
    private Logger logger = LoggerFactory.getLogger(getClass());
 
    /**
     * spring MVC 啟動的時(shí)候會為我們注冊一個(gè)objectMapper
     */
    @Autowired
    private ObjectMapper objectMapper;
 
    @Autowired
    private SecurityProperties securityProperties;
 
    /**
     * 登錄成功會調(diào)用該方法
     * @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 啟動的時(shí)候會為我們注冊一個(gè)objectMapper
     */
    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    /**
     * 登錄成功會調(diào)用該方法
     * @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") //如果需要身份認(rèn)證則跳轉(zhuǎn)到這里
                .loginProcessingUrl("/login")
                .successHandler(evolutionaryAuthenticationHandler)
                .failureHandler(evolutionaryAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers("/logintype",securityProperties.getBrower().getLoginPage())//不校驗(yàn)我們配置的登錄頁面
                .permitAll()
                .anyRequest()
                .authenticated()
                .and().csrf().disable();
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容