一、短信登錄驗證機制原理分析
了解短信驗證碼的登陸機制之前,我們首先是要了解用戶賬號密碼登陸的機制是如何的,我們來簡要分析一下Spring Security是如何驗證基于用戶名和密碼登錄方式的,
分析完畢之后,再一起思考如何將短信登錄驗證方式集成到Spring Security中。
1、賬號密碼登陸的流程
一般賬號密碼登陸都有附帶 圖形驗證碼 和 記住我功能 ,那么它的大致流程是這樣的。
1、 用戶在輸入用戶名,賬號、圖片驗證碼后點擊登陸。那么對于springSceurity首先會進入短信驗證碼Filter,因為在配置的時候會把它配置在
UsernamePasswordAuthenticationFilter之前,把當前的驗證碼的信息跟存在session的圖片驗證碼的驗證碼進行校驗。2、短信驗證碼通過后,進入 UsernamePasswordAuthenticationFilter 中,根據輸入的用戶名和密碼信息,構造出一個暫時沒有鑒權的
UsernamePasswordAuthenticationToken,并將 UsernamePasswordAuthenticationToken 交給 AuthenticationManager 處理。3、AuthenticationManager 本身并不做驗證處理,他通過 for-each 遍歷找到符合當前登錄方式的一個 AuthenticationProvider,并交給它進行驗證處理
,對于用戶名密碼登錄方式,這個 Provider 就是 DaoAuthenticationProvider。4、在這個 Provider 中進行一系列的驗證處理,如果驗證通過,就會重新構造一個添加了鑒權的 UsernamePasswordAuthenticationToken,并將這個
token 傳回到 UsernamePasswordAuthenticationFilter 中。5、在該 Filter 的父類 AbstractAuthenticationProcessingFilter 中,會根據上一步驗證的結果,跳轉到 successHandler 或者是 failureHandler。
流程圖
2、短信驗證碼登陸流程
因為短信登錄的方式并沒有集成到Spring Security中,所以往往還需要我們自己開發短信登錄邏輯,將其集成到Spring Security中,那么這里我們就模仿賬號
密碼登陸來實現短信驗證碼登陸。
1、用戶名密碼登錄有個 UsernamePasswordAuthenticationFilter,我們搞一個SmsAuthenticationFilter,代碼粘過來改一改。
2、用戶名密碼登錄需要UsernamePasswordAuthenticationToken,我們搞一個SmsAuthenticationToken,代碼粘過來改一改。
3、用戶名密碼登錄需要DaoAuthenticationProvider,我們模仿它也 implenments AuthenticationProvider,叫做 SmsAuthenticationProvider。
這個圖是網上找到,自己不想畫了
我們自己搞了上面三個類以后,想要實現的效果如上圖所示。當我們使用短信驗證碼登錄的時候:
1、先經過 SmsAuthenticationFilter,構造一個沒有鑒權的 SmsAuthenticationToken,然后交給 AuthenticationManager處理。
2、AuthenticationManager 通過 for-each 挑選出一個合適的 provider 進行處理,當然我們希望這個 provider 要是 SmsAuthenticationProvider。
3、驗證通過后,重新構造一個有鑒權的SmsAuthenticationToken,并返回給SmsAuthenticationFilter。
filter 根據上一步的驗證結果,跳轉到成功或者失敗的處理邏輯。
二、代碼實現
1、SmsAuthenticationToken
首先我們編寫 SmsAuthenticationToken,這里直接參考 UsernamePasswordAuthenticationToken 源碼,直接粘過來,改一改。
說明
principal 原本代表用戶名,這里保留,只是代表了手機號碼。
credentials 原本代碼密碼,短信登錄用不到,直接刪掉。
SmsCodeAuthenticationToken() 兩個構造方法一個是構造沒有鑒權的,一個是構造有鑒權的。
剩下的幾個方法去除無用屬性即可。
代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; /** * 在 UsernamePasswordAuthenticationToken 中該字段代表登錄的用戶名, * 在這里就代表登錄的手機號碼 */ private final Object principal; /** * 構建一個沒有鑒權的 SmsCodeAuthenticationToken */ public SmsCodeAuthenticationToken(Object principal) { super ( null ); this .principal = principal; setAuthenticated( false ); } /** * 構建擁有鑒權的 SmsCodeAuthenticationToken */ public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { super (authorities); this .principal = principal; // must use super, as we override 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(); } } |
2、SmsAuthenticationFilter
然后編寫 SmsAuthenticationFilter,參考 UsernamePasswordAuthenticationFilter 的源碼,直接粘過來,改一改。
說明
原本的靜態字段有 username 和 password,都干掉,換成我們的手機號字段。
SmsCodeAuthenticationFilter() 中指定了這個 filter 的攔截 Url,我指定為 post 方式的 /sms/login
。
剩下來的方法把無效的刪刪改改就好了。
代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { /** * form表單中手機號碼的字段name */ public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile" ; private String mobileParameter = "mobile" ; /** * 是否僅 POST 方式 */ private boolean postOnly = true ; public SmsCodeAuthenticationFilter() { //短信驗證碼的地址為/sms/login 請求也是post super ( new AntPathRequestMatcher( "/sms/login" , "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); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this .getAuthenticationManager().authenticate(authRequest); } protected String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter); } protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } public String getMobileParameter() { return mobileParameter; } 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; } } |
3、SmsAuthenticationProvider
這個方法比較重要,這個方法首先能夠在使用短信驗證碼登陸時候被 AuthenticationManager 挑中,其次要在這個類中處理驗證邏輯。
說明
實現 AuthenticationProvider 接口,實現 authenticate() 和 supports() 方法。
代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; /** * 處理session工具類 */ private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_SMS" ; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; String mobile = (String) authenticationToken.getPrincipal(); checkSmsCode(mobile); UserDetails userDetails = userDetailsService.loadUserByUsername(mobile); // 此時鑒權成功后,應當重新 new 一個擁有鑒權的 authenticationResult 返回 SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } private void checkSmsCode(String mobile) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); // 從session中獲取圖片驗證碼 SmsCode smsCodeInSession = (SmsCode) sessionStrategy.getAttribute( new ServletWebRequest(request), SESSION_KEY_PREFIX); String inputCode = request.getParameter( "smsCode" ); if (smsCodeInSession == null ) { throw new BadCredentialsException( "未檢測到申請驗證碼" ); } String mobileSsion = smsCodeInSession.getMobile(); if (!Objects.equals(mobile,mobileSsion)) { throw new BadCredentialsException( "手機號碼不正確" ); } String codeSsion = smsCodeInSession.getCode(); if (!Objects.equals(codeSsion,inputCode)) { throw new BadCredentialsException( "驗證碼錯誤" ); } } @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; } } |
4、SmsCodeAuthenticationSecurityConfig
既然自定義了攔截器,可以需要在配置里做改動。
代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Component public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private SmsUserService smsUserService; @Autowired private AuthenctiationSuccessHandler authenctiationSuccessHandler; @Autowired private AuthenctiationFailHandler authenctiationFailHandler; @Override public void configure(HttpSecurity http) { SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(); smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager. class )); smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenctiationSuccessHandler); smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenctiationFailHandler); SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(); //需要將通過用戶名查詢用戶信息的接口換成通過手機號碼實現 smsCodeAuthenticationProvider.setUserDetailsService(smsUserService); http.authenticationProvider(smsCodeAuthenticationProvider) .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter. class ); } } |
5、SmsUserService
因為用戶名,密碼登陸最終是通過用戶名查詢用戶信息,而手機驗證碼登陸是通過手機登陸,所以這里需要自己再實現一個SmsUserService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
@Service @Slf4j public class SmsUserService implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private RolesUserMapper rolesUserMapper; @Autowired private RolesMapper rolesMapper; /** * 手機號查詢用戶 */ @Override public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException { log.info( "手機號查詢用戶,手機號碼 = {}" ,mobile); //TODO 這里我沒有寫通過手機號去查用戶信息的sql,因為一開始我建user表的時候,沒有建mobile字段,現在我也不想臨時加上去 //TODO 所以這里暫且寫死用用戶名去查詢用戶信息(理解就好) User user = userMapper.findOneByUsername( "小小" ); if (user == null ) { throw new UsernameNotFoundException( "未查詢到用戶信息" ); } //獲取用戶關聯角色信息 如果為空說明用戶并未關聯角色 List<RolesUser> userList = rolesUserMapper.findAllByUid(user.getId()); if (CollectionUtils.isEmpty(userList)) { return user; } //獲取角色ID集合 List<Integer> ridList = userList.stream().map(RolesUser::getRid).collect(Collectors.toList()); List<Roles> rolesList = rolesMapper.findByIdIn(ridList); //插入用戶角色信息 user.setRoles(rolesList); return user; } } |
6、總結
到這里思路就很清晰了,我這里在總結下。
1、首先從獲取驗證的時候,就已經把當前驗證碼信息存到session,這個信息包含驗證碼和手機號碼。
2、用戶輸入驗證登陸,這里是直接寫在SmsAuthenticationFilter中先校驗驗證碼、手機號是否正確,再去查詢用戶信息。我們也可以拆開成用戶名密碼登陸那樣一個
過濾器專門驗證驗證碼和手機號是否正確,正確在走驗證碼登陸過濾器。3、在SmsAuthenticationFilter流程中也有關鍵的一步,就是用戶名密碼登陸是自定義UserService實現UserDetailsService后,通過用戶名查詢用戶名信息而這里是
通過手機號查詢用戶信息,所以還需要自定義SmsUserService實現UserDetailsService后。
三、測試
1、獲取驗證碼
獲取驗證碼的手機號是 15612345678 。因為這里沒有接第三方的短信SDK,只是在后臺輸出。
向手機號為:15612345678的用戶發送驗證碼:254792
2、登陸
1)驗證碼輸入不正確
發現登陸失敗,同樣如果手機號碼輸入不對也是登陸失敗
2)登陸成功
當手機號碼 和 短信驗證碼都正確的情況下 ,登陸就成功了。
參考
1、Spring Security技術棧開發企業級認證與授權(JoJo)
2、Spring Security實現短信驗證碼功能的示例代碼
到此這篇關于Spring Security實現短信驗證碼登陸的文章就介紹到這了,更多相關Spring Security短信驗證碼登陸內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://www.cnblogs.com/qdhxhz/p/12977015.html