用戶認證流程
UsernamePasswordAuthenticationFilter
我們直接來看UsernamePasswordAuthenticationFilter
類,
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 判斷是否是 POST 請求
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 獲取請求中的用戶,密碼。
// 就是最簡單的:request.getParameter(xxx)
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
// 生成 authRequest,本質就是個 usernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// 把 request 請求也一同塞進 token 里
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 將 authRequest 塞進 AuthenticationManager並返回
return this.getAuthenticationManager().authenticate(authRequest);
}
}
在attemptAuthentication()
方法中:主要是先進行請求判斷並獲取username
和password
的值,然后再生成一個UsernamePasswordAuthenticationToken
對象,將這個對象塞進AuthenticationManager
對象並返回,注意:此時的authRequest
的權限是沒有任何值的。
UsernamePasswordAuthenticationToken
不過我們可以先看看UsernamePasswordAuthenticationToken
的構造方法:
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
其實UsernamePasswordAuthenticationToken
是繼承於Authentication
,該對象在學習筆記一中的中"自定義處理登錄成功/失敗"章節里的自定義登錄成功里有提到過,它是處理登錄成功回調方法中的一個參數,里面包含了用戶信息、請求信息等參數。
來一張繼承關系圖,對其有個大概的認識,注意到Authentication
繼承了Principal
。
AuthenticationManager
AuthenticationManager
是一個接口,它的所有實現類如圖:
其中一個十分核心的類就是:ProviderManager
,在attemptAuthentication()
方法最后返回的就是這個類
this.getAuthenticationManager().authenticate(authRequest);
進入authenticate()
方法查看具體做了什么:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
// 1.判斷是否有provider支持該Authentication
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
// 2. 真正的邏輯判斷
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
……
}
}
……
}
這里首先通過 provider 判斷是否支持當前傳入進來的Authentication
,目前我們使用的是UsernamePasswordAuthenticationToken
,因為除了帳號密碼登錄的方式,還會有其他的方式,比如JwtAuthenticationToken
。
從整體來看Authentication
的實現類如圖:
官方 API 文檔列出了所有的子類
從整體來看AuthenticationProvider
的實現類如圖:
官方 API 文檔列出了所有的子類
根據我們目前所使用的UsernamePasswordAuthenticationToken
,provider 對應的是AbstractUserDetailsAuthenticationProvider
抽象類的子類DaoAuthenticationProvider
,其authenticate()
屬於抽象類本身的方法。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 1.從緩存中獲取 UserDetails
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 2.緩存獲取不到,就去接口實現類中獲取
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
……
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// 3.用戶信息預檢查(用戶是否密碼過期,用戶信息被刪除等)
preAuthenticationChecks.check(user);
// 4.附加的檢查(密碼檢查:匹配用戶的密碼和服務器中的用戶密碼是否一致)
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
// 5.最后的檢查
postAuthenticationChecks.check(user);
……
// 6.返回真正的經過認證的Authentication
return createSuccessAuthentication(principalToReturn, authentication, user);
}
注意:retrieveUser()
的具體方法實現是由DaoAuthenticationProvider
類完成的:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 獲取用戶信息
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) {
……
}
}
}
同時createSuccessAuthentication()
的方法也是由DaoAuthenticationProvider
類來完成的:
// 子類拿 user 對象
@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);
}
// 調用父類的方法完成 Authentication 的創建
return super.createSuccessAuthentication(principal, authentication, user);
}
// 創建已認證的 Authentication
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
小結:authenticate()
的認證邏輯
- 去調用自己實現的
UserDetailsService
,返回UserDetails
- 對 UserDetails 的信息進行校驗,主要是帳號是否被凍結,是否過期等
- 對密碼進行檢查,這里調用了
PasswordEncoder
,檢查 UserDetails 是否可用。 - 返回經過認證的
Authentication
編碼技巧提示:這里在認證之前使用了
Assert.isInstanceOf()
進行斷言校驗,方法內部也不斷用了Assert.notNull()
,這種編碼非常的靈巧,省去了后續的類型判斷。
這里的兩次對UserDetails
的檢查,主要就是通過它的四個返回 boolean 類型的方法(isAccountNonExpired()
,isAccountNonLocked()
,isCredentialsNonExpired()
,isEnabled()
)。
經過信息的校驗之后,通過UsernamePasswordAuthenticationToken
的全參構造方法,返回了一個已經過認證的Authentication
。
拿到經過認證的Authentication
之后,至此UsernamePasswordAuthenticationFilter
的過濾步驟就完全結束了,之后就會進入BasicAuthenticationFilter
,具體來說就是去調用successHandler
。或者未通過認證,去調用failureHandler
。
已認證數據共享
完成了用戶認證處理流程之后,我們思考一下是如何在多個請求之間共享這個認證結果的呢?因為沒有做關於這方面的配置,所以可以聯想到默認的方式應該是在session中存入了認證結果。思考:那么是什么時候存放入session中的呢?
認證流程完畢之后,再看是誰調用的它,發現是AbstractAuthenticationProcessingFilter
的doFilter()
進行調用的,這是AbstractAuthenticationProcessingFilter
繼承關系結構圖:
當認證成功之后會調用successfulAuthentication(request, response, chain, authResult)
,該方法中,不僅調用了successHandler
,還有一行比較重要的代碼:
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// 調用了 UsernamePasswordAuthenticationFilter
authResult = attemptAuthentication(request, response);
……
// 調用方法,目的是保存到session
successfulAuthentication(request, response, chain, authResult);
}
// 將成功認證的用戶信息保存到session
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
// 保存到 SecurityContextHolder 的靜態屬性 SecurityContextHolderStrategy 里, 非常重要的代碼
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
}
// SecurityContextHolder類中存着 靜態屬性:SecurityContextHolderStrategy
public class SecurityContextHolder {
……
private static SecurityContextHolderStrategy strategy;
……
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
}
SecurityContextHolderStrategy
接口的所有實現類:
非常顯眼的看出:ThreadLocalSecurityContextHolderStrategy
類:
final class ThreadLocalSecurityContextHolderStrategy implements
SecurityContextHolderStrategy {
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
……
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
// 將已認證的用戶對象保存到 ThreadLocal<SecurityContext> 中
contextHolder.set(context);
}
……
}
注意:
SecurityContext
類的equals()
和hashCode()
方法已經重寫了,用來保證了authentication的唯一性。
身份認證成功后,最后在UsernamePasswordAuthenticationFilter
返回后會進入一個AbstractAuthenticationProcessingFilter
類中調用successfulAuthentication()
方法,這個方法最后會返回我們自己定義的登錄成功處理器handler
。
在返回之前,它會調用SecurityContext
,最后將認證的結果放入SecurityContextHolder
中,SecurityContext 類很簡單,重寫了equals()
方法和hashCode()
方法,保證了authentication的唯一性。
從代碼可以看出:SecurityContextHolder
類實際上是對ThreadLocal
的一個封裝,可以在不同方法之間進行通信,可以簡單理解為線程級別的一個全局變量。
因此,可以在同一個線程中的不同方法中獲取到認證信息。最后會被SecurityContextPersistenceFilter
過濾器使用,這個過濾器的作用是:
當一個請求來的時候,它會將 session 中的值傳入到該線程中,當請求返回的時候,它會判斷該請求線程是否有 SecurityContext
,如果有它會將其放入到 session 中,因此保證了請求結果可以在不同的請求之間共享。
用戶認證流程總結
引用徐靖峰在個人博客Spring Security(一)--Architecture Overview中的概括性總結,非常的到位:
- 用戶名和密碼被過濾器獲取到,封裝成
Authentication
,通常情況下是UsernamePasswordAuthenticationToken
這個實現類。 AuthenticationManager
身份管理器負責驗證這個Authentication
。- 認證成功后,
AuthenticationManager
身份管理器返回一個被填充滿了信息的(包括上面提到的權限信息,身份信息,細節信息,但密碼通常會被移除)Authentication
實例。 SecurityContextHolder
安全上下文容器將第3步填充了信息的Authentication
,通過SecurityContextHolder.getContext().setAuthentication(…)
方法,設置到其中。
高度概括起來本章節所有用的核心認證相關接口:SecurityContextHolder
是
身份信息的存放容器,Authentication
是身份信息的抽象,AuthenticationManager
是身份認證器,一般常用的是用戶名+密碼的身份認證器,還有其它認證器,如郵箱+密碼、手機號碼+密碼等。
再引用一張十分流行的流程圖來表示用戶的認證過程:
架構概覽圖
為了更加形象的理解,在徐靖峰大佬的經典架構圖之上,根據自己的理解,做了更多的細化和調整:
獲取認證用戶信息
如果我們需要獲取用的校驗過的所有信息,該如何獲取呢?上面我們知道了會將校驗結果放入 session 中,因此,我們可以通過 session 獲取:
@GetMapping("/me1")
@ResponseBody
public Object getMeDetail() {
return SecurityContextHolder.getContext().getAuthentication();
}
@GetMapping("/me2")
@ResponseBody
public Object getMeDetail(Authentication authentication){
return authentication;
}
在登錄成功之后,上面有兩種方式來獲取,訪問上面的請求,就會獲取用戶全部的校驗信息,包括ip地址等信息。
如果我們只想獲取用戶名和密碼以及它的權限,不需要ip地址等太多的信息可以使用下面的方式來獲取信息:
@GetMapping("/me3")
@ResponseBody
public Object getMeDetail(@AuthenticationPrincipal UserDetails userDetails){
return userDetails;
}
參考資料:
https://www.cnkirito.moe/spring-security-1/