SpringSecurity身份認證原理解析
Spring Security身份認證
- 用戶名和密碼被過濾器獲取到,封裝成 Authentication ,通常情況下是 UsernamePasswordAuthenticationToken 這個實現類。
- AuthenticationManager 身份管理器負責驗證這個 Authentication
- 認證成功后, AuthenticationManager 身份管理器返回一個被填充滿了信息的(包括上面提到的 權限信息,身份信息,細節(jié)信息,但密碼通常會被移除) Authentication 實例。
- SecurityContextHolder 安全上下文容器將第3步填充了信息的 Authentication ,通過 SecurityContextHolder.getContext().setAuthentication(…)方法,設置到其中。
public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.println("please enter your username:");
String name = in.readLine();
System.out.println("please enter your password:");
String password = null;
password = in.readLine();
try {
// 封裝認證信息,未認證通過
UsernamePasswordAuthenticationToken request = new UsernamePasswordAuthenticationToken(name, password);
// 認證邏輯
Authentication result = am.authenticate(request);
//當前線程綁定認證信息
SecurityContextHolder.getContext().setAuthentication(result);
break;
} catch (AuthenticationException e) {
System.out.println("Authentication failed: " + e.getMessage());
}
}
}
static class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 判斷條件,用戶名和密碼是否相同
if (authentication.getName().equals(authentication.getCredentials())){
return new UsernamePasswordAuthenticationToken(authentication.getName(),authentication.getCredentials(),AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
}
}測試:

認證流程

