javascript
Spring Security构建Rest服务-0702-短信验证码登录
先來看下?Spring Security密碼登錄大概流程,模擬這個流程,開發(fā)短信登錄流程
1,密碼登錄請求發(fā)送給過濾器 UsernamePasswordAuthenticationFilter?
2,過濾器拿出用戶名密碼組裝成 UsernamePasswordAuthenticationToken 對象傳給AuthenticationManager
3,AuthenticationManager 會從一堆 AuthenticationProvider 里選出一個Provider 處理認(rèn)證請求。挑選的依據(jù)是AuthenticationProvider 里有個
boolean supports(Class<?> authentication);方法,判斷當(dāng)前的provider是否支持傳進(jìn)的token,如果支持就用這個provider認(rèn)證這個token,并調(diào)用authenticate() 方法 進(jìn)行認(rèn)證
4,認(rèn)證過程會調(diào)用UserDetailsService獲取用戶信息,跟傳進(jìn)來的登錄信息做比對。認(rèn)證通過會把UsernamePasswordAuthenticationToken做一個標(biāo)識? ?標(biāo)記已認(rèn)證,放進(jìn)session。
做短信登錄,不能在這個流程上改,這是兩種不同的登錄方式,混在一起代碼質(zhì)量不好,需要仿照這個流程寫一套自己的流程:
SmsAuthenticationFilter:攔截短信登錄請求,從請求中獲取手機號,封裝成 SmsAuthenticationToken 也會傳給AuthenticationManager,AuthenticationManager會找適合的provider,自定義SmsAuthenticationProvider校驗SmsAuthenticationToken 里手機號信息。也會調(diào)UserDetailsService 看是否能登錄,能的話標(biāo)記為已登錄。
其中SmsAuthenticationFilter 參考UsernamePasswordAuthenticationFilter寫,SmsCodeAuthenticationToken參考UsernamePasswordAuthenticationToken寫,其實就是就是復(fù)制粘貼改改
從上圖可知,需要寫三個類:
1,SmsAuthenticationToken:復(fù)制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);}@Overridepublic void eraseCredentials() {super.eraseCredentials();}@Overridepublic 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,登錄表單的urlsuper(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交給AuthenticationManagerreturn 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);這句話就是進(jìn)到 SmsCodeAuthenticationProvider 先調(diào)用?supports() 方法,通過后,再調(diào)用?authenticate()方法進(jìn)行認(rèn)證
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 認(rèn)證時候需要用的一個Provider* ClassName: SmsCodeAuthenticationProvider * @Description: TODO* @author lihaoyang* @date 2018年3月8日*/ public class SmsCodeAuthenticationProvider implements AuthenticationProvider {private UserDetailsService userDetailsService;/*** 認(rèn)證*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {//能進(jìn)到這說明authentication是SmsCodeAuthenticationToken,轉(zhuǎn)一下SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken)authentication;//token.getPrincipal()就是手機號 mobileUserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());//認(rèn)證沒通過if(user == null){throw new InternalAuthenticationServiceException("無法獲取用戶信息");}//認(rèn)證通過SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());//把認(rèn)證之前得token里存的用戶信息賦值給認(rèn)證后的token對象 authenticationResult.setDetails(authenticationToken.getDetails());return authenticationResult;}/*** 告訴AuthenticationManager,如果是SmsCodeAuthenticationToken的話用這個類處理*/@Overridepublic boolean supports(Class<?> authentication) {//判斷傳進(jìn)來的authentication是不是SmsCodeAuthenticationToken類型的return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);}public UserDetailsService getUserDetailsService() {return userDetailsService;}public void setUserDetailsService(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}}短信 驗證碼過濾器,照著圖片驗證碼過濾器寫,其實可以重構(gòu),不會弄:
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提供的工具,保證過濾器每次只會被調(diào)用一次* 實現(xiàn) InitializingBean接口的目的:* 在其他參數(shù)都組裝完畢的時候,初始化需要攔截的urls的值* @author lihaoyang* @date 2018年3月2日*/ public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean{private Logger logger = LoggerFactory.getLogger(getClass());//認(rèn)證失敗處理器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的方法,設(shè)置需要攔截的urls*/@Overridepublic void afterPropertiesSet() throws ServletException {super.afterPropertiesSet();//讀取配置的攔截的urlsString[] 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);}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {//如果是 登錄請求 則執(zhí)行 // if(StringUtils.equals("/authentication/form", request.getRequestURI()) // &&StringUtils.equalsIgnoreCase(request.getMethod(), "post")){ // try { // validate(new ServletWebRequest(request)); // } catch (ValidateCodeException e) { // //調(diào)用錯誤處理器,最終調(diào)用自己的 // authenticationFailureHandler.onAuthenticationFailure(request, response, e); // return ;//結(jié)束方法,不再調(diào)用過濾器鏈 // } // }/*** 可配置的驗證碼校驗* 判斷請求的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) {//調(diào)用錯誤處理器,最終調(diào)用自己的 authenticationFailureHandler.onAuthenticationFailure(request, response, e);return ;//結(jié)束方法,不再調(diào)用過濾器鏈 }}//不是登錄請求,調(diào)用其它過濾器鏈 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> {@Autowiredprivate AuthenticationFailureHandler imoocAuthenticationFailureHandler;@Autowiredprivate AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;@Autowiredprivate UserDetailsService userDetailsService;@Overridepublic void configure(HttpSecurity http) throws Exception {//1,配置短信驗證碼過濾器SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));//設(shè)置認(rèn)證失敗成功處理器 smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);smsCodeAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);//配置pproviderSmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);http.authenticationProvider(smsCodeAuthenticationProvider).addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);}}最后在BrowserSecurityConfig里配置短信驗證碼
@Configuration //這是一個配置 public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{//讀取用戶配置的登錄頁配置 @Autowiredprivate SecurityProperties securityProperties;//自定義的登錄成功后的處理器 @Autowiredprivate AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;//自定義的認(rèn)證失敗后的處理器 @Autowiredprivate AuthenticationFailureHandler imoocAuthenticationFailureHandler;//數(shù)據(jù)源 @Autowiredprivate DataSource dataSource;@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;@Autowiredprivate SpringSocialConfigurer imoocSocialSecurityConfig;//注意是org.springframework.security.crypto.password.PasswordEncoder @Beanpublic PasswordEncoder passwordencoder(){//BCryptPasswordEncoder implements PasswordEncoderreturn new BCryptPasswordEncoder();}/*** 記住我TokenRepository配置,在登錄成功后執(zhí)行* 登錄成功后往數(shù)據(jù)庫存token的* @Description: 記住我TokenRepository配置* @param @return JdbcTokenRepositoryImpl* @return PersistentTokenRepository * @throws* @author lihaoyang* @date 2018年3月5日*/@Beanpublic PersistentTokenRepository persistentTokenRepository(){JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();jdbcTokenRepository.setDataSource(dataSource);//啟動時自動生成相應(yīng)表,可以在JdbcTokenRepositoryImpl里自己執(zhí)行CREATE_TABLE_SQL腳本生成表//第二次啟動表已存在,需要注釋 // jdbcTokenRepository.setCreateTableOnStartup(true);return jdbcTokenRepository;}//版本二:可配置的登錄頁 @Overrideprotected 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();//實現(xiàn)需要認(rèn)證的接口跳轉(zhuǎn)表單登錄,安全=認(rèn)證+授權(quán)//http.httpBasic() //這個就是默認(rèn)的彈框認(rèn)證// http .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class) // .apply(imoocSocialSecurityConfig)//社交登錄 // .and()//把驗證碼過濾器加載登錄過濾器前邊.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)//表單認(rèn)證相關(guān)配置 .formLogin() .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL) //處理用戶認(rèn)證BrowserSecurityController//登錄過濾器UsernamePasswordAuthenticationFilter默認(rèn)登錄的url是"/login",在這能改 .loginProcessingUrl(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM) .successHandler(imoocAuthenticationSuccessHandler)//自定義的認(rèn)證后處理器.failureHandler(imoocAuthenticationFailureHandler) //登錄失敗后的處理 .and()//記住我相關(guān)配置 .rememberMe().tokenRepository(persistentTokenRepository())//TokenRepository,登錄成功后往數(shù)據(jù)庫存token的.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())//記住我秒數(shù).userDetailsService(userDetailsService) //記住我成功后,調(diào)用userDetailsService查詢用戶信息 .and()//授權(quán)相關(guān)的配置 .authorizeRequests() // /authentication/require:處理登錄,securityProperties.getBrowser().getLoginPage():用戶配置的登錄頁 .antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,securityProperties.getBrowser().getLoginPage(),//放過登錄頁不過濾,否則報錯SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX+"/*").permitAll() //驗證碼.anyRequest() //任何請求.authenticated() //都需要身份認(rèn)證 .and().csrf().disable() //關(guān)閉csrf防護(hù).apply(smsCodeAuthenticationSecurityConfig);//把短信驗證碼配置應(yīng)用上 } }訪問登陸頁,點擊發(fā)送驗證碼模擬發(fā)送驗證碼
輸入后臺打印的驗證碼
登錄成功:
完整代碼在github:https://github.com/lhy1234/spring-security
?
轉(zhuǎn)載于:https://www.cnblogs.com/lihaoyang/p/8523279.html
總結(jié)
以上是生活随笔為你收集整理的Spring Security构建Rest服务-0702-短信验证码登录的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MVVM模式理解
- 下一篇: gradle idea java ssm