SpringSecurity系列學習(三):認證流程和源碼解析


系列導航

SpringSecurity系列

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"
  }
}

認證流程分析

認證流程

認證流程如圖所示

  1. 請求進入認證過濾器(AuthenticationFilter),獲取用戶名和密碼,構建成UserNamepasswordAuthenticationToken,但是這個對象沒有被完全的初始化,因為這個時候你只加入了用戶名與密碼,一般這個角色里面還有角色列表和是否認證等信息。
  2. 認證過濾器(AuthenticationFilter)將部分初始化的UserNamepasswordAuthenticationToken傳遞給AuthenticationManager,其也是一個接口類,用戶真正執行認證的類,其中有一個AuthenticationProvider集合。一個AuthenticationProvider就是一種具體的認證機制,比如有時候我們需要從數據庫中讀取用戶信息進行認證,有時候我們需要進行多因子認證,比如短信,郵箱等。執行認證的時候AuthenticationManager就去遍歷AuthenticationProvider集合,判斷是否支持當前認證方式,如果支持,則調用當前AuthenticationProvider的認證方法進行認證
  3. 如果是數據庫認證的AuthenticationProvider來說,就會去調用UserDetailsService去獲取用戶信息進行認證。
  4. 成功了,就進行返回,返回到認證過濾器(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方法,這就是常見的“記住我”功能!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM