Spring Security構建Rest服務-0702-短信驗證碼登陸

先來看下 Spring Security密碼登陸大概流程,模擬這個流程,開發短信登陸流程java

1,密碼登陸請求發送給過濾器 UsernamePasswordAuthenticationFilter git

2,過濾器拿出用戶名密碼組裝成 UsernamePasswordAuthenticationToken 對象傳給AuthenticationManagergithub

3,AuthenticationManager 會從一堆 AuthenticationProvider 裏選出一個Provider 處理認證請求。挑選的依據是AuthenticationProvider 裏有個web

boolean supports(Class<?> authentication);方法,判斷當前的provider是否支持傳進的token,若是支持就用這個provider認證這個token,並調用authenticate() 方法 進行認證spring

4,認證過程會調用UserDetailsService獲取用戶信息,跟傳進來的登陸信息作比對。認證經過會把UsernamePasswordAuthenticationToken作一個標識   標記已認證,放進session。數據庫

作短信登陸,不能在這個流程上改,這是兩種不一樣的登陸方式,混在一塊兒代碼質量很差,須要仿照這個流程寫一套本身的流程:apache

SmsAuthenticationFilter:攔截短信登陸請求,從請求中獲取手機號,封裝成 SmsAuthenticationToken 也會傳給AuthenticationManager,AuthenticationManager會找適合的provider,自定義SmsAuthenticationProvider校驗SmsAuthenticationToken 裏手機號信息。也會調UserDetailsService 看是否能登陸,能的話標記爲已登陸。安全

其中SmsAuthenticationFilter 參考UsernamePasswordAuthenticationFilter寫,SmsCodeAuthenticationToken參考UsernamePasswordAuthenticationToken寫,其實就是就是複製粘貼改改session

從上圖可知,須要寫三個類:app

1,SmsAuthenticationToken:複製UsernamePasswordAuthenticationToken,把沒用的去掉

package com.imooc.security.core.authentication.mobile;

import java.util.Collection;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

/**
 * 模仿UsernamePasswordAuthenticationToken寫的短信登陸token
 * ClassName: SmsCodeAuthenticationToken 
 * @Description: TODO
 * @author lihaoyang
 * @date 2018年3月7日
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;


    //沒登錄,放手機號,登陸成功,放用戶信息
    private final Object principal;



    /**
     * 沒登陸放手機號
     * <p>Description: </p>
     * @param mobile
     */
    public SmsCodeAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;//沒登陸放手機號
        setAuthenticated(false);//沒登陸
    }

    
    public SmsCodeAuthenticationToken(Object principal, Object credentials,
            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }

    // ~ Methods
    // ========================================================================================================

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

    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");
        }

        super.setAuthenticated(false);
    }

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

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

2,SmsCodeAuthenticationFilter,參考UsernamePasswordAuthenticationFilter

package com.imooc.security.core.authentication.mobile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;

/**
 * 模仿UsernamePasswordAuthenticationFilter 寫的短信驗證碼過濾器
 * ClassName: SmsCodeAuthenticationFilter 
 * @Description: TODO
 * @author lihaoyang
 * @date 2018年3月8日
 */
public class SmsCodeAuthenticationFilter extends
AbstractAuthenticationProcessingFilter{
    
    public static final String IMOOC_FORM_MOBILE_KEY = "mobile";

    private String mobilePatameter = IMOOC_FORM_MOBILE_KEY;
    private boolean postOnly = true;

    // ~ Constructors
    // ===================================================================================================

    public SmsCodeAuthenticationFilter() {
        //過濾的請求url,登陸表單的url
        super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
    }

    // ~ Methods
    // ========================================================================================================

    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String mobile = obtainMobile(request);

        if (mobile == null) {
            mobile = "";
        }


        mobile = mobile.trim();

        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        //在這裏把SmsCodeAuthenticationToken交給AuthenticationManager
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    /**
     * 獲取手機號
     * @Description: TODO
     * @param @param request
     * @param @return   
     * @return String  
     * @throws
     * @author lihaoyang
     * @date 2018年3月7日
     */
    private String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobilePatameter);
    }

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

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

    

}

3,SmsCodeAuthenticationProvider:

 在 SmsCodeAuthenticationFilter 裏 attemptAuthentication方法的最後, return this.getAuthenticationManager().authenticate(authRequest);這句話就是進到 SmsCodeAuthenticationProvider 先調用 supports() 方法,經過後,再調用 authenticate()方法進行認證

package com.imooc.security.core.authentication.mobile;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

