概要
前面一節,通過簡單配置即可實現SpringSecurity表單認證功能,而今天這一節將通過閱讀原始碼的形式來學習SpringSecurity是如何實現這些功能, 前方高能預警,本篇分析原始碼篇幅較長
。
過濾器鏈
前面我說過SpringSecurity是基於過濾器鏈的形式,那麼我解析將會介紹一下具體有哪些過濾器。
Filter Class | 介紹 |
---|---|
SecurityContextPersistenceFilter | 判斷當前使用者是否登入 |
CrsfFilter | 用於防止csrf攻擊 |
LogoutFilter | 處理登出請求 |
UsernamePasswordAuthenticationFilter | 處理表單登入的請求(也是我們今天的主角) |
BasicAuthenticationFilter | 處理http basic認證的請求 |
由於過濾器鏈中的過濾器實在太多,我沒有一一列舉,調了幾個比較重要的介紹一下。
通過上面我們知道SpringSecurity對於表單登入的認證請求是交給了UsernamePasswordAuthenticationFilter處理的,那麼具體的認證流程如下:
從上圖可知,UsernamePasswordAuthenticationFilter
繼承於抽象類AbstractAuthenticationProcessingFilter
。
具體認證是:
- 進入doFilter方法,判斷是否要認證,如果需要認證則進入attemptAuthentication方法,如果不需要直接結束
- attemptAuthentication方法中根據username跟password構造一個UsernamePasswordAuthenticationToken物件(此時的token是未認證的),並且將它交給ProviderManger來完成認證。
- ProviderManger中維護這一個AuthenticationProvider物件列表,通過遍歷判斷並且最後選擇DaoAuthenticationProvider物件來完成最後的認證。
- DaoAuthenticationProvider根據ProviderManger傳來的token取出username,並且呼叫我們寫的UserDetailsService的loadUserByUsername方法從資料庫中讀取使用者資訊,然後對比使用者密碼,如果認證通過,則返回使用者資訊也是就是UserDetails物件,在重新構造UsernamePasswordAuthenticationToken(此時的token是 已經認證通過了的)。
接下來我們將通過原始碼來分析具體的整個認證流程。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter 是一個抽象類。所有的認證認證請求的過濾器都會繼承於它,它主要將一些公共的功能實現,而具體的驗證邏輯交給子類實現,有點類似於父類設定好認證流程,子類負責具體的認證邏輯,這樣跟設計模式的模板方法模式有點相似。
現在我們分析一下 它裡面比較重要的方法
1、doFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// 省略不相干程式碼。。。
// 1、判斷當前請求是否要認證
if (!requiresAuthentication(request, response)) {
// 不需要直接走下一個過濾器
chain.doFilter(request, response);
return;
}
try {
// 2、開始請求認證,attemptAuthentication具體實現給子類,如果認證成功返回一個認證通過的Authenticaion物件
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
// 3、登入成功 將認證成功的使用者資訊放入session SessionAuthenticationStrategy介面,用於擴充套件
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
//2.1、發生異常,登入失敗,進入登入失敗handler回撥
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
//2.1、發生異常,登入失敗,進入登入失敗處理器
unsuccessfulAuthentication(request, response, failed);
return;
}
// 3.1、登入成功,進入登入成功處理器。
successfulAuthentication(request, response, chain, authResult);
}
複製程式碼
2、successfulAuthentication
登入成功處理器
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
//1、登入成功 將認證成功的Authentication物件存入SecurityContextHolder中
// SecurityContextHolder本質是一個ThreadLocal
SecurityContextHolder.getContext().setAuthentication(authResult);
//2、如果開啟了記住我功能,將呼叫rememberMeServices的loginSuccess 將生成一個token
// 將token放入cookie中這樣 下次就不用登入就可以認證。具體關於記住我rememberMeServices的相關分析我 們下面幾篇文章會深入分析的。
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
//3、釋出一個登入事件。
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//4、呼叫我們自己定義的登入成功處理器,這樣也是我們擴充套件得知登入成功的一個擴充套件點。
successHandler.onAuthenticationSuccess(request, response, authResult);
}
複製程式碼
3、unsuccessfulAuthentication
登入失敗處理器
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
//1、登入失敗,將SecurityContextHolder中的資訊清空
SecurityContextHolder.clearContext();
//2、關於記住我功能的登入失敗處理
rememberMeServices.loginFail(request, response);
//3、呼叫我們自己定義的登入失敗處理器,這裡可以擴充套件記錄登入失敗的日誌。
failureHandler.onAuthenticationFailure(request, response, failed);
}
複製程式碼
關於AbstractAuthenticationProcessingFilter主要分析就到這。我們可以從原始碼中知道,當請求進入該過濾器中具體的流程是
- 判斷該請求是否要被認證
- 呼叫
attemptAuthentication
方法開始認證,由於是抽象方法具體認證邏輯給子類 - 如果登入成功,則將認證結果
Authentication
物件根據session策略寫入session中,將認證結果寫入到SecurityContextHolder
,如果開啟了記住我功能,則根據記住我功能,生成token並且寫入cookie中,最後呼叫一個successHandler
物件的方法,這個物件可以是我們配置注入的,用於處理我們的自定義登入成功的一些邏輯(比如記錄登入成功日誌等等)。 - 如果登入失敗,則清空
SecurityContextHolder
中的資訊,並且呼叫我們自己注入的failureHandler
物件,處理我們自己的登入失敗邏輯。
UsernamePasswordAuthenticationFilter
從上面分析我們可以知道,UsernamePasswordAuthenticationFilter
是繼承於AbstractAuthenticationProcessingFilter
,並且實現它的attemptAuthentication
方法,來實現認證具體的邏輯實現。接下來,我們通過閱讀UsernamePasswordAuthenticationFilter
的原始碼來解讀,它是如何完成認證的。 由於這裡會涉及UsernamePasswordAuthenticationToken
物件構造,所以我們先看看UsernamePasswordAuthenticationToken
的原始碼
1、UsernamePasswordAuthenticationToken
// 繼承至AbstractAuthenticationToken
// AbstractAuthenticationToken主要定義一下在SpringSecurity中toke需要存在一些必須資訊
// 例如許可權集合 Collection<GrantedAuthority> authorities; 是否認證通過boolean authenticated = false;認證通過的使用者資訊Object details;
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
// 未登入情況下 存的是使用者名稱 登入成功情況下存的是UserDetails物件
private final Object principal;
// 密碼
private Object credentials;
/**
* 建構函式,使用者沒有登入的情況下,此時的authenticated是false,代表尚未認證
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
/**
* 建構函式,使用者登入成功的情況下,多了一個引數 是使用者的許可權集合,此時的authenticated是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
}
}
複製程式碼
接下來我們就可以分析attemptAuthentication方法了。
2、attemptAuthentication
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 1、判斷是不是post請求,如果不是則丟擲AuthenticationServiceException異常,注意這裡丟擲的異常都在AbstractAuthenticationProcessingFilter#doFilter方法中捕獲,捕獲之後會進入登入失敗的邏輯。
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 2、從request中拿使用者名稱跟密碼
String username = obtainUsername(request);
String password = obtainPassword(request);
// 3、非空處理,防止NPE異常
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
// 4、除去空格
username = username.trim();
// 5、根據username跟password構造出一個UsernamePasswordAuthenticationToken物件 從上文分析可知道,此時的token是未認證的。
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// 6、配置一下其他資訊 ip 等等
setDetails(request, authRequest);
// 7、呼叫ProviderManger的authenticate的方法進行具體認證邏輯
return this.getAuthenticationManager().authenticate(authRequest);
}
複製程式碼
ProviderManager
維護一個AuthenticationProvider列表,進行認證邏輯驗證
1、authenticate
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// 1、拿到token的型別。
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
// 2、遍歷AuthenticationProvider列表
for (AuthenticationProvider provider : getProviders()) {
// 3、AuthenticationProvider不支援當前token型別,則直接跳過
if (!provider.supports(toTest)) {
continue;
}
try {
// 4、如果Provider支援當前token,則交給Provider完成認證。
result = provider.authenticate(authentication);
}
catch (AccountStatusException e) {
throw e;
}
catch (InternalAuthenticationServiceException e) {
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
// 5、登入成功 返回登入成功的token
if (result != null) {
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
}
複製程式碼
AbstractUserDetailsAuthenticationProvider
1、authenticate
AbstractUserDetailsAuthenticationProvider
實現了AuthenticationProvider
介面,並且實現了部分方法,DaoAuthenticationProvider
繼承於AbstractUserDetailsAuthenticationProvider
類,所以我們先來看看AbstractUserDetailsAuthenticationProvider
的實現。
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
// 國際化處理
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
/**
* 對token一些檢查,具體檢查邏輯交給子類實現,抽象方法
*/
protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
/**
* 認證邏輯的實現,呼叫抽象方法retrieveUser根據username獲取UserDetails物件
*/
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// 1、獲取usernmae
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
// 2、嘗試去快取中獲取UserDetails物件
UserDetails user = this.userCache.getUserFromCache(username);
// 3、如果為空,則代表當前物件沒有快取。
if (user == null) {
cacheWasUsed = false;
try {
//4、呼叫retrieveUser去獲取UserDetail物件,為什麼這個方法是抽象方法大家很容易知道,如果UserDetail資訊存在關聯式資料庫 則可以重寫該方法並且去關聯式資料庫獲取使用者資訊,如果UserDetail資訊存在其他地方,可以重寫該方法用其他的方法去獲取使用者資訊,這樣絲毫不影響整個認證流程,方便擴充套件。
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
// 捕獲異常 日誌處理 並且往上丟擲,登入失敗。
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
}
try {
// 5、前置檢查 判斷當前使用者是否鎖定,禁用等等
preAuthenticationChecks.check(user);
// 6、其他的檢查,在DaoAuthenticationProvider是檢查密碼是否一致
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
}
// 7、後置檢查,判斷密碼是否過期
postAuthenticationChecks.check(user);
// 8、登入成功通過UserDetail物件重新構造一個認證通過的Token物件
return createSuccessAuthentication(principalToReturn, authentication, user);
}
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// 呼叫第二個構造方法,構造一個認證通過的Token物件
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
}
複製程式碼
接下來我們具體看看retrieveUser
的實現,沒看原始碼大家應該也可以知道,retrieveUser
方法應該是呼叫UserDetailsService
去資料庫查詢是否有該使用者,以及使用者的密碼是否一致。
DaoAuthenticationProvider
DaoAuthenticationProvider 主要是通過UserDetailService來獲取UserDetail物件。
1、retrieveUser
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
try {
// 1、呼叫UserDetailsService介面的loadUserByUsername方法獲取UserDeail物件
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
// 2、如果loadedUser為null 代表當前使用者不存在,丟擲異常 登入失敗。
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
// 3、返回查詢的結果
return loadedUser;
}
}
複製程式碼
2、additionalAuthenticationChecks
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
// 1、如果密碼為空,則丟擲異常、
if (authentication.getCredentials() == null) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
// 2、獲取使用者輸入的密碼
String presentedPassword = authentication.getCredentials().toString();
// 3、呼叫passwordEncoder的matche方法 判斷密碼是否一致
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
// 4、如果不一致 則丟擲異常。
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
複製程式碼
總結
至此,整認證流程已經分析完畢,大家如果有什麼不懂可以關注我的公眾號一起討論。
學習是一個漫長的過程,學習原始碼可能會很困難但是隻要努力一定就會有獲取,大家一致共勉。