這篇文章主要講解了Spring Security實(shí)現(xiàn)短信驗(yàn)證碼登錄功能的方法,內(nèi)容清晰明了,對(duì)此有興趣的小伙伴可以學(xué)習(xí)一下,相信大家閱讀完之后會(huì)有幫助。
創(chuàng)新互聯(lián)公司主營(yíng)易門(mén)網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營(yíng)網(wǎng)站建設(shè)方案,成都app軟件開(kāi)發(fā),易門(mén)h5微信小程序定制開(kāi)發(fā)搭建,易門(mén)網(wǎng)站營(yíng)銷(xiāo)推廣歡迎易門(mén)等地區(qū)企業(yè)咨詢(xún)
之前文章都是基于用戶(hù)名密碼登錄,第六章圖形驗(yàn)證碼登錄其實(shí)還是用戶(hù)名密碼登錄,只不過(guò)多了一層圖形驗(yàn)證碼校驗(yàn)而已;Spring Security默認(rèn)提供的認(rèn)證流程就是用戶(hù)名密碼登錄,整個(gè)流程都已經(jīng)固定了,雖然提供了一些接口擴(kuò)展,但是有些時(shí)候我們就需要有自己特殊的身份認(rèn)證邏輯,比如用
前端代碼
手機(jī)號(hào): 短信驗(yàn)證碼: 發(fā)送驗(yàn)證碼
短信驗(yàn)證碼流程原理
短信驗(yàn)證碼登錄和用戶(hù)名密碼登錄對(duì)比
步驟流程
SmsAuthenticationFilter
過(guò)濾器處理,這個(gè)過(guò)濾器拿到請(qǐng)求以后會(huì)在登錄請(qǐng)求中拿到手機(jī)號(hào),然后封裝成自定義的一個(gè)SmsAuthenticationToken(未認(rèn)證)。SmsAuthenticationProvider
,用它來(lái)校驗(yàn)自己寫(xiě)的SmsAuthenticationToken的手機(jī)號(hào)信息。UserDetailsService
,把手機(jī)號(hào)傳給它讓它去讀用戶(hù)信息,去判斷是否能登錄,登錄成功的話(huà)再把SmsAuthenticationToken標(biāo)記為已認(rèn)證。在SmsAuthenticationFilter過(guò)濾器之前加一個(gè)過(guò)濾器來(lái)驗(yàn)證短信驗(yàn)證碼
。代碼實(shí)現(xiàn)
SmsCodeAuthenticationToken
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final Object principal; /** * 進(jìn)入SmsAuthenticationFilter時(shí),構(gòu)建一個(gè)未認(rèn)證的Token * * @param mobile */ public SmsCodeAuthenticationToken(String mobile) { super(null); this.principal = mobile; setAuthenticated(false); } /** * 認(rèn)證成功以后構(gòu)建為已認(rèn)證的Token * * @param principal * @param authorities */ 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"); } super.setAuthenticated(false); } @Override public void eraseCredentials() { super.eraseCredentials(); } }
SmsCodeAuthenticationFilter
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private String mobileParameter = "mobile"; private boolean postOnly = true; /** * 表示要處理的請(qǐng)求路徑 */ public SmsCodeAuthenticationFilter() { super(new AntPathRequestMatcher("/authentication/mobile", "POST")); } @Override 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); // 把請(qǐng)求信息設(shè)到Token中 setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } /** * 獲取手機(jī)號(hào) */ protected String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter); } protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } public void setMobileParameter(String usernameParameter) { Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); this.mobileParameter = usernameParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getMobileParameter() { return mobileParameter; } }
SmsAuthenticationProvider
public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; /** * 進(jìn)行身份認(rèn)證的邏輯 * * @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("無(wú)法獲取用戶(hù)信息"); } SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } /** * 表示支持校驗(yàn)的Token,這里是SmsCodeAuthenticationToken * * @param authentication * @return */ @Override public boolean supports(Class<?> authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } }
ValidateCodeFilter
@Component("validateCodeFilter") public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean { /** * 驗(yàn)證碼校驗(yàn)失敗處理器 */ @Autowired private AuthenticationFailureHandler authenticationFailureHandler; /** * 系統(tǒng)配置信息 */ @Autowired private SecurityProperties securityProperties; /** * 系統(tǒng)中的校驗(yàn)碼處理器 */ @Autowired private ValidateCodeProcessorHolder validateCodeProcessorHolder; /** * 存放所有需要校驗(yàn)驗(yàn)證碼的url */ private MapurlMap = new HashMap<>(); /** * 驗(yàn)證請(qǐng)求url與配置的url是否匹配的工具類(lèi) */ private AntPathMatcher pathMatcher = new AntPathMatcher(); /** * 初始化要攔截的url配置信息 */ @Override public void afterPropertiesSet() throws ServletException { super.afterPropertiesSet(); urlMap.put("/authentication/mobile", ValidateCodeType.SMS); addUrlToMap(securityProperties.getCode().getSms().getUrl(), ValidateCodeType.SMS); } /** * 講系統(tǒng)中配置的需要校驗(yàn)驗(yàn)證碼的URL根據(jù)校驗(yàn)的類(lèi)型放入map * * @param urlString * @param type */ protected void addUrlToMap(String urlString, ValidateCodeType type) { if (StringUtils.isNotBlank(urlString)) { String[] urls = StringUtils.splitByWholeSeparatorPreserveAllTokens(urlString, ","); for (String url : urls) { urlMap.put(url, type); } } } /** * 驗(yàn)證短信驗(yàn)證碼 * * @param request * @param response * @param chain * @throws ServletException * @throws IOException */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { ValidateCodeType type = getValidateCodeType(request); if (type != null) { logger.info("校驗(yàn)請(qǐng)求(" + request.getRequestURI() + ")中的驗(yàn)證碼,驗(yàn)證碼類(lèi)型" + type); try { // 進(jìn)行驗(yàn)證碼的校驗(yàn) validateCodeProcessorHolder.findValidateCodeProcessor(type) .validate(new ServletWebRequest(request, response)); logger.info("驗(yàn)證碼校驗(yàn)通過(guò)"); } catch (ValidateCodeException exception) { // 如果校驗(yàn)拋出異常,則交給我們之前文章定義的異常處理器進(jìn)行處理 authenticationFailureHandler.onAuthenticationFailure(request, response, exception); return; } } // 繼續(xù)調(diào)用后邊的過(guò)濾器 chain.doFilter(request, response); } /** * 獲取校驗(yàn)碼的類(lèi)型,如果當(dāng)前請(qǐng)求不需要校驗(yàn),則返回null * * @param request * @return */ private ValidateCodeType getValidateCodeType(HttpServletRequest request) { ValidateCodeType result = null; if (!StringUtils.equalsIgnoreCase(request.getMethod(), "GET")) { Set urls = urlMap.keySet(); for (String url : urls) { if (pathMatcher.match(url, request.getRequestURI())) { result = urlMap.get(url); } } } return result; } }
添加配置
SmsCodeAuthenticationSecurityConfig
作用:配置SmsCodeAuthenticationFilter,后面需要把這些配置加到主配置類(lèi)BrowserSecurityConfig
@Component public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter{ @Autowired private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler; @Autowired private AuthenticationFailureHandler meicloudAuthenticationFailureHandler; @Autowired private UserDetailsService userDetailsService; @Autowired private PersistentTokenRepository persistentTokenRepository; @Override public void configure(HttpSecurity http) throws Exception { SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(); // 設(shè)置AuthenticationManager smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); // 設(shè)置登錄成功處理器 smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(meicloudAuthenticationSuccessHandler); // 設(shè)置登錄失敗處理器 smsCodeAuthenticationFilter.setAuthenticationFailureHandler(meicloudAuthenticationFailureHandler); String key = UUID.randomUUID().toString(); smsCodeAuthenticationFilter.setRememberMeServices(new PersistentTokenBasedRememberMeServices(key, userDetailsService, persistentTokenRepository)); SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(); smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService); // 將自己寫(xiě)的Provider加到Provider集合里去 http.authenticationProvider(smsCodeAuthenticationProvider) .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
BrowserSecurityConfig
作用:主配置類(lèi);添加短信驗(yàn)證碼配置類(lèi)、添加SmsCodeAuthenticationSecurityConfig配置
@Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Autowired private SecurityProperties securityProperties; @Autowired private DataSource dataSource; @Autowired private UserDetailsService userDetailsService; @Autowired private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler; @Autowired private AuthenticationFailureHandler meicloudAuthenticationFailureHandler; @Autowired private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; @Override protected void configure(HttpSecurity http) throws Exception { // 驗(yàn)證碼校驗(yàn)過(guò)濾器 ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(); // 將驗(yàn)證碼校驗(yàn)過(guò)濾器加到 UsernamePasswordAuthenticationFilter 過(guò)濾器之前 http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) .formLogin() // 當(dāng)用戶(hù)登錄認(rèn)證時(shí)默認(rèn)跳轉(zhuǎn)的頁(yè)面 .loginPage("/authentication/require") // 以下這行 UsernamePasswordAuthenticationFilter 會(huì)知道要處理表單的 /authentication/form 請(qǐng)求,而不是默認(rèn)的 /login .loginProcessingUrl("/authentication/form") .successHandler(meicloudAuthenticationSuccessHandler) .failureHandler(meicloudAuthenticationFailureHandler) // 配置記住我功能 .and() .rememberMe() // 配置TokenRepository .tokenRepository(persistentTokenRepository()) // 配置Token過(guò)期時(shí)間 .tokenValiditySeconds(3600) // 最終拿到用戶(hù)名之后,使用UserDetailsService去做登錄 .userDetailsService(userDetailsService) .and() .authorizeRequests() // 排除對(duì) "/authentication/require" 和 "/meicloud-signIn.html" 的身份驗(yàn)證 .antMatchers("/authentication/require", securityProperties.getBrowser().getSignInPage(), "/code/*").permitAll() // 表示所有請(qǐng)求都需要身份驗(yàn)證 .anyRequest() .authenticated() .and() .csrf().disable()// 暫時(shí)把跨站請(qǐng)求偽造的功能關(guān)閉掉 // 相當(dāng)于把smsCodeAuthenticationSecurityConfig里的配置加到上面這些配置的后面 .apply(smsCodeAuthenticationSecurityConfig); } /** * 記住我功能的Token存取器配置 * * @return */ @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); // 啟動(dòng)的時(shí)候自動(dòng)創(chuàng)建表,建表語(yǔ)句 JdbcTokenRepositoryImpl 已經(jīng)都寫(xiě)好了 tokenRepository.setCreateTableOnStartup(true); return tokenRepository; } }
看完上述內(nèi)容,是不是對(duì)Spring Security實(shí)現(xiàn)短信驗(yàn)證碼登錄功能的方法有進(jìn)一步的了解,如果還想學(xué)習(xí)更多內(nèi)容,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。