先來看下 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