SpringSceurity實(shí)現(xiàn)短信驗(yàn)證碼登陸
一、短信登錄驗(yàn)證機(jī)制原理分析
了解短信驗(yàn)證碼的登陸機(jī)制之前,我們首先是要了解用戶賬號(hào)密碼登陸的機(jī)制是如何的,我們來簡(jiǎn)要分析一下Spring Security是如何驗(yàn)證基于用戶名和密碼登錄方式的,
分析完畢之后,再一起思考如何將短信登錄驗(yàn)證方式集成到Spring Security中。
1、賬號(hào)密碼登陸的流程
一般賬號(hào)密碼登陸都有附帶 圖形驗(yàn)證碼 和 記住我功能 ,那么它的大致流程是這樣的。
1、 用戶在輸入用戶名,賬號(hào)、圖片驗(yàn)證碼后點(diǎn)擊登陸。那么對(duì)于springSceurity首先會(huì)進(jìn)入短信驗(yàn)證碼Filter,因?yàn)樵谂渲玫臅r(shí)候會(huì)把它配置在
UsernamePasswordAuthenticationFilter之前,把當(dāng)前的驗(yàn)證碼的信息跟存在session的圖片驗(yàn)證碼的驗(yàn)證碼進(jìn)行校驗(yàn)。2、短信驗(yàn)證碼通過后,進(jìn)入 UsernamePasswordAuthenticationFilter 中,根據(jù)輸入的用戶名和密碼信息,構(gòu)造出一個(gè)暫時(shí)沒有鑒權(quán)的
UsernamePasswordAuthenticationToken,并將 UsernamePasswordAuthenticationToken 交給 AuthenticationManager 處理。3、AuthenticationManager 本身并不做驗(yàn)證處理,他通過 for-each 遍歷找到符合當(dāng)前登錄方式的一個(gè) AuthenticationProvider,并交給它進(jìn)行驗(yàn)證處理
,對(duì)于用戶名密碼登錄方式,這個(gè) Provider 就是 DaoAuthenticationProvider。4、在這個(gè) Provider 中進(jìn)行一系列的驗(yàn)證處理,如果驗(yàn)證通過,就會(huì)重新構(gòu)造一個(gè)添加了鑒權(quán)的 UsernamePasswordAuthenticationToken,并將這個(gè)
token 傳回到 UsernamePasswordAuthenticationFilter 中。5、在該 Filter 的父類 AbstractAuthenticationProcessingFilter 中,會(huì)根據(jù)上一步驗(yàn)證的結(jié)果,跳轉(zhuǎn)到 successHandler 或者是 failureHandler。
流程圖
2、短信驗(yàn)證碼登陸流程
因?yàn)槎绦诺卿浀姆绞讲]有集成到Spring Security中,所以往往還需要我們自己開發(fā)短信登錄邏輯,將其集成到Spring Security中,那么這里我們就模仿賬號(hào)
密碼登陸來實(shí)現(xiàn)短信驗(yàn)證碼登陸。
1、用戶名密碼登錄有個(gè) UsernamePasswordAuthenticationFilter,我們搞一個(gè)SmsAuthenticationFilter,代碼粘過來改一改。
2、用戶名密碼登錄需要UsernamePasswordAuthenticationToken,我們搞一個(gè)SmsAuthenticationToken,代碼粘過來改一改。
3、用戶名密碼登錄需要DaoAuthenticationProvider,我們模仿它也 implenments AuthenticationProvider,叫做 SmsAuthenticationProvider。
這個(gè)圖是網(wǎng)上找到,自己不想畫了
我們自己搞了上面三個(gè)類以后,想要實(shí)現(xiàn)的效果如上圖所示。當(dāng)我們使用短信驗(yàn)證碼登錄的時(shí)候:
1、先經(jīng)過 SmsAuthenticationFilter,構(gòu)造一個(gè)沒有鑒權(quán)的 SmsAuthenticationToken,然后交給 AuthenticationManager處理。
2、AuthenticationManager 通過 for-each 挑選出一個(gè)合適的 provider 進(jìn)行處理,當(dāng)然我們希望這個(gè) provider 要是 SmsAuthenticationProvider。
3、驗(yàn)證通過后,重新構(gòu)造一個(gè)有鑒權(quán)的SmsAuthenticationToken,并返回給SmsAuthenticationFilter。
filter 根據(jù)上一步的驗(yàn)證結(jié)果,跳轉(zhuǎn)到成功或者失敗的處理邏輯。
二、代碼實(shí)現(xiàn)
1、SmsAuthenticationToken
首先我們編寫 SmsAuthenticationToken,這里直接參考 UsernamePasswordAuthenticationToken 源碼,直接粘過來,改一改。
說明
principal 原本代表用戶名,這里保留,只是代表了手機(jī)號(hào)碼。
credentials 原本代碼密碼,短信登錄用不到,直接刪掉。
SmsCodeAuthenticationToken() 兩個(gè)構(gòu)造方法一個(gè)是構(gòu)造沒有鑒權(quán)的,一個(gè)是構(gòu)造有鑒權(quán)的。
剩下的幾個(gè)方法去除無用屬性即可。
代碼
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; /** * 在 UsernamePasswordAuthenticationToken 中該字段代表登錄的用戶名, * 在這里就代表登錄的手機(jī)號(hào)碼 */ private final Object principal; /** * 構(gòu)建一個(gè)沒有鑒權(quán)的 SmsCodeAuthenticationToken */ public SmsCodeAuthenticationToken(Object principal) { super(null); this.principal = principal; setAuthenticated(false); } /** * 構(gòu)建擁有鑒權(quán)的 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 的源碼,直接粘過來,改一改。
說明
原本的靜態(tài)字段有 username 和 password,都干掉,換成我們的手機(jī)號(hào)字段。
SmsCodeAuthenticationFilter() 中指定了這個(gè) filter 的攔截 Url,我指定為 post 方式的 /sms/login
。
剩下來的方法把無效的刪刪改改就好了。
代碼
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { /** * form表單中手機(jī)號(hào)碼的字段name */ public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile"; private String mobileParameter = "mobile"; /** * 是否僅 POST 方式 */ private boolean postOnly = true; public SmsCodeAuthenticationFilter() { //短信驗(yàn)證碼的地址為/sms/login 請(qǐng)求也是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
這個(gè)方法比較重要,這個(gè)方法首先能夠在使用短信驗(yàn)證碼登陸時(shí)候被 AuthenticationManager 挑中,其次要在這個(gè)類中處理驗(yàn)證邏輯。
說明
實(shí)現(xiàn) AuthenticationProvider 接口,實(shí)現(xiàn) authenticate() 和 supports() 方法。
代碼
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); // 此時(shí)鑒權(quán)成功后,應(yīng)當(dāng)重新 new 一個(gè)擁有鑒權(quán)的 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中獲取圖片驗(yàn)證碼 SmsCode smsCodeInSession = (SmsCode) sessionStrategy.getAttribute(new ServletWebRequest(request), SESSION_KEY_PREFIX); String inputCode = request.getParameter("smsCode"); if(smsCodeInSession == null) { throw new BadCredentialsException("未檢測(cè)到申請(qǐng)驗(yàn)證碼"); } String mobileSsion = smsCodeInSession.getMobile(); if(!Objects.equals(mobile,mobileSsion)) { throw new BadCredentialsException("手機(jī)號(hào)碼不正確"); } String codeSsion = smsCodeInSession.getCode(); if(!Objects.equals(codeSsion,inputCode)) { throw new BadCredentialsException("驗(yàn)證碼錯(cuò)誤"); } } @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
既然自定義了攔截器,可以需要在配置里做改動(dòng)。
代碼
@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(); //需要將通過用戶名查詢用戶信息的接口換成通過手機(jī)號(hào)碼實(shí)現(xiàn) smsCodeAuthenticationProvider.setUserDetailsService(smsUserService); http.authenticationProvider(smsCodeAuthenticationProvider) .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
5、SmsUserService
因?yàn)橛脩裘?密碼登陸最終是通過用戶名查詢用戶信息,而手機(jī)驗(yàn)證碼登陸是通過手機(jī)登陸,所以這里需要自己再實(shí)現(xiàn)一個(gè)SmsUserService
@Service @Slf4j public class SmsUserService implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private RolesUserMapper rolesUserMapper; @Autowired private RolesMapper rolesMapper; /** * 手機(jī)號(hào)查詢用戶 */ @Override public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException { log.info("手機(jī)號(hào)查詢用戶,手機(jī)號(hào)碼 = {}",mobile); //TODO 這里我沒有寫通過手機(jī)號(hào)去查用戶信息的sql,因?yàn)橐婚_始我建user表的時(shí)候,沒有建mobile字段,現(xiàn)在我也不想臨時(shí)加上去 //TODO 所以這里暫且寫死用用戶名去查詢用戶信息(理解就好) User user = userMapper.findOneByUsername("小小"); if (user == null) { throw new UsernameNotFoundException("未查詢到用戶信息"); } //獲取用戶關(guān)聯(lián)角色信息 如果為空說明用戶并未關(guān)聯(lián)角色 List<RolesUser> userList = rolesUserMapper.findAllByUid(user.getId()); if (CollectionUtils.isEmpty(userList)) { return user; } //獲取角色I(xiàn)D集合 List<Integer> ridList = userList.stream().map(RolesUser::getRid).collect(Collectors.toList()); List<Roles> rolesList = rolesMapper.findByIdIn(ridList); //插入用戶角色信息 user.setRoles(rolesList); return user; } }
6、總結(jié)
到這里思路就很清晰了,我這里在總結(jié)下。
1、首先從獲取驗(yàn)證的時(shí)候,就已經(jīng)把當(dāng)前驗(yàn)證碼信息存到session,這個(gè)信息包含驗(yàn)證碼和手機(jī)號(hào)碼。
2、用戶輸入驗(yàn)證登陸,這里是直接寫在SmsAuthenticationFilter中先校驗(yàn)驗(yàn)證碼、手機(jī)號(hào)是否正確,再去查詢用戶信息。我們也可以拆開成用戶名密碼登陸那樣一個(gè)
過濾器專門驗(yàn)證驗(yàn)證碼和手機(jī)號(hào)是否正確,正確在走驗(yàn)證碼登陸過濾器。3、在SmsAuthenticationFilter流程中也有關(guān)鍵的一步,就是用戶名密碼登陸是自定義UserService實(shí)現(xiàn)UserDetailsService后,通過用戶名查詢用戶名信息而這里是
通過手機(jī)號(hào)查詢用戶信息,所以還需要自定義SmsUserService實(shí)現(xiàn)UserDetailsService后。
三、測(cè)試
1、獲取驗(yàn)證碼
獲取驗(yàn)證碼的手機(jī)號(hào)是 15612345678 。因?yàn)檫@里沒有接第三方的短信SDK,只是在后臺(tái)輸出。
向手機(jī)號(hào)為:15612345678的用戶發(fā)送驗(yàn)證碼:254792
2、登陸
1)驗(yàn)證碼輸入不正確
發(fā)現(xiàn)登陸失敗,同樣如果手機(jī)號(hào)碼輸入不對(duì)也是登陸失敗
2)登陸成功
當(dāng)手機(jī)號(hào)碼 和 短信驗(yàn)證碼都正確的情況下 ,登陸就成功了。
參考
1、Spring Security技術(shù)棧開發(fā)企業(yè)級(jí)認(rèn)證與授權(quán)(JoJo)
2、SpringSceurity實(shí)現(xiàn)短信驗(yàn)證碼功能的示例代碼
到此這篇關(guān)于SpringSceurity實(shí)現(xiàn)短信驗(yàn)證碼登陸的文章就介紹到這了,更多相關(guān)SpringSceurity短信驗(yàn)證碼登陸內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot實(shí)現(xiàn)短信驗(yàn)證碼校驗(yàn)方法思路詳解
- Spring Security Oauth2.0 實(shí)現(xiàn)短信驗(yàn)證碼登錄示例
- SpringBoot + SpringSecurity 短信驗(yàn)證碼登錄功能實(shí)現(xiàn)
- SpringBoot+Security 發(fā)送短信驗(yàn)證碼的實(shí)現(xiàn)
- Spring Security 實(shí)現(xiàn)短信驗(yàn)證碼登錄功能
- springboot短信驗(yàn)證碼登錄功能的實(shí)現(xiàn)
- Spring?Security短信驗(yàn)證碼實(shí)現(xiàn)詳解
- SpringBoot發(fā)送短信驗(yàn)證碼的實(shí)例
- Spring中使用騰訊云發(fā)送短信驗(yàn)證碼的實(shí)現(xiàn)示例
相關(guān)文章
idea中創(chuàng)建maven的Javaweb工程并進(jìn)行配置(圖文教程)
這篇文章主要介紹了idea中創(chuàng)建maven的Javaweb工程并進(jìn)行配置,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),文中給大家提到了tomcat的運(yùn)行方法,具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-02-02Java實(shí)現(xiàn)從jar包中讀取指定文件的方法
這篇文章主要介紹了Java實(shí)現(xiàn)從jar包中讀取指定文件的方法,涉及java針對(duì)jar文件的讀取及查找相關(guān)操作技巧,需要的朋友可以參考下2017-08-08Java利用EasyExcel實(shí)現(xiàn)導(dǎo)出導(dǎo)入功能的示例代碼
EasyExcel是一個(gè)基于Java的、快速、簡(jiǎn)潔、解決大文件內(nèi)存溢出的Excel處理工具。本文廢話不多說,直接上手試試,用代碼試試EasyExcel是否真的那么好用2022-11-11關(guān)于Jackson的JSON工具類封裝 JsonUtils用法
這篇文章主要介紹了關(guān)于Jackson的JSON工具類封裝 JsonUtils用法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09基于Java實(shí)現(xiàn)遍歷文件目錄并去除中文文件名
這篇文章主要為大家詳細(xì)介紹了如何使用Java實(shí)現(xiàn)遍歷文件目錄并去除中文文件名,文中的示例代碼講解詳細(xì),有需要的小伙伴可以參考一下2024-03-03log4j2采用AsyncLogger出現(xiàn)的錯(cuò)誤及解決方案
這篇文章主要介紹了log4j2采用AsyncLogger出現(xiàn)的錯(cuò)誤及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12