SecurityFilterChain 過濾器鏈
Spring Security采用的是filterChain的設計方式,主要的功能大都由過濾器實現,在啟動項目的時候,可以在日志中看到已有的過濾器,可在類似下面的日志里找到 DefaultSecurityFilterChain ,這里面則是SecurityFilterChain
2021-01-07 11:27:30.410 INFO 13880 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@153cd6bb, org.springframework.security.web.context.SecurityContextPersistenceFilter@71f0b72e, org.springframework.security.web.header.HeaderWriterFilter@aa149ed, org.springframework.security.web.authentication.logout.LogoutFilter@2de50ee4, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@151ef57f, org.springframework.security.web.session.ConcurrentSessionFilter@5c73f672, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@2f508f3c, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5eed2d86, org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter@36fc05ff, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@61d84e08, org.springframework.security.web.session.SessionManagementFilter@31ff6309, org.springframework.security.web.access.ExceptionTranslationFilter@10fbbdb, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4e1459ea]
把各個過濾器抽取出來,我們可以看到是這樣,這也是過濾器鏈的先后順序。
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- LogoutFilter
- UsernamePasswordAuthenticationFilter
- JwtAuthorizationTokenFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
介紹幾個主要的作用
- SecurityContextPersistenceFilter
- Filter的入口和出口,它是用來將SecurityContext(認證的上下文,里面有登錄成功后的認證授權信息)對象持久到Session的Filter,同時會把SecurityContext設置給SecurityContextHolder方便我們獲取用戶認證授權信息
- UsernamePasswordAuthenticationFilter
- 默認攔截“/login”登錄請求,處理表單提交的登錄認證,將請求中的認證信息包括username,password等封裝成UsernamePasswordAuthenticationToken,然后調用AuthenticationManager的認證方法進行認證
- BasicAuthenticationFilter
- 基本認證,支持httpBasic認證方式的Filter
- RememberAuthenticationFilter
- 記住我功能實現的Filter
- AnonymousAuthenticationFilter
- 匿名Filter,用來處理匿名訪問的資源,如果用戶未登錄,SecurityContext中沒有Authentication,就會創(chuàng)建匿名的Token(AnonymousAuthenticationToken),然后通過SecurityContextHodler設置到SecurityContext中。
- ExceptionTranslationFilter
- 用來捕獲FilterChain所有的異常,進行處理,但是只會處理 AuthenticationException和AccessDeniedException,異常,其他的異常 會繼續(xù)拋出。
- FilterSecurityInterceptor
用來做授權的Filter,通過父類(AbstractSecurityInterceptor.beforeInvocation)調用AccessDecisionManager.decide方法對用戶進行授權。
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter ,顧名思義,是用來處理用戶名密碼登錄的過濾器。所有的Filter核心方法都是 doFilter ,該過濾器的doFilter在其父抽象類中,過濾器只需實現 attemptAuthentication 方法即可。
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
//從登錄請求中獲取參數:username,password的名字
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
//默認支持POST登錄
private boolean postOnly = true;
//默認攔截/login請求,Post方式
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
// ~ Methods
// ========================================================================================================
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());
}
//獲取到用戶名和密碼
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//用戶名和密碼封裝Token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
//設置details屬性
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//調用AuthenticationManager().authenticate進行認證,參數就是Token對象
return this.getAuthenticationManager().authenticate(authRequest);
}AuthenticationManager
請求通過UsernamePasswordAuthenticationFilter調用AuthenticationManager,默認走的實現類是ProviderManager,它會找到能支持當前認證的AuthenticationProvider實現類調用器authenticate方法執(zhí)行認證,認證成功后會清除密碼,然后拋出AuthenticationSuccessEvent事件
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
...省略...
//這里authentication 是封裝了登錄請求的認證參數,
//即:UsernamePasswordAuthenticationFilter傳入的Token對象
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();
//找到所有的AuthenticationProvider ,選擇合適的進行認證
for (AuthenticationProvider provider : getProviders()) {
//是否支持當前認證
if (!provider.supports(toTest)) {
continue;
}
```java
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
//調用provider執(zhí)行認證
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
...省略...
}
...省略...
//result就是Authentication ,使用的實現類依然是UsernamepasswordAuthenticationToken,
//封裝了認證成功后的用戶的認證信息和授權信息
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
//這里在擦除登錄密碼
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
//發(fā)布事件
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}DaoAuthenticationProvider
請求到達AuthenticationProvider,默認實現是DaoAuthenticationProvider,它的作用是根據傳入的Token中的username調用UserDetailService加載數據庫中的認證授權信息(UserDetails),然后使用PasswordEncoder對比用戶登錄密碼是否正確
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
//密碼編碼器
private PasswordEncoder passwordEncoder;
//UserDetailsService ,根據用戶名加載UserDetails對象,從數據庫加載的認證授權信息
private UserDetailsService userDetailsService;
//認證檢查方法
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
```java
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
//獲取密碼
String presentedPassword = authentication.getCredentials().toString();
//通過passwordEncoder比較密碼,presentedPassword是用戶傳入的密碼,userDetails.getPassword()是從數據庫加載到的密碼
//passwordEncoder編碼器不一樣比較密碼的方式也不一樣
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
//檢索用戶,參數為用戶名和Token對象
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//調用UserDetailsService的loadUserByUsername方法,
//根據用戶名檢索數據庫中的用戶,封裝成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);
}
}
//創(chuàng)建認證成功的認證對象Authentication,使用的實現是UsernamepasswordAuthenticationToken,
//封裝了認證成功后的認證信息和授權信息,以及賬戶的狀態(tài)等
@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);
}
...省略...這里提供了三個方法
- additionalAuthenticationChecks:通過passwordEncoder比對密碼
- retrieveUser:根據用戶名調用UserDetailsService加載用戶認證授權信息
- createSuccessAuthentication:登錄成功,創(chuàng)建認證對象Authentication
然而你發(fā)現 DaoAuthenticationProvider 中并沒有authenticate認證方法,真正的認證邏輯是通過父類AbstractUserDetailsAuthenticationProvider.authenticate方法完成的
AbstractUserDetailsAuthenticationProvider
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
//認證邏輯
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//得到傳入的用戶名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
//從緩存中得到UserDetails
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
```java
try {
//檢索用戶,底層會調用UserDetailsService加載數據庫中的UserDetails對象,保護認證信息和授權信息
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
...省略...
}
try {
//前置檢查,主要檢查賬戶是否鎖定,賬戶是否過期等
preAuthenticationChecks.check(user);
//比對密碼在這個方法里面比對的
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
...省略...
}
//后置檢查
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
//設置UserDetails緩存
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//認證成功,創(chuàng)建Auhentication認證對象
return createSuccessAuthentication(principalToReturn, authentication, user);
}SecurityContextHolder
認證成功,請求會重新回到UsernamePasswordAuthenticationFilter,然后會通過其父類AbstractAuthenticationProcessingFilter.successfulAuthentication方法將認證對象封裝成SecurityContext設置到SecurityContextHolder中
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
```java
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
//認證成功,吧Authentication 設置到SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authResult);
//處理記住我業(yè)務邏輯
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//重定向登錄成功地址
successHandler.onAuthenticationSuccess(request, response, authResult);
}然后后續(xù)請求又會回到SecurityContextPersistenceFilter,它就可以從SecurityContextHolder獲取到SecurityContext持久到SecurityContextRepository(默認實現是HttpSessionSecurityContextRepository基于Session存儲)
到此這篇關于SpringSecurity身份認證原理解析的文章就介紹到這了,更多相關SpringSecurity身份認證內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
ThreadPoolExecutor參數含義及源碼執(zhí)行流程詳解
這篇文章主要為大家介紹了ThreadPoolExecutor參數含義及源碼執(zhí)行流程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11
java使用poi讀取ppt文件和poi讀取excel、word示例
這篇文章主要介紹了java使用poi讀取ppt文件和poi讀取excel、word示例,需要的朋友可以參考下2014-03-03
從概念到實踐的深度解析Java 面向對象進階之多態(tài)
多態(tài)作為Java面向對象編程的重要特性,通過繼承、方法重寫和父類引用指向子類對象,實現了同一操作在不同對象上的多樣化行為,本文給大家介紹從概念到實踐的深度解析Java 面向對象進階之多態(tài),感興趣的朋友一起看看吧2025-06-06
idea2020.2卡死在reading maven projects
這篇文章主要介紹了idea2020.2卡死在reading maven projects,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-09-09
Mybatis單個參數的if判斷報異常There is no getter for property named ''x
今天小編就為大家分享一篇關于Mybatis單個參數的if判斷報異常There is no getter for property named 'xxx' in 'class java.lang.Integer'的解決方案,小編覺得內容挺不錯的,現在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-12-12

