javascript
Spring Security 短信验证码登录(5)
在Spring Security添加圖形驗證碼中,我們已經(jīng)實現(xiàn)了基于Spring Boot + Spring Security的賬號密碼登錄,并集成了圖形驗證碼功能。時下另一種非常常見的網(wǎng)站登錄方式為手機短信驗證碼登錄,但Spring Security默認(rèn)只提供了賬號密碼的登錄認(rèn)證邏輯,所以要實現(xiàn)手機短信驗證碼登錄認(rèn)證功能,我們需要模仿Spring Security賬號密碼登錄邏輯代碼來實現(xiàn)一套自己的認(rèn)證邏輯。
1. 短信驗證碼生成
我們在Spring Security添加圖形驗證碼的基礎(chǔ)上來集成短信驗證碼登錄的功能。
和圖形驗證碼類似,我們先定義一個短信驗證碼對象SmsCode:
@Data public class SmsCode {private String code;private LocalDateTime expireTime;public SmsCode(String code, int expireIn) {this.code = code;this.expireTime = LocalDateTime.now().plusSeconds(expireIn);}public SmsCode(String code, LocalDateTime expireTime) {this.code = code;this.expireTime = expireTime;}public boolean isExpire() {return LocalDateTime.now().isAfter(expireTime);} }SmsCode對象包含了兩個屬性:code驗證碼和expireTime過期時間。isExpire方法用于判斷短信驗證碼是否已過期。
接著在ValidateCodeController中加入生成短信驗證碼相關(guān)請求對應(yīng)的方法:
@RestController public class ValidateController {public final static String SESSION_KEY_SMS_CODE = "SESSION_KEY_SMS_CODE";private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();@GetMapping("/code/sms")public void createSmsCode(HttpServletRequest request, HttpServletResponse response, String mobile) throws IOException {SmsCode smsCode = createSMSCode();sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_SMS_CODE + mobile, smsCode);// 輸出驗證碼到控制臺代替短信發(fā)送服務(wù)System.out.println("您的登錄驗證碼為:" + smsCode.getCode() + ",有效時間為60秒");}private SmsCode createSMSCode() {String code = RandomStringUtils.randomNumeric(6);return new SmsCode(code, 60);}}這里我們使用createSMSCode方法生成了一個6位的純數(shù)字隨機數(shù),有效時間為60秒。然后通過SessionStrategy對象的setAttribute方法將短信驗證碼保存到了Session中,對應(yīng)的key為SESSION_KEY_SMS_CODE。
至此,短信驗證碼生成模塊編寫完畢,下面開始改造登錄頁面。
2. 改造登錄頁
我們在登錄頁面中加入一個與手機短信驗證碼認(rèn)證相關(guān)的Form表單:
<form class="login-page" action="/login/mobile" method="post"><div class="form"><h3>短信驗證碼登錄</h3><input type="text" placeholder="手機號" name="mobile" value="17777777777" required="required"/><span style="display: inline"><input type="text" name="smsCode" placeholder="短信驗證碼" style="width: 50%;"/><a href="/code/sms?mobile=17777777777">發(fā)送驗證碼</a></span><button type="submit">登錄</button></div> </form>其中a標(biāo)簽的href屬性值對應(yīng)我們的短信驗證碼生成方法的請求URL。Form的action對應(yīng)處理短信驗證碼登錄方法的請求URL,這個方法下面在進(jìn)行具體實現(xiàn)。同時,我們需要在Spring Security中配置/code/sms路徑免驗證:
@Overrideprotected void configure(HttpSecurity http) throws Exception {http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加驗證碼校驗過濾器.formLogin() // 表單方式//http.httpBasic() // HTTP Basic方式 // .loginPage("/login.html").loginPage("/authentication/require") // 登錄跳轉(zhuǎn) URL.loginProcessingUrl("/login").successHandler(authenticationSuccessHandler) // 處理登錄成功.failureHandler(authenticationFailureHandler) // 處理登錄失敗.and().rememberMe().tokenRepository(persistentTokenRepository()) // 配置 token 持久化倉庫.tokenValiditySeconds(3600) // remember 過期時間,單為秒.userDetailsService(userDetailService) // 處理自動登錄邏輯.and().authorizeRequests() // 授權(quán)配置.antMatchers("/authentication/require", "/login.html", "/code/image", "/code/sms").permitAll().anyRequest() // 所有請求.authenticated() // 都需要認(rèn)證.and().csrf().disable();}重啟項目,訪問http://localhost:8080/login.html:
點擊發(fā)送驗證碼,控制臺輸出如下:
接下來開始實現(xiàn)使用短信驗證碼登錄認(rèn)證邏輯。
3. 添加短信驗證碼認(rèn)證
在Spring Security中,使用用戶名密碼認(rèn)證的過程大致如下圖所示:
Spring Security使用UsernamePasswordAuthenticationFilter過濾器來攔截用戶名密碼認(rèn)證請求,將用戶名和密碼封裝成一個UsernamePasswordToken對象交給AuthenticationManager處理。AuthenticationManager將挑出一個支持處理該類型Token的AuthenticationProvider(這里為DaoAuthenticationProvider,AuthenticationProvider的其中一個實現(xiàn)類)來進(jìn)行認(rèn)證,認(rèn)證過程中DaoAuthenticationProvider將調(diào)用UserDetailService的loadUserByUsername方法來獲取UserDetails對象,如果UserDetails不為空并且密碼和用戶輸入的密碼匹配一致的話,則將認(rèn)證信息保存到Session中,認(rèn)證后我們便可以通過Authentication對象獲取到認(rèn)證的信息了。
由于Spring Security并沒用提供短信驗證碼認(rèn)證的流程,所以我們需要仿照上面這個流程來實現(xiàn):
在這個流程中,我們自定義了一個名為SmsAuthenticationFitler的過濾器來攔截短信驗證碼登錄請求,并將手機號碼封裝到一個叫SmsAuthenticationToken的對象中。在Spring Security中,認(rèn)證處理都需要通過AuthenticationManager來代理,所以這里我們依舊將SmsAuthenticationToken交由AuthenticationManager處理。接著我們需要定義一個支持處理SmsAuthenticationToken對象的SmsAuthenticationProvider,SmsAuthenticationProvider調(diào)用UserDetailService的loadUserByUsername方法來處理認(rèn)證。與用戶名密碼認(rèn)證不一樣的是,這里是通過SmsAuthenticationToken中的手機號去數(shù)據(jù)庫中查詢是否有與之對應(yīng)的用戶,如果有,則將該用戶信息封裝到UserDetails對象中返回并將認(rèn)證后的信息保存到Authentication對象中。
為了實現(xiàn)這個流程,我們需要定義SmsAuthenticationFitler、SmsAuthenticationToken和SmsAuthenticationProvider,并將這些組建組合起來添加到Spring Security中。下面我們來逐步實現(xiàn)這個過程。
3.1 定義SmsAuthenticationToken
查看UsernamePasswordAuthenticationToken的源碼,將其復(fù)制出來重命名為SmsAuthenticationToken,并稍作修改,修改后的代碼如下所示:
public class SmsAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;private final Object principal;public SmsAuthenticationToken(String mobile) {super(null);this.principal = mobile;setAuthenticated(false);}public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;super.setAuthenticated(true); // must use super, as we override}@Overridepublic Object getCredentials() {return null;}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();} }SmsAuthenticationToken包含一個principal屬性,從它的兩個構(gòu)造函數(shù)可以看出,在認(rèn)證之前principal存的是手機號,認(rèn)證之后存的是用戶信息。UsernamePasswordAuthenticationToken原來還包含一個credentials屬性用于存放密碼,這里不需要就去掉了。
3.2 定義SmsAuthenticationFilter
定義完SmsAuthenticationToken后,我們接著定義用于處理短信驗證碼登錄請求的過濾器SmsAuthenticationFilter,同樣的復(fù)制UsernamePasswordAuthenticationFilter源碼并稍作修改:
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {public static final String MOBILE_KEY = "mobile";private String mobileParameter = MOBILE_KEY;private boolean postOnly = true;public SmsAuthenticationFilter() {super(new AntPathRequestMatcher("/login/mobile", "POST"));}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();SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}protected String obtainMobile(HttpServletRequest request) {return request.getParameter(mobileParameter);}protected void setDetails(HttpServletRequest request,SmsAuthenticationToken authRequest) {authRequest.setDetails(authenticationDetailsSource.buildDetails(request));}public void setMobileParameter(String mobileParameter) {Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");this.mobileParameter = mobileParameter;}public void setPostOnly(boolean postOnly) {this.postOnly = postOnly;}public final String getMobileParameter() {return mobileParameter;} }構(gòu)造函數(shù)中指定了當(dāng)請求為/login/mobile,請求方法為POST的時候該過濾器生效。mobileParameter屬性值為mobile,對應(yīng)登錄頁面手機號輸入框的name屬性。attemptAuthentication方法從請求中獲取到mobile參數(shù)值,并調(diào)用SmsAuthenticationToken的SmsAuthenticationToken(String mobile)構(gòu)造方法創(chuàng)建了一個SmsAuthenticationToken。下一步就如流程圖中所示的那樣,SmsAuthenticationFilter將SmsAuthenticationToken交給AuthenticationManager處理。
3.3 定義SmsAuthenticationProvider
在創(chuàng)建完SmsAuthenticationFilter后,我們需要創(chuàng)建一個支持處理該類型Token的類,即SmsAuthenticationProvider,該類需要實現(xiàn)AuthenticationProvider的兩個抽象方法:
public class SmsAuthenticationProvider implements AuthenticationProvider {private UserDetailService userDetailService;@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;UserDetails userDetails = userDetailService.loadUserByUsername((String) authenticationToken.getPrincipal());if (userDetails == null)throw new InternalAuthenticationServiceException("未找到與該手機號對應(yīng)的用戶");SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());authenticationResult.setDetails(authenticationToken.getDetails());return authenticationResult;}@Overridepublic boolean supports(Class<?> aClass) {return SmsAuthenticationToken.class.isAssignableFrom(aClass);}public UserDetailService getUserDetailService() {return userDetailService;}public void setUserDetailService(UserDetailService userDetailService) {this.userDetailService = userDetailService;} }其中supports方法指定了支持處理的Token類型為SmsAuthenticationToken,authenticate方法用于編寫具體的身份認(rèn)證邏輯。在authenticate方法中,我們從SmsAuthenticationToken中取出了手機號信息,并調(diào)用了UserDetailService的loadUserByUsername方法。該方法在用戶名密碼類型的認(rèn)證中,主要邏輯是通過用戶名查詢用戶信息,如果存在該用戶并且密碼一致則認(rèn)證成功;而在短信驗證碼認(rèn)證的過程中,該方法需要通過手機號去查詢用戶,如果存在該用戶則認(rèn)證通過。認(rèn)證通過后接著調(diào)用SmsAuthenticationToken的SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities)構(gòu)造函數(shù)構(gòu)造一個認(rèn)證通過的Token,包含了用戶信息和用戶權(quán)限。
你可能會問,為什么這一步?jīng)]有進(jìn)行短信驗證碼的校驗?zāi)?#xff1f;實際上短信驗證碼的校驗是在SmsAuthenticationFilter之前完成的,即只有當(dāng)短信驗證碼正確以后才開始走認(rèn)證的流程。所以接下來我們需要定一個過濾器來校驗短信驗證碼的正確性。
3.4 定義SmsCodeFilter
短信驗證碼的校驗邏輯其實和圖形驗證碼的校驗邏輯基本一致,所以我們在圖形驗證碼過濾器的基礎(chǔ)上稍作修改,代碼如下所示:
@Component public class SmsCodeFilter extends OncePerRequestFilter {@Autowiredprivate AuthenticationFailureHandler authenticationFailureHandler;private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,FilterChain filterChain) throws ServletException, IOException {if (StringUtils.equalsIgnoreCase("/login/mobile", httpServletRequest.getRequestURI())&& StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "post")) {try {validateSmsCode(new ServletWebRequest(httpServletRequest));} catch (ValidateCodeException e) {authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);return;}}filterChain.doFilter(httpServletRequest, httpServletResponse);}private void validateSmsCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {String smsCodeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "smsCode");String mobile = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "mobile");SmsCode smsCode = (SmsCode) sessionStrategy.getAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobile);if (StringUtils.isBlank(smsCodeInRequest)) {throw new ValidateCodeException("驗證碼不能為空!");}if (smsCode == null) {throw new ValidateCodeException("驗證碼不存在,請重新發(fā)送!");}if (smsCode.isExpire()) {sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobile);throw new ValidateCodeException("驗證碼已過期,請重新發(fā)送!");}if (!StringUtils.equalsIgnoreCase(smsCode.getCode(), smsCodeInRequest)) {throw new ValidateCodeException("驗證碼不正確!");}sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobile);} }方法的基本邏輯和之前定義的ValidateCodeFilter一致,這里不再贅述。
3.5 配置生效
在定義完所需的組件后,我們需要進(jìn)行一些配置,將這些組件組合起來形成一個和上面流程圖對應(yīng)的流程。創(chuàng)建一個配置類SmsAuthenticationConfig:
@Component public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {@Autowiredprivate AuthenticationSuccessHandler authenticationSuccessHandler;@Autowiredprivate AuthenticationFailureHandler authenticationFailureHandler;@Autowiredprivate UserDetailService userDetailService;@Overridepublic void configure(HttpSecurity http) throws Exception {SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();smsAuthenticationProvider.setUserDetailService(userDetailService);http.authenticationProvider(smsAuthenticationProvider).addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);} }在流程中第一步需要配置SmsAuthenticationFilter,分別設(shè)置了AuthenticationManager、AuthenticationSuccessHandler和AuthenticationFailureHandler屬性。這些屬性都是來自SmsAuthenticationFilter繼承的AbstractAuthenticationProcessingFilter類中。
第二步配置SmsAuthenticationProvider,這一步只需要將我們自個的UserDetailService注入進(jìn)來即可。
最后調(diào)用HttpSecurity的authenticationProvider方法指定了AuthenticationProvider為SmsAuthenticationProvider,并將SmsAuthenticationFilter過濾器添加到了UsernamePasswordAuthenticationFilter后面。
到這里我們已經(jīng)將短信驗證碼認(rèn)證的各個組件組合起來了,最后一步需要做的是配置短信驗證碼校驗過濾器,并且將短信驗證碼認(rèn)證流程加入到Spring Security中。在SecurityConfig的configure方法中添加如下配置:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate MyAuthenticationSuccessHandler authenticationSuccessHandler;@Autowiredprivate MyAuthenticationFailureHandler authenticationFailureHandler;@Autowiredprivate ValidateCodeFilter validateCodeFilter;@Autowiredprivate DataSource dataSource;@Autowiredprivate UserDetailService userDetailService;@Autowiredprivate SmsCodeFilter smsCodeFilter;@Autowiredprivate SmsAuthenticationConfig smsAuthenticationConfig;/*** 處理自動登錄** @return*/@Beanpublic PersistentTokenRepository persistentTokenRepository() {JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();jdbcTokenRepository.setDataSource(dataSource);jdbcTokenRepository.setCreateTableOnStartup(false);return jdbcTokenRepository;}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加驗證碼校驗過濾器.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加短信驗證碼校驗過濾器.formLogin() // 表單方式//http.httpBasic() // HTTP Basic方式 // .loginPage("/login.html").loginPage("/authentication/require") // 登錄跳轉(zhuǎn) URL.loginProcessingUrl("/login").successHandler(authenticationSuccessHandler) // 處理登錄成功.failureHandler(authenticationFailureHandler) // 處理登錄失敗.and().rememberMe().tokenRepository(persistentTokenRepository()) // 配置 token 持久化倉庫.tokenValiditySeconds(3600) // remember 過期時間,單為秒.userDetailsService(userDetailService) // 處理自動登錄邏輯.and().authorizeRequests() // 授權(quán)配置.antMatchers("/authentication/require", "/login.html", "/code/image", "/code/sms").permitAll().anyRequest() // 所有請求.authenticated() // 都需要認(rèn)證.and().csrf().disable().apply(smsAuthenticationConfig); // 將短信驗證碼認(rèn)證配置加到 Spring Security 中}}4. 測試
重啟項目,訪問http://localhost:8080/login.html,點擊發(fā)送驗證碼,控制臺輸出如下:這個時候界面會跳轉(zhuǎn)到其他界面,返回一下就可以了,然后輸入驗證碼。
輸入該驗證碼,點擊登錄后頁面如下所示:
5. 項目地址
短信驗證登錄
總結(jié)
以上是生活随笔為你收集整理的Spring Security 短信验证码登录(5)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: kylin云平台搭建问题
- 下一篇: 使用proxy_pool来为爬虫程序自动