javascript
你有没有遇到要实现多种登录方式的场景丫 一起来看看咯 Spring Security 实现多种登录方式,如常规方式外的邮件、手机验证码登录
你好丫,我是博主寧在春,一起加油吧!!!
不知道, 你在用Spring Security的時候,有沒有想過,用它實現多種登錄方式勒,這次我的小伙伴就給我提了一些登錄方面的需求,需要在原有賬號密碼登錄的基礎上,另外實現電話驗證碼以及郵件驗證碼登錄,以及在實現之后,讓我能夠做到實現第三方登錄,如gitee、github等。
本文主要是講解Security在實現賬號密碼的基礎上,并且不改變原有業務情況下,實現郵件、電話驗證碼登錄。
大家好,我是博主寧在春。有沒有小伙伴玩掘金的丫。有的話,可以來投投票啦
投票鏈接
至于為啥不發CSDN的勒,搞不贏 (捂臉)
前言:
上一篇文章我寫了 Security登錄詳細流程詳解有源碼有分析。掌握這個登錄流程,我們才能更好的做Security的定制操作。
我在寫這篇文章之前,也看過很多博主的文章,寫的非常好,有對源碼方面的解析,也有對一些相關設計理念的理解的文章。
這對于已經學過一段時間,并且對Security已經有了解的小伙伴來說,還是比較合適的,但是對于我以及其他一些急于解決當下問題的小白,并不是那么友善。😂
一、🤸?♂?理論知識
我們先思考一下這個流程大致是如何的?
大致流程就是如此。從這個流程中我們可以知道,需要重寫的組件有以下幾個:
接下來,我是模仿著源碼寫出我的代碼,建議大家可以在使用的時候,多去看看,我這里去除了一些不是和這個相關的代碼。
來吧!!
二、EmailCodeAuthenticationFilter
我們需要重寫的 EmailCodeAuthenticationFilter,實際繼承了AbstractAuthenticationProcessingFilter抽象類,我們不會寫,可以先看看它的默認實現UsernamePasswordAuthenticationFilter是怎么樣的嗎,抄作業這是大家的強項的哈。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login","POST");//從前臺傳過來的參數private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;private boolean postOnly = true;// 初始化一個用戶密碼 認證過濾器 默認的登錄uri 是 /login 請求方式是POSTpublic UsernamePasswordAuthenticationFilter() {super(DEFAULT_ANT_PATH_REQUEST_MATCHER);}public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);}/**執行實際身份驗證。實現應執行以下操作之一:1、為經過身份驗證的用戶返回填充的身份驗證令牌,表示身份驗證成功2、返回null,表示認證過程還在進行中。 在返回之前,實現應該執行完成流程所需的任何額外工作。3、如果身份驗證過程失敗,則拋出AuthenticationException*/@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throws AuthenticationException {if (this.postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}String username = obtainUsername(request);username = (username != null) ? username : "";username = username.trim();String password = obtainPassword(request);password = (password != null) ? password : "";//生成 UsernamePasswordAuthenticationToken 稍后交由AuthenticationManager中的authenticate進行認證UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);// 可以放一些其他信息進去setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}@Nullableprotected String obtainPassword(HttpServletRequest request) {return request.getParameter(this.passwordParameter);}@Nullableprotected String obtainUsername(HttpServletRequest request) {return request.getParameter(this.usernameParameter);}protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));}//set、get方法 }接下來我們就抄個作業哈:
package com.crush.security.auth.email_code;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher;import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.ArrayList;/*** @Author: crush* @Date: 2021-09-08 21:13* version 1.0*/ public class EmailCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {/*** 前端傳來的 參數名 - 用于request.getParameter 獲取*/private final String DEFAULT_EMAIL_NAME="email";private final String DEFAULT_EMAIL_CODE="e_code";@Autowired@Overridepublic void setAuthenticationManager(AuthenticationManager authenticationManager) {super.setAuthenticationManager(authenticationManager);}/*** 是否 僅僅post方式*/private boolean postOnly = true;/*** 通過 傳入的 參數 創建 匹配器* 即 Filter過濾的url*/public EmailCodeAuthenticationFilter() {super(new AntPathRequestMatcher("/email/login","POST"));}/*** filter 獲得 用戶名(郵箱) 和 密碼(驗證碼) 裝配到 token 上 ,* 然后把token 交給 provider 進行授權*/@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {if(postOnly && !request.getMethod().equals("POST") ){throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}else{String email = getEmail(request);if(email == null){email = "";}email = email.trim();//如果 驗證碼不相等 故意讓token出錯 然后走springsecurity 錯誤的流程boolean flag = checkCode(request);//封裝 tokenEmailCodeAuthenticationToken token = new EmailCodeAuthenticationToken(email,new ArrayList<>());this.setDetails(request,token);//交給 manager 發證return this.getAuthenticationManager().authenticate(token);}}/*** 獲取 頭部信息 讓合適的provider 來驗證他*/public void setDetails(HttpServletRequest request , EmailCodeAuthenticationToken token ){token.setDetails(this.authenticationDetailsSource.buildDetails(request));}/*** 獲取 傳來 的Email信息*/public String getEmail(HttpServletRequest request ){String result= request.getParameter(DEFAULT_EMAIL_NAME);return result;}/*** 判斷 傳來的 驗證碼信息 以及 session 中的驗證碼信息*/public boolean checkCode(HttpServletRequest request ){String code1 = request.getParameter(DEFAULT_EMAIL_CODE);System.out.println("code1**********"+code1);// TODO 另外再寫一個鏈接 生成 驗證碼 那個驗證碼 在生成的時候 存進redis 中去//TODO 這里的驗證碼 寫在Redis中, 到時候取出來判斷即可 驗證之后 刪除驗證碼if(code1.equals("123456")){return true;}return false;}// set、get方法... }三、🤖EmailCodeAuthenticationToken
我們EmailCodeAuthenticationToken是繼承AbstractAuthenticationToken的,按照同樣的方式,我們接著去看看AbstractAuthenticationToken的默認實現是什么樣的就行了。
/***/ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;// 這里指的賬號密碼哈private final Object principal;private Object credentials;/**沒經過身份驗證時,初始化權限為空,setAuthenticated(false)設置為不可信令牌*/public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {super(null);this.principal = principal;this.credentials = credentials;setAuthenticated(false);}/**經過身份驗證后,將權限放進去,setAuthenticated(true)設置為可信令牌*/public UsernamePasswordAuthenticationToken(Object principal, Object credentials,Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;this.credentials = credentials;super.setAuthenticated(true); // must use super, as we override}@Overridepublic Object getCredentials() {return this.credentials;}@Overridepublic Object getPrincipal() {return this.principal;}@Overridepublic void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {Assert.isTrue(!isAuthenticated,"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");super.setAuthenticated(false);}@Overridepublic void eraseCredentials() {super.eraseCredentials();this.credentials = null;}}日常抄作業哈:
/*** @Author: crush* @Date: 2021-09-08 21:13* version 1.0*/ public class EmailCodeAuthenticationToken extends AbstractAuthenticationToken {/*** 這里的 principal 指的是 email 地址(未認證的時候)*/private final Object principal;public EmailCodeAuthenticationToken(Object principal) {super((Collection) null);this.principal = principal;setAuthenticated(false);}public EmailCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;super.setAuthenticated(true);}@Overridepublic Object getCredentials() {return null;}@Overridepublic Object getPrincipal() {return this.principal;}@Overridepublic 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");} else {super.setAuthenticated(false);}}}這個很簡單的哈。👨?💻
四、EmailCodeAuthenticationProvider
自定義的EmailCodeAuthenticationProvider是實現了AuthenticationProvider接口,抄作業就得學會看看源碼。我們接著來。
4.1、先看看AbstractUserDetailsAuthenticationProvider,我們再來模仿
AuthenticationProvider 接口有很多實現類,不一一說明了,直接看我們需要看的AbstractUserDetailsAuthenticationProvider, 該類旨在響應 UsernamePasswordAuthenticationToken 身份驗證請求。但是它是一個抽象類,但其實就一個步驟在它的實現類中實現的,很簡單,稍后會講到。
在這個源碼中我把和檢查相關的一些操作都給刪除,只留下幾個重點,我們一起來看一看哈。
//該類旨在響應UsernamePasswordAuthenticationToken身份驗證請求。 public abstract class AbstractUserDetailsAuthenticationProviderimplements AuthenticationProvider, InitializingBean, MessageSourceAware {protected final Log logger = LogFactory.getLog(getClass());private UserCache userCache = new NullUserCache();@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports","Only UsernamePasswordAuthenticationToken is supported"));//獲取用戶名String username = determineUsername(authentication);//判斷緩存中是否存在boolean cacheWasUsed = true;UserDetails user = this.userCache.getUserFromCache(username);if (user == null) {cacheWasUsed = false;try {// 緩存中沒有 通過字類實現的retrieveUser 從數據庫進行檢索,返回一個 UserDetails 對象user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);}catch (UsernameNotFoundException ex) {this.logger.debug("Failed to find user '" + username + "'");if (!this.hideUserNotFoundExceptions) {throw ex;}throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));}Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");}try {//進行相關檢查 因為可能是從緩存中取出來的 并非是最新的this.preAuthenticationChecks.check(user);additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);}catch (AuthenticationException ex) {if (!cacheWasUsed) {throw ex;}// 沒有通過檢查, 重新檢索最新的數據cacheWasUsed = false;user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);this.preAuthenticationChecks.check(user);additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);}// 再次進行檢查this.postAuthenticationChecks.check(user);// 存進緩存中去if (!cacheWasUsed) {this.userCache.putUserInCache(user);}Object principalToReturn = user;if (this.forcePrincipalAsString) {principalToReturn = user.getUsername();}//創建一個可信的身份令牌返回return createSuccessAuthentication(principalToReturn, authentication, user);}private String determineUsername(Authentication authentication) {return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();}/** 簡而言之就是創建了一個通過身份驗證的UsernamePasswordAuthenticationToken*/protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,UserDetails user) {UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));result.setDetails(authentication.getDetails());this.logger.debug("Authenticated user");return result;}/** 允許子類從特定于實現的位置實際檢索UserDetails ,如果提供的憑據不正確,則可以選擇立即拋出AuthenticationException (如果需要以用戶身份綁定到資源以獲得或生成一個UserDetails )*/protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)throws AuthenticationException;//...//簡而言之:當然有時候我們有多個不同的 `AuthenticationProvider`,它們分別支持不同的 `Authentication`對象,那么當一個具體的 `AuthenticationProvier`傳進入 `ProviderManager`的內部時,就會在 `AuthenticationProvider`列表中挑選其對應支持的provider對相應的 Authentication對象進行驗證@Overridepublic boolean supports(Class<?> authentication) {return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));}}關于 protected abstract UserDetails retrieveUser 的實現,AbstractUserDetailsAuthenticationProvider實現是DaoAuthenticationProvider.
DaoAuthenticationProvider主要操作是兩個,第一個是從數據庫中檢索出相關信息,第二個是給檢索出的用戶信息進行密碼的加密操作。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {private UserDetailsService userDetailsService;@Overrideprotected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {prepareTimingAttackProtection();try {// 檢索用戶,一般我們都會實現 UserDetailsService接口,改為從數據庫中檢索用戶信息 返回安全核心類 UserDetailsUserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);if (loadedUser == null) {throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");}return loadedUser;}catch (UsernameNotFoundException ex) {mitigateAgainstTimingAttack(authentication);throw ex;}catch (InternalAuthenticationServiceException ex) {throw ex;}catch (Exception ex) {throw new InternalAuthenticationServiceException(ex.getMessage(), ex);}}@Overrideprotected Authentication createSuccessAuthentication(Object principal, Authentication authentication,UserDetails user) {// 判斷是否用了密碼加密 針對這個點 沒有深入 大家好奇可以去查一查這個知識點boolean upgradeEncoding = this.userDetailsPasswordService != null&& this.passwordEncoder.upgradeEncoding(user.getPassword());if (upgradeEncoding) {String presentedPassword = authentication.getCredentials().toString();String newPassword = this.passwordEncoder.encode(presentedPassword);user = this.userDetailsPasswordService.updatePassword(user, newPassword);}return super.createSuccessAuthentication(principal, authentication, user);}}4.2、抄作業啦
看完源碼,其實我們如果要重寫的話,主要要做到以下幾個事情:
重寫public boolean supports(Class<?> authentication)方法。
有時候我們有多個不同的 AuthenticationProvider,它們分別支持不同的 Authentication對象,那么當一個具體的 AuthenticationProvier 傳進入 ProviderManager的內部時,就會在 AuthenticationProvider列表中挑選其對應支持的 provider 對相應的 Authentication對象進行驗證
簡單說就是指定AuthenticationProvider驗證哪個 Authentication 對象。如指定DaoAuthenticationProvider認證UsernamePasswordAuthenticationToken,
所以我們指定EmailCodeAuthenticationProvider認證EmailCodeAuthenticationToken。
檢索數據庫,返回一個安全核心類UserDetail。
創建一個經過身份驗證的Authentication對象
了解要做什么事情了,我們就可以動手看看代碼啦。
/*** @Author: crush* @Date: 2021-09-08 21:14* version 1.0*/ @Slf4j public class EmailCodeAuthenticationProvider implements AuthenticationProvider {ITbUserService userService;public EmailCodeAuthenticationProvider(ITbUserService userService) {this.userService = userService;}/*** 認證*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {if (!supports(authentication.getClass())) {return null;}log.info("EmailCodeAuthentication authentication request: %s", authentication);EmailCodeAuthenticationToken token = (EmailCodeAuthenticationToken) authentication;UserDetails user = userService.getByEmail((String) token.getPrincipal());System.out.println(token.getPrincipal());if (user == null) {throw new InternalAuthenticationServiceException("無法獲取用戶信息");}System.out.println(user.getAuthorities());EmailCodeAuthenticationToken result =new EmailCodeAuthenticationToken(user, user.getAuthorities());/*Details 中包含了 ip地址、 sessionId 等等屬性 也可以存儲一些自己想要放進去的內容*/result.setDetails(token.getDetails());return result;}@Overridepublic boolean supports(Class<?> aClass) {return EmailCodeAuthenticationToken.class.isAssignableFrom(aClass);} }五、在配置類中進行配置
主要就是做下面幾件事:
六、測試及源代碼
項目具體的配置、啟動方式、環境等、都在github及gitee的文檔上有詳細說明。
源代碼中包含sql文件、配置文件以及相關博客鏈接,源代碼中也加了很多注釋,盡最大程度讓大家能夠看明白。
在最大程度上保證大家都能正確的運行及測試。
源碼:gitee-Security
七、自言自語
如果這篇存在不太懂的內容,可以先看我的另一篇文章:
SpringBoot集成Security實現安全控制,使用Jwt制作Token令牌。
之后再回過頭來看這一篇文章,應該會更加容易理解。
我是真的想要教會大家,并非是胡亂寫寫,主要是想收到來自大家成功后的那種開心,會讓人心情愉悅。
今天的文章就到這里了。
你好,我是博主寧在春:CSDN主頁
如若在文章中遇到疑惑,請留言或私信,或者加主頁聯系方式,都會盡快回復。
如若發現文章中存在問題,望你能夠指正,不勝感謝。
如果覺得對你有所幫助的話,請點個贊再走吧!
總結
以上是生活随笔為你收集整理的你有没有遇到要实现多种登录方式的场景丫 一起来看看咯 Spring Security 实现多种登录方式,如常规方式外的邮件、手机验证码登录的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何使用Docker安装Mycat中间件
- 下一篇: SpringSecurity鉴权流程分析