SpringSecurity實現短信驗證碼登錄驗證


在這裏插入圖片描述
SpringSecurity進行用戶登錄認證時,通過UsernamePasswordAuthenticationFilter獲取用戶信息,獲取一個UsernamePasswordAuthenticationToken,將該token設置給AuthenticationManager進行管理,選擇不同的Provider進行認證處理,在Provider中通過UserDetaitlsService獲取用戶信息進行認證,生成新的認證信息。

在短信驗證時,我們只要按照登錄驗證的步驟進行重新寫一個自己的過濾器、token類、provider即可。

1、自己實現一個SmsAuthenticationToken類

/**
 *  短信驗證碼token
 *
 *  仿照UsernamePasswordAuthenticationToken
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 520L;
    /**
     *  該屬性沒登錄前放手機號,登錄後的放用戶信息
     */
    private final Object principal;


    public SmsCodeAuthenticationToken(String mobile) {
        super((Collection)null);
        this.principal = mobile;
        this.setAuthenticated(false);
    }

    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        } else {
            super.setAuthenticated(false);
        }
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

2、自己實現一個SmsCodeAuthenticationFilter,驗證用戶

/**
 *  仿照UsernamePasswordAuthenticationFilter
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public static final String IMOOC_FROM_MOBILE_KEY = "mobile";
    private String mobileParameter = "username";
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String mobile = this.obtainMobile(request);
            if (mobile == null) {
                mobile = "";
            }



            mobile = mobile.trim();
            // 生成token
            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
            this.setDetails(request, authRequest);
            // 調用 AuthenticationManager
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    /**
     *  獲取手機號
     * @param request
     * @return
     */
    @Nullable
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(this.mobileParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setUsernameParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, "Username parameter must not be empty or null");
        this.mobileParameter = mobileParameter;
    }



    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getMobileParameter() {
        return this.mobileParameter;
    }

}

3、自己實現一個SmsCodeAuthenticationProvider

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;
    /**
     *   進行身份認證的邏輯
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken)authentication;
        UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
        if (user == null) {
            throw new InternalAuthenticationServiceException("無法獲取用戶信息");
        }
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    /**
     *  根據該方法判斷 AuthenticationManager選擇哪個 Provider進行認證處理
     * @param authentication
     * @return
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

4、驗證碼驗證過濾器

/**
 *  繼承一個過濾器,實現驗證碼驗證
 */
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {

    private AuthenticationFailureHandler authenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    private Set<String> urls = new HashSet<>();

    private SecurityProperties securityProperties;

    private AntPathMatcher pathMatcher = new AntPathMatcher();

    /**
     *  初始化設置
     *   將配置文件中的值讀取,存在urls中
     * @throws ServletException
     */
    @Override
    public void afterPropertiesSet() throws ServletException {
       super.afterPropertiesSet();
       String[] configUrls = StringUtils.split(securityProperties.getCode().getSms().getUrl(),",");
       // 添加配置的url
        for (String configUrl : configUrls) {
            urls.add(configUrl);
        }
        // 添加攔截的url
        urls.add("authentication/mobile");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        /**
         *  如果是登錄請求,並且是post請求就執行驗證
         *
         */
        boolean action = false;
        for (String url : urls) {
            if (pathMatcher.match(url, request.getRequestURI())) {
                action = true;
            }
        }
        if (action) {
            try {
                validate(new ServletWebRequest(request));
            } catch (ValidateCodeException e) {
                authenticationFailureHandler.onAuthenticationFailure(request,response,e);
                return ;
            }
        }
//        if (StringUtils.equals("authentication/from", request.getRequestURI())
//            && StringUtils.equalsIgnoreCase(request.getMethod(),"post")){
//
//            try {
//                validate(new ServletWebRequest(request));
//            } catch (ValidateCodeException e) {
//                authenticationFailureHandler.onAuthenticationFailure(request,response,e);
//                return ;
//            }
//        }
        filterChain.doFilter(request, response);
    }

    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        // 讀取session
        ValidateCode codeInSession = (ValidateCode) sessionStrategy.getAttribute(request,
                ValidateCodeController.SESSION_KEY);
       // 獲取驗證碼的值
        String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(),"smsCode");

        if (StringUtils.isEmpty(codeInRequest)) {
            throw new ValidateCodeException("驗證碼的值不能爲空");
        }
        if (codeInSession == null) {
            throw  new ValidateCodeException("驗證碼不存在");
        }
        if (codeInSession.isExpried()) {
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
            throw  new ValidateCodeException("驗證碼已過期");
        }
        if (!StringUtils.equals(codeInSession.getCode(),codeInRequest)) {
            throw  new ValidateCodeException("驗證碼不匹配");
        }

        sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
    }

    public AuthenticationFailureHandler getAuthenticationFailureHandler() {
        return authenticationFailureHandler;
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    public SessionStrategy getSessionStrategy() {
        return sessionStrategy;
    }

    public void setSessionStrategy(SessionStrategy sessionStrategy) {
        this.sessionStrategy = sessionStrategy;
    }

    public Set<String> getUrls() {
        return urls;
    }

    public void setUrls(Set<String> urls) {
        this.urls = urls;
    }

    public SecurityProperties getSecurityProperties() {
        return securityProperties;
    }

    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }
}

