系列導航
SpringSecurity系列
- SpringSecurity系列學習(一):初識SpringSecurity
- SpringSecurity系列學習(二):密碼驗證
- SpringSecurity系列學習(三):認證流程和源碼解析
- SpringSecurity系列學習(四):基於JWT的認證
- SpringSecurity系列學習(四-番外):多因子驗證和TOTP
- SpringSecurity系列學習(五):授權流程和源碼分析
- SpringSecurity系列學習(六):基於RBAC的授權
SpringSecurityOauth2系列
- SpringSecurityOauth2系列學習(一):初認Oauth2
- SpringSecurityOauth2系列學習(二):授權服務
- SpringSecurityOauth2系列學習(三):資源服務
- SpringSecurityOauth2系列學習(四):自定義登陸登出接口
- SpringSecurityOauth2系列學習(五):授權服務自定義異常處理
認證流程和源碼解析
有小伙伴看到這里就會說了:說好的編碼呢?說好的編碼呢?為什么還要看原理啊?憋嗦話,上號!
現在開始編碼,你必不能啟動項目!
咳咳
先打基礎打基礎,我們學習SpringSecurity不是為了寫一個Hello world
就闊以了,最后是為了把SpringSecurity用在項目中,現在理解的原理越多,后面出現bug才好定位修復!
SpringSecurity核心組件
在分析認證流程之前,我們先來看一下一些SpringSecurity的一些概念
SecurityContextHolder
:是一個工具類,它提供了對安全上下文的訪問。默認情況下,它使用一個ThreadLocal
對象來存儲安全上下文,這意味着它是線程安全的。
SecurityContext
:是用來存儲當前認證的用戶的詳細信息。
Authentication
:
- 存儲了當前用戶(與應用程序交互的主體)的詳細信息
Principal
可以理解為用戶的信息(比較簡單的情況下,有可能是用戶名)Credentials
可以理解為密碼Authorities
可以理解為權限
Authentication
是Spring認證體系的核心元素,Spring Security內建了很多具體的派生類,比如最常見的用於用戶名/密碼登錄場景的UsernamePasswordAuthenticationToken
我們在第一章最后自定義過濾器的時候,返回了一個Authentication
的實現類,那個實現類就是UsernamePasswordAuthenticationToken
{
"authenticated": true,
"authorities": [
{
"authority": "ROLE_ADMIN"
},
{
"authority": "ROLE_USER"
}
],
"details": {
"remoteAddress": "127.0.0.1"
},
"name": "user",
"principal": {
"accountNonExpired": true,
"accountNonLocked": true,
"authorities": [
{
"$ref": "$.authorities[0]"
},
{
"$ref": "$.authorities[1]"
}
],
"credentialsNonExpired": true,
"enabled": true,
"username": "user"
}
}
認證流程分析
認證流程
認證流程如圖所示
- 請求進入認證過濾器(
AuthenticationFilter
),獲取用戶名和密碼,構建成UserNamepasswordAuthenticationToken
,但是這個對象沒有被完全的初始化,因為這個時候你只加入了用戶名與密碼,一般這個角色里面還有角色列表和是否認證等信息。 - 認證過濾器(
AuthenticationFilter
)將部分初始化的UserNamepasswordAuthenticationToken
傳遞給AuthenticationManager
,其也是一個接口類,用戶真正執行認證的類,其中有一個AuthenticationProvider
集合。一個AuthenticationProvider
就是一種具體的認證機制,比如有時候我們需要從數據庫中讀取用戶信息進行認證,有時候我們需要進行多因子認證,比如短信,郵箱等。執行認證的時候AuthenticationManager
就去遍歷AuthenticationProvider
集合,判斷是否支持當前認證方式,如果支持,則調用當前AuthenticationProvider
的認證方法進行認證 - 如果是數據庫認證的
AuthenticationProvider
來說,就會去調用UserDetailsService
去獲取用戶信息進行認證。 - 成功了,就進行返回,返回到認證過濾器(
AuthenticationFilter
)的時候,就將Authentication
對象,也就是初始化之后的UserNamepasswordAuthenticationToken
(其實是重新構造了一個,使用了不同的構造函數),放到SecuityContext
當中。
認證源碼解析
UsernamePasswordAuthenticationFilter
先看主要負責認證的過濾器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 String usernameParameter = "username";
private String passwordParameter = "password";
private Boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse
response) throws AuthenticationException {
//必須為POST請求
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " +
request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//將填寫的用戶名和密碼封裝到了UsernamePasswordAuthenticationToken中
UsernamePasswordAuthenticationToken authRequest = new
UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
//調用AuthenticationManager對象實現認證
return this.getAuthenticationManager().authenticate(authRequest);
}
}
}
AuthenticationManager
由上面源碼得知,真正認證操作在AuthenticationManager
里面!
然后看AuthenticationManager
的實現類ProviderManager
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
private static final Log logger = LogFactory.getLog(ProviderManager.class);
private AuthenticationEventPublisher eventPublisher;
private List<AuthenticationProvider> providers;
protected MessageSourceAccessor messages;
private AuthenticationManager parent;
private Boolean eraseCredentialsAfterAuthentication;
//注意AuthenticationProvider這個對象,SpringSecurity針對每一種認證,什么qq登錄啊,
//用戶名密碼登陸啊,微信登錄啊都封裝了一個AuthenticationProvider對象。
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, (AuthenticationManager)null);
}
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();
Iterator var8 = this.getProviders().iterator();
//循環所有AuthenticationProvider,匹配當前認證類型。
while(var8.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var8.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " +
provider.getClass().getName());
}
try {
//找到了對應認證類型就繼續調用AuthenticationProvider對象完成認證業務。
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException var13) {
this.prepareException(var13, authentication);
throw var13;
}
catch (InternalAuthenticationServiceException var14) {
this.prepareException(var14, authentication);
throw var14;
}
catch (AuthenticationException var15) {
lastException = var15;
}
}
}
if (result == null && this.parent != null) {
try {
result = parentResult = this.parent.authenticate(authentication);
}
catch (ProviderNotFoundException var11) {
}
catch (AuthenticationException var12) {
parentException = var12;
lastException = var12;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof
CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} else {
if (lastException == null) {
lastException = new
ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new
Object[]{toTest.getName()
}
, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
this.prepareException((AuthenticationException)lastException, authentication);
}
throw lastException;
}
}
}
AbstractUserDetailsAuthenticationProvider
咱們繼續再找到AuthenticationProvider
的實現類AbstractUserDetailsAuthenticationProvider
:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private PasswordEncoder passwordEncoder;
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
//重點來了!主要就在這里了!
//UserDetails就是SpringSecurity自己的用戶對象。
//this.getUserDetailsService()其實就是得到UserDetailsService的一個實現類
//loadUserByUsername里面就是真正的認證邏輯
//也就是說我們可以直接編寫一個UserDetailsService的實現類,告訴SpringSecurity就可以了!
//loadUserByUsername方法中只需要返回一個UserDetails對象即可
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
//若返回null,就拋出異常,認證失敗。
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned
null, which is an interface contract violation");
} else {
//若有得到了UserDetails對象,返回即可。
return loadedUser;
}
}
catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
}
catch (InternalAuthenticationServiceException var5) {
throw var5;
}
catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
}
authenticate返回值
上面不是說到返回了一個
UserDetails對象對象嗎?跟着它,就又回到了AbstractUserDetailsAuthenticationProvider
對象中authenticate
方法的最后一行了。
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
public Authentication authenticate(Authentication authentication) throws
AuthenticationException {
//最后一行返回值,調用了createSuccessAuthentication方法,此方法就在下面!
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
//咿!?怎么又封裝了一次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());
return result;
}
}
UsernamePasswordAuthenticationToken
來到UsernamePasswordAuthenticationToken
對象發現里面有兩個構造方法
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 510L;
private final Object principal;
private Object credentials;
//認證成功前,調用的是這個帶有兩個參數的。
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
//認證成功后,調用的是這個帶有三個參數的。
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
//看看父類干了什么!
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
}
AbstractAuthenticationToken
再點進去super(authorities)看看:
public abstract class AbstractAuthenticationToken implements Authentication,
CredentialsContainer {
private final Collection<GrantedAuthority> authorities;
private Object details;
private Boolean authenticated = false;
public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
//這是兩個參數那個分支!
if (authorities == null) {
this.authorities = AuthorityUtils.NO_AUTHORITIES;
} else {
//三個參數的,看這里!
Iterator var2 = authorities.iterator();
//原來是多個了添加權限信息的步驟
GrantedAuthority a;
do {
if (!var2.hasNext()) {
ArrayList<GrantedAuthority> temp = new ArrayList(authorities.size());
temp.addAll(authorities);
this.authorities = Collections.unmodifiableList(temp);
return;
}
a = (GrantedAuthority)var2.next();
}
while(a != null);
//若沒有權限信息,是會拋出異常的!
throw new IllegalArgumentException("Authorities collection cannot contain any null
elements");
}
}
}
由此,咱們需要牢記自定義認證業務邏輯返回的UserDetails對象中一定要放置權限信息啊!
現在可以結束源碼分析了吧?先不要着急!
咱們回到最初的地方UsernamePasswordAuthenticationFilter
,你看,這可是個過濾器,咱們分析這么久,都沒提到doFilter方法,你不覺得心里不踏實?
可是這里面也沒有doFilter呀?那就從父類找!
AbstractAuthenticationProcessingFilter
點開AbstractAuthenticationProcessingFilter
,刪掉不必要的代碼!
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
//doFilter在此!
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws
IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the
user.", var8);
this.unsuccessfulAuthentication(request, response, var8);
return;
}
catch (AuthenticationException var9) {
this.unsuccessfulAuthentication(request, response, var9);
return;
}
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
this.successfulAuthentication(request, response, chain, authResult);
}
}
protected Boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse
response) {
return this.requiresAuthenticationRequestMatcher.matches(request);
}
//成功走successfulAuthentication
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse
response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to
contain: " + authResult);
}
//認證成功,將認證信息存儲到SecurityContext中!
SecurityContextHolder.getContext().setAuthentication(authResult);
//登錄成功調用rememberMeServices
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new
InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
//失敗走unsuccessfulAuthentication
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse
response, AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication request failed: " + failed.toString(), failed);
this.logger.debug("Updated SecurityContextHolder to contain null Authentication");
this.logger.debug("Delegating to authentication failure handler " +
this.failureHandler);
}
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
}
可見AbstractAuthenticationProcessingFilter
這個過濾器對於認證成功與否,做了兩個分支,成功執行
successfulAuthentication
,失敗執行unsuccessfulAuthentication
。
在successfulAuthentication
內部,將認證信息存儲到了SecurityContext
中。並調用了loginSuccess
方法,這就是常見的“記住我”功能!