/**
 * AuthenticationManager 認證時候須要用的一個Provider
 * ClassName: SmsCodeAuthenticationProvider 
 * @Description: TODO
 * @author lihaoyang
 * @date 2018年3月8日
 */
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;
    
    /**
     * 認證
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //能進到這說明authentication是SmsCodeAuthenticationToken,轉一下
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken)authentication;
        //token.getPrincipal()就是手機號 mobile
        UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
        
        //認證沒經過
        if(user == null){
            throw new InternalAuthenticationServiceException("沒法獲取用戶信息");
        }
        //認證經過
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
        //把認證以前得token裏存的用戶信息賦值給認證後的token對象
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    /**
     * 告訴AuthenticationManager,若是是SmsCodeAuthenticationToken的話用這個類處理
     */
    @Override
    public boolean supports(Class<?> authentication) {
        //判斷傳進來的authentication是否是SmsCodeAuthenticationToken類型的
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

}

短信 驗證碼過濾器,照着圖片驗證碼過濾器寫,其實能夠重構,不會弄:

package com.imooc.security.core.validate.code;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

import com.imooc.security.core.properties.SecurityConstants;
import com.imooc.security.core.properties.SecurityProperties;

/**
 * 短信驗證碼過濾器
 * ClassName: ValidateCodeFilter 
 * @Description:
 *  繼承OncePerRequestFilter:spring提供的工具,保證過濾器每次只會被調用一次
 *  實現 InitializingBean接口的目的:
 *      在其餘參數都組裝完畢的時候,初始化須要攔截的urls的值
 * @author lihaoyang
 * @date 2018年3月2日
 */
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean{

    private Logger logger = LoggerFactory.getLogger(getClass());
    
    //認證失敗處理器
    private AuthenticationFailureHandler authenticationFailureHandler;

    //獲取session工具類
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    
    //須要攔截的url集合
    private Set<String> urls = new HashSet<>();
    //讀取配置
    private SecurityProperties securityProperties;
    //spring工具類
    private AntPathMatcher antPathMatcher = new AntPathMatcher();
    
    /**
     * 重寫InitializingBean的方法,設置須要攔截的urls
     */
    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        //讀取配置的攔截的urls
        String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getSms().getUrl(), ",");
        //若是配置了須要驗證碼攔截的url,不判斷,若是沒有配置會空指針
        if(configUrls != null && configUrls.length > 0){
            for (String configUrl : configUrls) {
                logger.info("ValidateCodeFilter.afterPropertiesSet()--->配置了驗證碼攔截接口:"+configUrl);
                urls.add(configUrl);
            }
        }else{
            logger.info("----->沒有配置攔驗證碼攔截接口<-------");
        }
        //短信驗證碼登陸必定攔截
        urls.add(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        //若是是 登陸請求 則執行
//        if(StringUtils.equals("/authentication/form", request.getRequestURI())
//                &&StringUtils.equalsIgnoreCase(request.getMethod(), "post")){
//            try {
//                validate(new ServletWebRequest(request));
//            } catch (ValidateCodeException e) {
//                //調用錯誤處理器,最終調用本身的
//                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
//                return ;//結束方法,再也不調用過濾器鏈
//            }
//        }
        
        
        /**
         * 可配置的驗證碼校驗
         * 判斷請求的url和配置的是否有匹配的,匹配上了就過濾
         */
        boolean action = false;
        for(String url:urls){
            if(antPathMatcher.match(url, request.getRequestURI())){
                action = true;
            }
        }
        if(action){
            try {
                validate(new ServletWebRequest(request));
            } catch (ValidateCodeException e) {
                //調用錯誤處理器,最終調用本身的
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return ;//結束方法,再也不調用過濾器鏈
            }
        }
        
        //不是登陸請求,調用其它過濾器鏈
        filterChain.doFilter(request, response);
    }

    /**
     * 校驗驗證碼
     * @Description: 校驗驗證碼
     * @param @param request
     * @param @throws ServletRequestBindingException   
     * @return void  
     * @throws ValidateCodeException
     * @author lihaoyang
     * @date 2018年3月2日
     */
    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        //拿出session中的ImageCode對象
        ValidateCode smsCodeInSession = (ValidateCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY_SMS);
        //拿出請求中的驗證碼
        String imageCodeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
        //校驗
        if(StringUtils.isBlank(imageCodeInRequest)){
            throw new ValidateCodeException("驗證碼不能爲空");
        }
        if(smsCodeInSession == null){
            throw new ValidateCodeException("驗證碼不存在,請刷新驗證碼");
        } 
        if(smsCodeInSession.isExpired()){
            //從session移除過時的驗證碼
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY_SMS);
            throw new ValidateCodeException("驗證碼已過時,請刷新驗證碼");
        }
        if(!StringUtils.equalsIgnoreCase(smsCodeInSession.getCode(), imageCodeInRequest)){
            throw new ValidateCodeException("驗證碼錯誤");
        }
        //驗證經過,移除session中驗證碼
        sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY_SMS);
    }

    public AuthenticationFailureHandler getAuthenticationFailureHandler() {
        return authenticationFailureHandler;
    }

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

    public SecurityProperties getSecurityProperties() {
        return securityProperties;
    }

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

