不知道疗涉, 你在用Spring Security的時候拿霉,有沒有想過,用它實現(xiàn)多種登錄方式勒咱扣,這次我的小伙伴就給我提了一些登錄方面的需求绽淘,需要在原有賬號密碼登錄的基礎上,另外實現(xiàn)電話驗證碼以及郵件驗證碼登錄闹伪,以及在實現(xiàn)之后沪铭,讓我能夠做到實現(xiàn)第三方登錄,如gitee偏瓤、github等杀怠。
本文主要是講解Security在實現(xiàn)賬號密碼的基礎上,并且不改變原有業(yè)務情況下厅克,實現(xiàn)郵件赔退、電話驗證碼登錄。
前言:
這對于已經學過一段時間证舟,并且對Security已經有了解的小伙伴來說硕旗,還是比較合適的,但是對于我以及其他一些急于解決當下問題的小白女责,并不是那么友善漆枚。
一、理論知識
我們先思考一下這個流程大致是如何的抵知?
填寫郵件號碼墙基,獲取驗證碼
輸入獲取到的驗證碼進行登錄(登錄的接口:/email/login,這里不能使用默認的/login刷喜,因為我們是擴展)
在自定義的過濾器EmailCodeAuthenticationFilter中獲取發(fā)送過來的郵件號碼及驗證碼碘橘,判斷驗證碼是否正確,郵件賬號是否為空等
封裝成一個需要認證的Authentication吱肌,此處我們自定義實現(xiàn)為EmailCodeAuthenticationToken。
將 Authentiction 傳給 AuthenticationManager接口中authenticate 方法進行認證處理
AuthenticationManager 默認是實現(xiàn)類為 ProviderManager 仰禽,ProviderManager 又委托給 AuthenticationProvider 進行處理
我們自定義一個EmailCodeAuthenticationProvider實現(xiàn)AuthenticationProvider,實現(xiàn)身份驗證氮墨。
自定義的EmailCodeAuthenticationFilter繼承了 AbstractAuthenticationProcessingFilter 抽象類,AbstractAuthenticationProcessingFilter 在 successfulAuthentication 方法中對登錄成功進行了處理吐葵,通過 SecurityContextHolder.getContext().setAuthentication() 方法將 Authentication 認證信息對象綁定到 SecurityContext即安全上下文中规揪。
其實對于身份驗證通過后的處理,有兩種方案温峭,一種是直接在過濾器重寫successfulAuthentication猛铅,另外一種就是實現(xiàn)AuthenticationSuccessHandler來處理身份驗證通過。
身份驗證失敗也是一樣凤藏,可重寫unsuccessfulAuthentication方法奸忽,也可以實現(xiàn) AuthenticationFailureHandler來對身份驗證失敗進行處理堕伪。
大致流程就是如此。從這個流程中我們可以知道栗菜,需要重寫的組件有以下幾個:
EmailCodeAuthenticationFilter:郵件驗證登錄過濾器
EmailCodeAuthenticationToken:身份驗證令牌
EmailCodeAuthenticationProvider:郵件身份認證處理
AuthenticationSuccessHandler:處理登錄成功操作
AuthenticationFailureHandler:處理登錄失敗操作
接下來欠雌,我是模仿著源碼寫出我的代碼,建議大家可以在使用的時候疙筹,多去看看富俄,我這里去除了一些不是和這個相關的代碼。
來吧6亍霍比!
二、EmailCodeAuthenticationFilter
我們需要重寫的
EmailCodeAuthenticationFilter暴备,實際繼承了AbstractAuthenticationProcessingFilter抽象類悠瞬,我們不會寫,可以先看看它的默認實現(xiàn)UsernamePasswordAuthenticationFilter是怎么樣的嗎馍驯,抄作業(yè)這是大家的強項的哈阁危。
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");
//從前臺傳過來的參數(shù)
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
// 初始化一個用戶密碼 認證過濾器 默認的登錄uri 是 /login 請求方式是POST
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
/**
執(zhí)行實際身份驗證。實現(xiàn)應執(zhí)行以下操作之一:
1汰瘫、為經過身份驗證的用戶返回填充的身份驗證令牌狂打,表示身份驗證成功
2、返回null混弥,表示認證過程還在進行中趴乡。 在返回之前,實現(xiàn)應該執(zhí)行完成流程所需的任何額外工作蝗拿。
3晾捏、如果身份驗證過程失敗,則拋出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進行認證
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// 可以放一些其他信息進去
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方法
}
接下來我們就抄個作業(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 {
/*- 前端傳來的 參數(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;
/**
- 通過 傳入的 參數(shù) 創(chuàng)建 匹配器
- 即 Filter過濾的url
*/
public EmailCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/email/login","POST"));
}
- 前端傳來的 參數(shù)名 - 用于request.getParameter 獲取
/**
* filter 獲得 用戶名(郵箱) 和 密碼(驗證碼) 裝配到 token 上 惦辛,
* 然后把token 交給 provider 進行授權
*/
@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();
//如果 驗證碼不相等 故意讓token出錯 然后走springsecurity 錯誤的流程
boolean flag = checkCode(request);
//封裝 token
EmailCodeAuthenticationToken token = new EmailCodeAuthenticationToken(email,new ArrayList<>());
this.setDetails(request,token);
//交給 manager 發(fā)證
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的默認實現(xiàn)是什么樣的就行了呀伙。
/**
*/
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
}
@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 地址(未認證的時候)
*/
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);
}
}
}
這個很簡單的哈。
四雨女、EmailCodeAuthenticationProvider
自定義的
EmailCodeAuthenticationProvider是實現(xiàn)了AuthenticationProvider接口谚攒,抄作業(yè)就得學會看看源碼。我們接著來戚篙。
4.1五鲫、先看看AbstractUserDetailsAuthenticationProvider,我們再來模仿
AuthenticationProvider接口有很多實現(xiàn)類岔擂,不一一說明了位喂,直接看我們需要看的
AbstractUserDetailsAuthenticationProvider, 該類旨在響應UsernamePasswordAuthenticationToken身份驗證請求。但是它是一個抽象類乱灵,但其實就一個步驟在它的實現(xiàn)類中實現(xiàn)的塑崖,很簡單,稍后會講到痛倚。
在這個源碼中我把和檢查相關的一些操作都給刪除规婆,只留下幾個重點,我們一起來看一看哈蝉稳。
//該類旨在響應UsernamePasswordAuthenticationToken身份驗證請求抒蚜。
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 {
// 緩存中沒有 通過字類實現(xiàn)的retrieveUser 從數(shù)據(jù)庫進行檢索,返回一個 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;
}
// 沒有通過檢查耘戚, 重新檢索最新的數(shù)據(jù)
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();
}
//創(chuàng)建一個可信的身份令牌返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
private String determineUsername(Authentication authentication) {
return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
}
/**
簡而言之就是創(chuàng)建了一個通過身份驗證的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;
}
/**
允許子類從特定于實現(xiàn)的位置實際檢索UserDetails 嗡髓,如果提供的憑據(jù)不正確,則可以選擇立即拋出AuthenticationException (如果需要以用戶身份綁定到資源以獲得或生成一個UserDetails )
*/
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
//...
//簡而言之:當然有時候我們有多個不同的 `AuthenticationProvider`收津,它們分別支持不同的 `Authentication`對象饿这,那么當一個具體的 `AuthenticationProvier`傳進入 `ProviderManager`的內部時,就會在 `AuthenticationProvider`列表中挑選其對應支持的provider對相應的 Authentication對象進行驗證
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}
關于protected abstract UserDetails retrieveUser的實現(xiàn)撞秋,
AbstractUserDetailsAuthenticationProvider實現(xiàn)是DaoAuthenticationProvider.
DaoAuthenticationProvider主要操作是兩個长捧,第一個是從數(shù)據(jù)庫中檢索出相關信息,第二個是給檢索出的用戶信息進行密碼的加密操作吻贿。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 檢索用戶串结,一般我們都會實現(xiàn) UserDetailsService接口,改為從數(shù)據(jù)庫中檢索用戶信息 返回安全核心類 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) {
// 判斷是否用了密碼加密 針對這個點 沒有深入 大家好奇可以去查一查這個知識點
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è)啦
看完源碼奉芦,其實我們如果要重寫的話,主要要做到以下幾個事情:
重寫public boolean supports(Class<?> authentication)方法剧蹂。 有時候我們有多個不同的 AuthenticationProvider,它們分別支持不同的 Authentication對象烦却,那么當一個具體的 AuthenticationProvier傳進入 ProviderManager的內部時宠叼,就會在 AuthenticationProvider列表中挑選其對應支持的provider對相應的 Authentication對象進行驗證 簡單說就是指定AuthenticationProvider驗證哪個Authentication對象。如指定DaoAuthenticationProvider認證UsernamePasswordAuthenticationToken, 所以我們指定EmailCodeAuthenticationProvider認證EmailCodeAuthenticationToken冒冬。
檢索數(shù)據(jù)庫伸蚯,返回一個安全核心類UserDetail。
創(chuàng)建一個經過身份驗證的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;
}
/**
* 認證
*/
@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("無法獲取用戶信息");
}
System.out.println(user.getAuthorities());
EmailCodeAuthenticationToken result =
new EmailCodeAuthenticationToken(user, user.getAuthorities());
/*
Details 中包含了 ip地址、 sessionId 等等屬性 也可以存儲一些自己想要放進去的內容
*/
result.setDetails(token.getDetails());
return result;
}
@Override
public boolean supports(Class<?> aClass) {
return EmailCodeAuthenticationToken.class.isAssignableFrom(aClass);
}
}
五横侦、在配置類中進行配置
主要就是做下面幾件事:
將過濾器挥萌、認證器注入到spring中
將登錄成功處理、登錄失敗處理器注入到Spring中报辱,或者在自定義過濾器中對登錄成功和失敗進行處理畸颅。
添加到過濾鏈中
@Bean
public EmailCodeAuthenticationFilter emailCodeAuthenticationFilter() {
EmailCodeAuthenticationFilter emailCodeAuthenticationFilter = new EmailCodeAuthenticationFilter();
emailCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
emailCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
return emailCodeAuthenticationFilter;
}
@Bean
public EmailCodeAuthenticationProvider emailCodeAuthenticationProvider() {
return new EmailCodeAuthenticationProvider(userService);
}
/**
* 因為使用了BCryptPasswordEncoder來進行密碼的加密舰罚,所以身份驗證的時候也的用他來判斷哈、憨栽,
*
* @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)
六屑柔、測試及源代碼
項目具體的配置、啟動方式珍剑、環(huán)境等掸宛、都在github及gitee的文檔上有詳細說明。
源代碼中包含sql文件次慢、配置文件以及相關博客鏈接旁涤,源代碼中也加了很多注釋,盡最大程度讓大家能夠看明白迫像。
在最大程度上保證大家都能正確的運行及測試劈愚。