5、將SmsCodeAuthenticationFitler和SmsCodeAuthenticationProvider進行配置

將短信驗證碼登錄驗證的filter和provider,進行單獨配置。一般在項目中,短信驗證碼會是一個公共項目組件。

/**
 *  配置SmsCodeAuthenticationFilter
 *   和SmsCodeAuthenticationProvider
 */
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
       SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
       smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
       smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
       smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);

       SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

6、驗證碼的驗證進行配置

/**
 * 覆蓋springboot對security的默認配置
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;

    /**
     * 注入登錄成功操作類
     */
    @Autowired
    private AuthenticationSuccessHandler imoocAutheticationSuccessHandler;
    /**
     * 注入登錄失敗的處理器
     */
    @Autowired
    private AuthenticationFailureHandler imoocAuthenticationFailuredHandler;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private UserDetailsService userDetailsService;
    /**
    *  將短信驗證的單獨配置引入進來,通過HttpSecurity 的apply()方法進行配置
    */
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

    /**
     * 用戶密碼加密類配置
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 這裏可以返回自己實現的加密類
        return new BCryptPasswordEncoder();
    }

    /**
     *  記住我的配置
     * @return
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository () {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 啓動的時候創建表
//        tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 驗證碼過濾器配置
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        // 設置失敗處理器
        validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailuredHandler);
        validateCodeFilter.setSecurityProperties(securityProperties);
        validateCodeFilter.afterPropertiesSet();

        // 短信驗證碼過濾器配置
        SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
        // 設置失敗處理器
        smsCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailuredHandler);
        smsCodeFilter.setSecurityProperties(securityProperties);
        smsCodeFilter.afterPropertiesSet();

        http.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin() // 表單登錄  http.httpBasic() httpbasic登錄
//                .loginPage("/imooc-signIn.html")  // 配置登錄頁面
                .loginPage("/authentication/require")
                .loginProcessingUrl("/authentication/from") // 配置登錄請求
                .successHandler(imoocAutheticationSuccessHandler) // 登錄成功處理器
                .failureHandler(imoocAuthenticationFailuredHandler)
                .and()
                .rememberMe()  // 配置記住我
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
                 .userDetailsService(userDetailsService)
                .and()
                .authorizeRequests()  // 請求驗證
//                .antMatchers("/imooc-signIn.html").permitAll()// 匹配不需要身份認證的路徑
                .antMatchers("/authentication/require",
                        securityProperties.getBrowser().getLoginPage(),
                        "/code/*").permitAll()
                .anyRequest() // 任何請求
                .authenticated() // 都需要身份認證
                .and()
                .csrf().disable()
                .apply(smsCodeAuthenticationSecurityConfig); // 關閉僞造跨站請求防護功能
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章