把新建的這三個類作下配置,讓spring security知道

SmsCodeAuthenticationSecurityConfig:

package com.imooc.security.core.authentication.mobile;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

/**
 * 短信驗證碼配置
 * ClassName: SmsCodeAuthenticationSecurityConfig 
 * @Description: TODO
 * @author lihaoyang
 * @date 2018年3月8日
 */
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
    
    @Autowired
    private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //1,配置短信驗證碼過濾器
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        //設置認證失敗成功處理器
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
        
        //配置pprovider
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
    
        http.authenticationProvider(smsCodeAuthenticationProvider)
            .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

    
}

最後在BrowserSecurityConfig裏配置短信驗證碼

@Configuration //這是一個配置
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
    
    //讀取用戶配置的登陸頁配置
    @Autowired
    private SecurityProperties securityProperties;
    
    //自定義的登陸成功後的處理器
    @Autowired
    private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
    
    //自定義的認證失敗後的處理器
    @Autowired
    private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
    
    //數據源
    @Autowired
    private DataSource dataSource;
    
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
    
    @Autowired
    private SpringSocialConfigurer imoocSocialSecurityConfig;

    //注意是org.springframework.security.crypto.password.PasswordEncoder
    @Bean
    public PasswordEncoder passwordencoder(){
        //BCryptPasswordEncoder implements PasswordEncoder
        return new BCryptPasswordEncoder();
    }
    
    
    /**
     * 記住我TokenRepository配置,在登陸成功後執行
     * 登陸成功後往數據庫存token的
     * @Description: 記住我TokenRepository配置
     * @param @return   JdbcTokenRepositoryImpl
     * @return PersistentTokenRepository  
     * @throws
     * @author lihaoyang
     * @date 2018年3月5日
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //啓動時自動生成相應表,能夠在JdbcTokenRepositoryImpl裏本身執行CREATE_TABLE_SQL腳本生成表
        //第二次啓動表已存在,須要註釋
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }
    
  //版本二:可配置的登陸頁
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //~~~-------------> 圖片驗證碼過濾器 <------------------
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        //驗證碼過濾器中使用本身的錯誤處理
        validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
        //配置的驗證碼過濾url
        validateCodeFilter.setSecurityProperties(securityProperties);
        validateCodeFilter.afterPropertiesSet();
        
        //~~~-------------> 短信驗證碼過濾器 <------------------
        SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
        //驗證碼過濾器中使用本身的錯誤處理
        smsCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
        //配置的驗證碼過濾url
        smsCodeFilter.setSecurityProperties(securityProperties);
        smsCodeFilter.afterPropertiesSet();
        
        
        
        //實現須要認證的接口跳轉表單登陸,安全=認證+受權
        //http.httpBasic() //這個就是默認的彈框認證
        //
        http 
            .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
//            .apply(imoocSocialSecurityConfig)//社交登陸
//            .and()
            //把驗證碼過濾器加載登陸過濾器前邊
            .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
            //表單認證相關配置
            .formLogin() 
                .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL) //處理用戶認證BrowserSecurityController
                //登陸過濾器UsernamePasswordAuthenticationFilter默認登陸的url是"/login",在這能改
                .loginProcessingUrl(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM) 
                .successHandler(imoocAuthenticationSuccessHandler)//自定義的認證後處理器
                .failureHandler(imoocAuthenticationFailureHandler) //登陸失敗後的處理
                .and()
            //記住我相關配置    
            .rememberMe()
                .tokenRepository(persistentTokenRepository())//TokenRepository,登陸成功後往數據庫存token的
                .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())//記住我秒數
                .userDetailsService(userDetailsService) //記住我成功後,調用userDetailsService查詢用戶信息
            .and()
            //受權相關的配置 
            .authorizeRequests() 
                // /authentication/require:處理登陸,securityProperties.getBrowser().getLoginPage():用戶配置的登陸頁
                .antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
                securityProperties.getBrowser().getLoginPage(),//放過登陸頁不過濾,不然報錯
                SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX+"/*").permitAll() //驗證碼
                .anyRequest()        //任何請求
                .authenticated()    //都須要身份認證
            .and()
                .csrf().disable() //關閉csrf防禦
            .apply(smsCodeAuthenticationSecurityConfig);//把短信驗證碼配置應用上
    }
}

訪問登錄頁,點擊發送驗證碼模擬發送驗證碼

輸入後臺打印的驗證碼

登陸成功:

完整代碼在github:https://github.com/lhy1234/spring-security

相關文章
相關標籤/搜索