Spring Security 實(shí)現(xiàn)多種登錄方式(常規(guī)方式外的郵件、手機(jī)驗(yàn)證碼登錄)
不知道, 你在用Spring Security的時(shí)候,有沒(méi)有想過(guò),用它實(shí)現(xiàn)多種登錄方式勒,這次我的小伙伴就給我提了一些登錄方面的需求,需要在原有賬號(hào)密碼登錄的基礎(chǔ)上,另外實(shí)現(xiàn)電話驗(yàn)證碼以及郵件驗(yàn)證碼登錄,以及在實(shí)現(xiàn)之后,讓我能夠做到實(shí)現(xiàn)第三方登錄,如gitee、github等。
本文主要是講解Security在實(shí)現(xiàn)賬號(hào)密碼的基礎(chǔ)上,并且不改變?cè)袠I(yè)務(wù)情況下,實(shí)現(xiàn)郵件、電話驗(yàn)證碼登錄。
前言:
上一篇文章我寫(xiě)了 Security登錄詳細(xì)流程詳解有源碼有分析。掌握這個(gè)登錄流程,我們才能更好的做Security的定制操作。
我在寫(xiě)這篇文章之前,也看過(guò)很多博主的文章,寫(xiě)的非常好,有對(duì)源碼方面的解析,也有對(duì)一些相關(guān)設(shè)計(jì)理念的理解的文章。
這對(duì)于已經(jīng)學(xué)過(guò)一段時(shí)間,并且對(duì)Security已經(jīng)有了解的小伙伴來(lái)說(shuō),還是比較合適的,但是對(duì)于我以及其他一些急于解決當(dāng)下問(wèn)題的小白,并不是那么友善。??
一、??♂?理論知識(shí)
我們先思考一下這個(gè)流程大致是如何的?
- 填寫(xiě)郵件號(hào)碼,獲取驗(yàn)證碼
- 輸入獲取到的驗(yàn)證碼進(jìn)行登錄(登錄的接口:/email/login,這里不能使用默認(rèn)的/login,因?yàn)槲覀兪菙U(kuò)展)
- 在自定義的過(guò)濾器 EmailCodeAuthenticationFilter 中獲取發(fā)送過(guò)來(lái)的郵件號(hào)碼及驗(yàn)證碼,判斷驗(yàn)證碼是否正確,郵件賬號(hào)是否為空等
- 封裝成一個(gè)需要認(rèn)證的 Authentication ,此處我們自定義實(shí)現(xiàn)為 EmailCodeAuthenticationToken。
- 將 Authentiction 傳給 AuthenticationManager 接口中 authenticate 方法進(jìn)行認(rèn)證處理
- AuthenticationManager 默認(rèn)是實(shí)現(xiàn)類為 ProviderManager ,ProviderManager 又委托給 AuthenticationProvider 進(jìn)行處理
- 我們自定義一個(gè) EmailCodeAuthenticationProvider 實(shí)現(xiàn) AuthenticationProvider ,實(shí)現(xiàn)身份驗(yàn)證。
- 自定義的 EmailCodeAuthenticationFilter 繼承了 AbstractAuthenticationProcessingFilter 抽象類, AbstractAuthenticationProcessingFilter 在 successfulAuthentication 方法中對(duì)登錄成功進(jìn)行了處理,通過(guò) SecurityContextHolder.getContext().setAuthentication() 方法將 Authentication 認(rèn)證信息對(duì)象綁定到 SecurityContext即安全上下文中。
- 其實(shí)對(duì)于身份驗(yàn)證通過(guò)后的處理,有兩種方案,一種是直接在過(guò)濾器重寫(xiě)successfulAuthentication,另外一種就是實(shí)現(xiàn)AuthenticationSuccessHandler來(lái)處理身份驗(yàn)證通過(guò)。
- 身份驗(yàn)證失敗也是一樣,可重寫(xiě)unsuccessfulAuthentication方法,也可以實(shí)現(xiàn) AuthenticationFailureHandler來(lái)對(duì)身份驗(yàn)證失敗進(jìn)行處理。
大致流程就是如此。從這個(gè)流程中我們可以知道,需要重寫(xiě)的組件有以下幾個(gè):
- EmailCodeAuthenticationFilter:郵件驗(yàn)證登錄過(guò)濾器
- EmailCodeAuthenticationToken:身份驗(yàn)證令牌
- EmailCodeAuthenticationProvider:郵件身份認(rèn)證處理
- AuthenticationSuccessHandler:處理登錄成功操作
- AuthenticationFailureHandler:處理登錄失敗操作
接下來(lái),我是模仿著源碼寫(xiě)出我的代碼,建議大家可以在使用的時(shí)候,多去看看,我這里去除了一些不是和這個(gè)相關(guān)的代碼。
來(lái)吧!!
二、EmailCodeAuthenticationFilter
我們需要重寫(xiě)的 EmailCodeAuthenticationFilter,實(shí)際繼承了AbstractAuthenticationProcessingFilter抽象類,我們不會(huì)寫(xiě),可以先看看它的默認(rèn)實(shí)現(xiàn)UsernamePasswordAuthenticationFilter是怎么樣的嗎,抄作業(yè)這是大家的強(qiáng)項(xiàng)的哈。
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"); ?? ?//從前臺(tái)傳過(guò)來(lái)的參數(shù) ?? ?private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY; ?? ?private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY; ?? ?private boolean postOnly = true; ?? ? ? ? // ?初始化一個(gè)用戶密碼 認(rèn)證過(guò)濾器 ?默認(rèn)的登錄uri 是 /login 請(qǐng)求方式是POST ?? ?public UsernamePasswordAuthenticationFilter() { ?? ??? ?super(DEFAULT_ANT_PATH_REQUEST_MATCHER); ?? ?} ?? ?public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) { ?? ??? ?super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager); ?? ?} ? ? /** ? ? 執(zhí)行實(shí)際身份驗(yàn)證。實(shí)現(xiàn)應(yīng)執(zhí)行以下操作之一: ?? ?1、為經(jīng)過(guò)身份驗(yàn)證的用戶返回填充的身份驗(yàn)證令牌,表示身份驗(yàn)證成功 ?? ?2、返回null,表示認(rèn)證過(guò)程還在進(jìn)行中。 在返回之前,實(shí)現(xiàn)應(yīng)該執(zhí)行完成流程所需的任何額外工作。 ?? ?3、如果身份驗(yàn)證過(guò)程失敗,則拋出AuthenticationException ? ? */ ?? ?@Override ?? ?public 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進(jìn)行認(rèn)證 ?? ??? ?UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); ?? ??? ?// 可以放一些其他信息進(jìn)去 ?? ??? ?setDetails(request, authRequest); ?? ??? ?return this.getAuthenticationManager().authenticate(authRequest); ?? ?} ?? ?@Nullable ?? ?protected String obtainPassword(HttpServletRequest request) { ?? ??? ?return request.getParameter(this.passwordParameter); ?? ?} ?? ?@Nullable ?? ?protected String obtainUsername(HttpServletRequest request) { ?? ??? ?return request.getParameter(this.usernameParameter); ?? ?} ?? ?protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { ?? ??? ?authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); ?? ?} ?? ?//set、get方法 }
接下來(lái)我們就抄個(gè)作業(yè)哈:
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 { /** * 前端傳來(lái)的 參數(shù)名 - 用于request.getParameter 獲取 */ private final String DEFAULT_EMAIL_NAME="email"; private final String DEFAULT_EMAIL_CODE="e_code"; @Autowired @Override public void setAuthenticationManager(AuthenticationManager authenticationManager) { super.setAuthenticationManager(authenticationManager); } /** * 是否 僅僅post方式 */ private boolean postOnly = true; /** * 通過(guò) 傳入的 參數(shù) 創(chuàng)建 匹配器 * 即 Filter過(guò)濾的url */ public EmailCodeAuthenticationFilter() { super(new AntPathRequestMatcher("/email/login","POST")); } /** * filter 獲得 用戶名(郵箱) 和 密碼(驗(yàn)證碼) 裝配到 token 上 , * 然后把token 交給 provider 進(jìn)行授權(quán) */ @Override public 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(); //如果 驗(yàn)證碼不相等 故意讓token出錯(cuò) 然后走springsecurity 錯(cuò)誤的流程 boolean flag = checkCode(request); //封裝 token EmailCodeAuthenticationToken token = new EmailCodeAuthenticationToken(email,new ArrayList<>()); this.setDetails(request,token); //交給 manager 發(fā)證 return this.getAuthenticationManager().authenticate(token); } } /** * 獲取 頭部信息 讓合適的provider 來(lái)驗(yàn)證他 */ public void setDetails(HttpServletRequest request , EmailCodeAuthenticationToken token ){ token.setDetails(this.authenticationDetailsSource.buildDetails(request)); } /** * 獲取 傳來(lái) 的Email信息 */ public String getEmail(HttpServletRequest request ){ String result= request.getParameter(DEFAULT_EMAIL_NAME); return result; } /** * 判斷 傳來(lái)的 驗(yàn)證碼信息 以及 session 中的驗(yàn)證碼信息 */ public boolean checkCode(HttpServletRequest request ){ String code1 = request.getParameter(DEFAULT_EMAIL_CODE); System.out.println("code1**********"+code1); // TODO 另外再寫(xiě)一個(gè)鏈接 生成 驗(yàn)證碼 那個(gè)驗(yàn)證碼 在生成的時(shí)候 存進(jìn)redis 中去 //TODO 這里的驗(yàn)證碼 寫(xiě)在Redis中, 到時(shí)候取出來(lái)判斷即可 驗(yàn)證之后 刪除驗(yàn)證碼 if(code1.equals("123456")){ return true; } return false; } // set、get方法... }
三、EmailCodeAuthenticationToken
我們EmailCodeAuthenticationToken是繼承AbstractAuthenticationToken的,按照同樣的方式,我們接著去看看AbstractAuthenticationToken的默認(rèn)實(shí)現(xiàn)是什么樣的就行了。
/** ?*/ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { ?? ?private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; ? ? // 這里指的賬號(hào)密碼哈 ?? ?private final Object principal; ?? ?private Object credentials; ?? ?/** ?? ?沒(méi)經(jīng)過(guò)身份驗(yàn)證時(shí),初始化權(quán)限為空,setAuthenticated(false)設(shè)置為不可信令牌 ?? ? */ ?? ?public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { ?? ??? ?super(null); ?? ??? ?this.principal = principal; ?? ??? ?this.credentials = credentials; ?? ??? ?setAuthenticated(false); ?? ?} ?? ?/** ?? ?經(jīng)過(guò)身份驗(yàn)證后,將權(quán)限放進(jìn)去,setAuthenticated(true)設(shè)置為可信令牌 ?? ? */ ?? ?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 ?? ?} ?? ?@Override ?? ?public Object getCredentials() { ?? ??? ?return this.credentials; ?? ?} ?? ?@Override ?? ?public Object getPrincipal() { ?? ??? ?return this.principal; ?? ?} ?? ?@Override ?? ?public 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); ?? ?} ?? ?@Override ?? ?public void eraseCredentials() { ?? ??? ?super.eraseCredentials(); ?? ??? ?this.credentials = null; ?? ?} }
日常抄作業(yè)哈:
/** ?* @Author: crush ?* @Date: 2021-09-08 21:13 ?* version 1.0 ?*/ public class EmailCodeAuthenticationToken extends AbstractAuthenticationToken { ? ? /** ? ? ?* 這里的 principal 指的是 email 地址(未認(rèn)證的時(shí)候) ? ? ?*/ ? ? 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); ? ? } ? ? @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"); ? ? ? ? } else { ? ? ? ? ? ? super.setAuthenticated(false); ? ? ? ? } ? ? } }
這個(gè)很簡(jiǎn)單的哈
四、EmailCodeAuthenticationProvider
自定義的EmailCodeAuthenticationProvider是實(shí)現(xiàn)了AuthenticationProvider接口,抄作業(yè)就得學(xué)會(huì)看看源碼。我們接著來(lái)。
4.1、先看看AbstractUserDetailsAuthenticationProvider,我們?cè)賮?lái)模仿
AuthenticationProvider 接口有很多實(shí)現(xiàn)類,不一一說(shuō)明了,直接看我們需要看的AbstractUserDetailsAuthenticationProvider, 該類旨在響應(yīng) UsernamePasswordAuthenticationToken 身份驗(yàn)證請(qǐng)求。但是它是一個(gè)抽象類,但其實(shí)就一個(gè)步驟在它的實(shí)現(xiàn)類中實(shí)現(xiàn)的,很簡(jiǎn)單,稍后會(huì)講到。
在這個(gè)源碼中我把和檢查相關(guān)的一些操作都給刪除,只留下幾個(gè)重點(diǎn),我們一起來(lái)看一看哈。
//該類旨在響應(yīng)UsernamePasswordAuthenticationToken身份驗(yàn)證請(qǐng)求。 public abstract class AbstractUserDetailsAuthenticationProvider ?? ??? ?implements AuthenticationProvider, InitializingBean, MessageSourceAware { ?? ?protected final Log logger = LogFactory.getLog(getClass()); ?? ?private UserCache userCache = new NullUserCache(); ?? ?@Override ?? ?public 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 { ? ? ? ? ? ? ? ? // 緩存中沒(méi)有 通過(guò)字類實(shí)現(xiàn)的retrieveUser 從數(shù)據(jù)庫(kù)進(jìn)行檢索,返回一個(gè) UserDetails 對(duì)象 ?? ??? ??? ??? ?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 { ? ? ? ? ? ? //進(jìn)行相關(guān)檢查 ?因?yàn)榭赡苁菑木彺嬷腥〕鰜?lái)的 并非是最新的 ?? ??? ??? ?this.preAuthenticationChecks.check(user); ?? ??? ??? ?additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); ?? ??? ?} ?? ??? ?catch (AuthenticationException ex) { ?? ??? ??? ?if (!cacheWasUsed) { ?? ??? ??? ??? ?throw ex; ?? ??? ??? ?} ? ? ? ? ? ? // 沒(méi)有通過(guò)檢查, 重新檢索最新的數(shù)據(jù) ?? ??? ??? ?cacheWasUsed = false; ?? ??? ??? ?user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); ?? ??? ??? ?this.preAuthenticationChecks.check(user); ?? ??? ??? ?additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); ?? ??? ?} ? ? ? ? // 再次進(jìn)行檢查 ?? ??? ?this.postAuthenticationChecks.check(user); ?? ??? ?// 存進(jìn)緩存中去 ? ? ? ? if (!cacheWasUsed) { ?? ??? ??? ?this.userCache.putUserInCache(user); ?? ??? ?} ?? ??? ?Object principalToReturn = user; ?? ??? ?if (this.forcePrincipalAsString) { ?? ??? ??? ?principalToReturn = user.getUsername(); ?? ??? ?} ? ? ? ? //創(chuàng)建一個(gè)可信的身份令牌返回 ?? ??? ?return createSuccessAuthentication(principalToReturn, authentication, user); ?? ?} ?? ?private String determineUsername(Authentication authentication) { ?? ??? ?return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); ?? ?} ?? ?/** 簡(jiǎn)而言之就是創(chuàng)建了一個(gè)通過(guò)身份驗(yàn)證的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; ?? ?} ?? ?/** 允許子類從特定于實(shí)現(xiàn)的位置實(shí)際檢索UserDetails ,如果提供的憑據(jù)不正確,則可以選擇立即拋出AuthenticationException (如果需要以用戶身份綁定到資源以獲得或生成一個(gè)UserDetails ) ?? ? */ ?? ?protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) ?? ??? ??? ?throws AuthenticationException; ?? ?//... ?? ? ? ? //簡(jiǎn)而言之:當(dāng)然有時(shí)候我們有多個(gè)不同的 `AuthenticationProvider`,它們分別支持不同的 `Authentication`對(duì)象,那么當(dāng)一個(gè)具體的 `AuthenticationProvier`傳進(jìn)入 `ProviderManager`的內(nèi)部時(shí),就會(huì)在 `AuthenticationProvider`列表中挑選其對(duì)應(yīng)支持的provider對(duì)相應(yīng)的 Authentication對(duì)象進(jìn)行驗(yàn)證 ?? ?@Override ?? ?public boolean supports(Class<?> authentication) { ?? ??? ?return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); ?? ?} }
關(guān)于 protected abstract UserDetails retrieveUser 的實(shí)現(xiàn),AbstractUserDetailsAuthenticationProvider實(shí)現(xiàn)是DaoAuthenticationProvider.
DaoAuthenticationProvider主要操作是兩個(gè),第一個(gè)是從數(shù)據(jù)庫(kù)中檢索出相關(guān)信息,第二個(gè)是給檢索出的用戶信息進(jìn)行密碼的加密操作。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { ?? ?private UserDetailsService userDetailsService; ? ?? ?? ?@Override ?? ?protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) ?? ??? ??? ?throws AuthenticationException { ?? ??? ?prepareTimingAttackProtection(); ?? ??? ?try { ? ? ? ? ? ? // 檢索用戶,一般我們都會(huì)實(shí)現(xiàn) UserDetailsService接口,改為從數(shù)據(jù)庫(kù)中檢索用戶信息 返回安全核心類 UserDetails ?? ??? ??? ?UserDetails 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); ?? ??? ?} ?? ?} ?? ?@Override ?? ?protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, ?? ??? ??? ?UserDetails user) { ? ? ? ? // 判斷是否用了密碼加密 針對(duì)這個(gè)點(diǎn) 沒(méi)有深入 大家好奇可以去查一查這個(gè)知識(shí)點(diǎn) ?? ??? ?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、抄作業(yè)啦
看完源碼,其實(shí)我們?nèi)绻貙?xiě)的話,主要要做到以下幾個(gè)事情:
重寫(xiě)public boolean supports(Class<?> authentication)方法。
有時(shí)候我們有多個(gè)不同的 AuthenticationProvider,它們分別支持不同的 Authentication對(duì)象,那么當(dāng)一個(gè)具體的 AuthenticationProvier 傳進(jìn)入 ProviderManager的內(nèi)部時(shí),就會(huì)在 AuthenticationProvider列表中挑選其對(duì)應(yīng)支持的 provider 對(duì)相應(yīng)的 Authentication對(duì)象進(jìn)行驗(yàn)證
簡(jiǎn)單說(shuō)就是指定AuthenticationProvider驗(yàn)證哪個(gè) Authentication 對(duì)象。如指定DaoAuthenticationProvider認(rèn)證UsernamePasswordAuthenticationToken,
所以我們指定EmailCodeAuthenticationProvider認(rèn)證EmailCodeAuthenticationToken。
檢索數(shù)據(jù)庫(kù),返回一個(gè)安全核心類UserDetail。
創(chuàng)建一個(gè)經(jīng)過(guò)身份驗(yàn)證的Authentication對(duì)象
了解要做什么事情了,我們就可以動(dòng)手看看代碼啦。
/** ?* @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; ? ? } ? ? /** ? ? ?* 認(rèn)證 ? ? ?*/ ? ? @Override ? ? public 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("無(wú)法獲取用戶信息"); ? ? ? ? } ? ? ? ? System.out.println(user.getAuthorities()); ? ? ? ? EmailCodeAuthenticationToken result = ? ? ? ? ? ? ? ? new EmailCodeAuthenticationToken(user, user.getAuthorities()); ? ? ? ? ? ? ? ? /* ? ? ? ? ? ? ? ? Details 中包含了 ip地址、 sessionId 等等屬性 也可以存儲(chǔ)一些自己想要放進(jìn)去的內(nèi)容 ? ? ? ? ? ? ? ? */ ? ? ? ? result.setDetails(token.getDetails()); ? ? ? ? return result; ? ? } ? ? @Override ? ? public boolean supports(Class<?> aClass) { ? ? ? ? return EmailCodeAuthenticationToken.class.isAssignableFrom(aClass); ? ? } }
五、在配置類中進(jìn)行配置
主要就是做下面幾件事:將過(guò)濾器、認(rèn)證器注入到spring中
將登錄成功處理、登錄失敗處理器注入到Spring中,或者在自定義過(guò)濾器中對(duì)登錄成功和失敗進(jìn)行處理。
添加到過(guò)濾鏈中
? ? @Bean ? ? public EmailCodeAuthenticationFilter emailCodeAuthenticationFilter() { ? ? ? ? EmailCodeAuthenticationFilter emailCodeAuthenticationFilter = new EmailCodeAuthenticationFilter(); ? ? ? ? emailCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler); ? ? ? ? emailCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler); ? ? ? ? return emailCodeAuthenticationFilter; ? ? } ? ? @Bean ? ? public EmailCodeAuthenticationProvider emailCodeAuthenticationProvider() { ? ? ? ? return new EmailCodeAuthenticationProvider(userService); ? ? } ? ? /** ? ? ?* 因?yàn)槭褂昧薆CryptPasswordEncoder來(lái)進(jìn)行密碼的加密,所以身份驗(yàn)證的時(shí)候也的用他來(lái)判斷哈、, ? ? ?* ? ? ?* @param auth ? ? ?* @throws Exception ? ? ?*/ ? ? @Override ? ? protected void configure(AuthenticationManagerBuilder auth) throws Exception { ? ? ? ? auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder()); ? ? ? ? //authenticationProvider 根據(jù)傳入的自定義AuthenticationProvider添加身份AuthenticationProvider 。 ? ? ? ? auth.authenticationProvider(emailCodeAuthenticationProvider()); ? ? }
.and() ? ? .authenticationProvider(emailCodeAuthenticationProvider()) ? ? .addFilterBefore(emailCodeAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class) ? ? .authenticationProvider(mobileCodeAuthenticationProvider()) ? ? .addFilterBefore(mobileCodeAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class)
六、測(cè)試及源代碼
項(xiàng)目具體的配置、啟動(dòng)方式、環(huán)境等、都在github及gitee的文檔上有詳細(xì)說(shuō)明。
源代碼中包含sql文件、配置文件以及相關(guān)博客鏈接,源代碼中也加了很多注釋,盡最大程度讓大家能夠看明白。
在最大程度上保證大家都能正確的運(yùn)行及測(cè)試。
源碼:gitee-Security
七、自言自語(yǔ)
如果這篇存在不太懂的內(nèi)容,可以先看我的另一篇文章:
SpringBoot集成Security實(shí)現(xiàn)安全控制,使用Jwt制作Token令牌。
之后再回過(guò)頭來(lái)看這一篇文章,應(yīng)該會(huì)更加容易理解。
到此這篇關(guān)于Spring Security 實(shí)現(xiàn)多種登錄方式(常規(guī)方式外的郵件、手機(jī)驗(yàn)證碼登錄)的文章就介紹到這了,更多相關(guān)SpringSecurity 登錄 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring 整合mybatis后用不上session緩存的原因分析
因?yàn)橐恢庇胹pring整合了mybatis,所以很少用到mybatis的session緩存。什么原因呢?下面小編給大家介紹spring 整合mybatis后用不上session緩存的原因分析,需要的朋友可以參考下2017-02-02jackson 實(shí)現(xiàn)null轉(zhuǎn)0 以及0轉(zhuǎn)null的示例代碼
這篇文章主要介紹了jackson 實(shí)現(xiàn)null轉(zhuǎn)0 以及0轉(zhuǎn)null的示例代碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09SpringBoot @FixMethodOrder 如何調(diào)整單元測(cè)試順序
這篇文章主要介紹了SpringBoot @FixMethodOrder 調(diào)整單元測(cè)試順序方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09SpringBoot 自定義+動(dòng)態(tài)切換數(shù)據(jù)源教程
這篇文章主要介紹了SpringBoot 自定義+動(dòng)態(tài)切換數(shù)據(jù)源教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12解決spring-boot-maven-plugin報(bào)紅的問(wèn)題
這篇文章主要給大家介紹一下如何解決spring-boot-maven-plugin報(bào)紅的問(wèn)題,文中通過(guò)圖文講解的非常詳細(xì),具有一定的參考價(jià)值,需要的朋友可以參考下2023-08-08