日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

Spring Security 短信验证码登录(5)

發布時間:2023/12/31 javascript 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Spring Security 短信验证码登录(5) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

在Spring Security添加圖形驗證碼中,我們已經實現了基于Spring Boot + Spring Security的賬號密碼登錄,并集成了圖形驗證碼功能。時下另一種非常常見的網站登錄方式為手機短信驗證碼登錄,但Spring Security默認只提供了賬號密碼的登錄認證邏輯,所以要實現手機短信驗證碼登錄認證功能,我們需要模仿Spring Security賬號密碼登錄邏輯代碼來實現一套自己的認證邏輯。

1. 短信驗證碼生成

我們在Spring Security添加圖形驗證碼的基礎上來集成短信驗證碼登錄的功能。

和圖形驗證碼類似,我們先定義一個短信驗證碼對象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中加入生成短信驗證碼相關請求對應的方法:

@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);// 輸出驗證碼到控制臺代替短信發送服務System.out.println("您的登錄驗證碼為:" + smsCode.getCode() + ",有效時間為60秒");}private SmsCode createSMSCode() {String code = RandomStringUtils.randomNumeric(6);return new SmsCode(code, 60);}}

這里我們使用createSMSCode方法生成了一個6位的純數字隨機數,有效時間為60秒。然后通過SessionStrategy對象的setAttribute方法將短信驗證碼保存到了Session中,對應的key為SESSION_KEY_SMS_CODE。

至此,短信驗證碼生成模塊編寫完畢,下面開始改造登錄頁面。

2. 改造登錄頁

我們在登錄頁面中加入一個與手機短信驗證碼認證相關的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">發送驗證碼</a></span><button type="submit">登錄</button></div> </form>

其中a標簽的href屬性值對應我們的短信驗證碼生成方法的請求URL。Form的action對應處理短信驗證碼登錄方法的請求URL,這個方法下面在進行具體實現。同時,我們需要在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") // 登錄跳轉 URL.loginProcessingUrl("/login").successHandler(authenticationSuccessHandler) // 處理登錄成功.failureHandler(authenticationFailureHandler) // 處理登錄失敗.and().rememberMe().tokenRepository(persistentTokenRepository()) // 配置 token 持久化倉庫.tokenValiditySeconds(3600) // remember 過期時間,單為秒.userDetailsService(userDetailService) // 處理自動登錄邏輯.and().authorizeRequests() // 授權配置.antMatchers("/authentication/require", "/login.html", "/code/image", "/code/sms").permitAll().anyRequest() // 所有請求.authenticated() // 都需要認證.and().csrf().disable();}

重啟項目,訪問http://localhost:8080/login.html:

點擊發送驗證碼,控制臺輸出如下:

接下來開始實現使用短信驗證碼登錄認證邏輯。

3. 添加短信驗證碼認證

在Spring Security中,使用用戶名密碼認證的過程大致如下圖所示:

Spring Security使用UsernamePasswordAuthenticationFilter過濾器來攔截用戶名密碼認證請求,將用戶名和密碼封裝成一個UsernamePasswordToken對象交給AuthenticationManager處理。AuthenticationManager將挑出一個支持處理該類型Token的AuthenticationProvider(這里為DaoAuthenticationProvider,AuthenticationProvider的其中一個實現類)來進行認證,認證過程中DaoAuthenticationProvider將調用UserDetailService的loadUserByUsername方法來獲取UserDetails對象,如果UserDetails不為空并且密碼和用戶輸入的密碼匹配一致的話,則將認證信息保存到Session中,認證后我們便可以通過Authentication對象獲取到認證的信息了。

由于Spring Security并沒用提供短信驗證碼認證的流程,所以我們需要仿照上面這個流程來實現:

在這個流程中,我們自定義了一個名為SmsAuthenticationFitler的過濾器來攔截短信驗證碼登錄請求,并將手機號碼封裝到一個叫SmsAuthenticationToken的對象中。在Spring Security中,認證處理都需要通過AuthenticationManager來代理,所以這里我們依舊將SmsAuthenticationToken交由AuthenticationManager處理。接著我們需要定義一個支持處理SmsAuthenticationToken對象的SmsAuthenticationProvider,SmsAuthenticationProvider調用UserDetailService的loadUserByUsername方法來處理認證。與用戶名密碼認證不一樣的是,這里是通過SmsAuthenticationToken中的手機號去數據庫中查詢是否有與之對應的用戶,如果有,則將該用戶信息封裝到UserDetails對象中返回并將認證后的信息保存到Authentication對象中。

為了實現這個流程,我們需要定義SmsAuthenticationFitler、SmsAuthenticationToken和SmsAuthenticationProvider,并將這些組建組合起來添加到Spring Security中。下面我們來逐步實現這個過程。

3.1 定義SmsAuthenticationToken

查看UsernamePasswordAuthenticationToken的源碼,將其復制出來重命名為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屬性,從它的兩個構造函數可以看出,在認證之前principal存的是手機號,認證之后存的是用戶信息。UsernamePasswordAuthenticationToken原來還包含一個credentials屬性用于存放密碼,這里不需要就去掉了。

3.2 定義SmsAuthenticationFilter

定義完SmsAuthenticationToken后,我們接著定義用于處理短信驗證碼登錄請求的過濾器SmsAuthenticationFilter,同樣的復制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;} }

構造函數中指定了當請求為/login/mobile,請求方法為POST的時候該過濾器生效。mobileParameter屬性值為mobile,對應登錄頁面手機號輸入框的name屬性。attemptAuthentication方法從請求中獲取到mobile參數值,并調用SmsAuthenticationToken的SmsAuthenticationToken(String mobile)構造方法創建了一個SmsAuthenticationToken。下一步就如流程圖中所示的那樣,SmsAuthenticationFilter將SmsAuthenticationToken交給AuthenticationManager處理。

3.3 定義SmsAuthenticationProvider

在創建完SmsAuthenticationFilter后,我們需要創建一個支持處理該類型Token的類,即SmsAuthenticationProvider,該類需要實現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("未找到與該手機號對應的用戶");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方法用于編寫具體的身份認證邏輯。在authenticate方法中,我們從SmsAuthenticationToken中取出了手機號信息,并調用了UserDetailService的loadUserByUsername方法。該方法在用戶名密碼類型的認證中,主要邏輯是通過用戶名查詢用戶信息,如果存在該用戶并且密碼一致則認證成功;而在短信驗證碼認證的過程中,該方法需要通過手機號去查詢用戶,如果存在該用戶則認證通過。認證通過后接著調用SmsAuthenticationToken的SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities)構造函數構造一個認證通過的Token,包含了用戶信息和用戶權限。

你可能會問,為什么這一步沒有進行短信驗證碼的校驗呢?實際上短信驗證碼的校驗是在SmsAuthenticationFilter之前完成的,即只有當短信驗證碼正確以后才開始走認證的流程。所以接下來我們需要定一個過濾器來校驗短信驗證碼的正確性。

3.4 定義SmsCodeFilter

短信驗證碼的校驗邏輯其實和圖形驗證碼的校驗邏輯基本一致,所以我們在圖形驗證碼過濾器的基礎上稍作修改,代碼如下所示:

@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("驗證碼不存在,請重新發送!");}if (smsCode.isExpire()) {sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobile);throw new ValidateCodeException("驗證碼已過期,請重新發送!");}if (!StringUtils.equalsIgnoreCase(smsCode.getCode(), smsCodeInRequest)) {throw new ValidateCodeException("驗證碼不正確!");}sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobile);} }

方法的基本邏輯和之前定義的ValidateCodeFilter一致,這里不再贅述。

3.5 配置生效

在定義完所需的組件后,我們需要進行一些配置,將這些組件組合起來形成一個和上面流程圖對應的流程。創建一個配置類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,分別設置了AuthenticationManager、AuthenticationSuccessHandler和AuthenticationFailureHandler屬性。這些屬性都是來自SmsAuthenticationFilter繼承的AbstractAuthenticationProcessingFilter類中。

第二步配置SmsAuthenticationProvider,這一步只需要將我們自個的UserDetailService注入進來即可。

最后調用HttpSecurity的authenticationProvider方法指定了AuthenticationProvider為SmsAuthenticationProvider,并將SmsAuthenticationFilter過濾器添加到了UsernamePasswordAuthenticationFilter后面。

到這里我們已經將短信驗證碼認證的各個組件組合起來了,最后一步需要做的是配置短信驗證碼校驗過濾器,并且將短信驗證碼認證流程加入到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") // 登錄跳轉 URL.loginProcessingUrl("/login").successHandler(authenticationSuccessHandler) // 處理登錄成功.failureHandler(authenticationFailureHandler) // 處理登錄失敗.and().rememberMe().tokenRepository(persistentTokenRepository()) // 配置 token 持久化倉庫.tokenValiditySeconds(3600) // remember 過期時間,單為秒.userDetailsService(userDetailService) // 處理自動登錄邏輯.and().authorizeRequests() // 授權配置.antMatchers("/authentication/require", "/login.html", "/code/image", "/code/sms").permitAll().anyRequest() // 所有請求.authenticated() // 都需要認證.and().csrf().disable().apply(smsAuthenticationConfig); // 將短信驗證碼認證配置加到 Spring Security 中}}

4. 測試

重啟項目,訪問http://localhost:8080/login.html,點擊發送驗證碼,控制臺輸出如下:這個時候界面會跳轉到其他界面,返回一下就可以了,然后輸入驗證碼。

輸入該驗證碼,點擊登錄后頁面如下所示:

5. 項目地址

短信驗證登錄

總結

以上是生活随笔為你收集整理的Spring Security 短信验证码登录(5)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。