Spring Security 身份認證源碼解析

1. 認證信息的存儲

本文參考:
Spring Security 架構與源碼分析
spring security——基本介紹(一)

Spring Security requires a Java 8 or higher Runtime Environment.

當用戶登錄的時候,一般來說要校驗用戶名密碼的正確性。還要查詢此用戶的權限信息。這些信息被保存在Authentication中。
UsernamePasswordAuthenticationFilter的校驗過程爲例,如下圖所示
用戶名密碼被放在authRequest 中,作爲入參遞給AuthenticationManager的方法authenticate(authRequest)。

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
 }

在這裏插入圖片描述

1.1 Authentication 10.3

在這裏插入圖片描述
Authentication有兩個作用
1.AuthenticationManager的輸入,提供用戶提供的用於身份驗證的憑據。在此場景中使用時,isAuthenticated()返回false
2.表示當前已驗證的用戶。當前的身份驗證可以從SecurityContext中獲得。

包含信息:

  • principal -識別用戶。當使用用戶名/密碼進行身份驗證時,這通常是UserDetails的一個實例。.
  • credentials - 通常一個密碼。在許多情況下,在對用戶進行身份驗證以確保其不泄漏之後,會清除此信息。.
  • authorities - GrantedAuthoritys是授予用戶的高級權限。例如:角色或範圍。

isAuthenticated() @return true if the token has been authenticated and the AbstractSecurityInterceptor does not need to present the token to the AuthenticationManager again for re-authentication.

if (authentication.isAuthenticated() && !alwaysReauthenticate) {
	return authentication;
}
// 否則
authentication = authenticationManager.authenticate(authentication);

1.2 SecurityContextHolder 10.1

我們從SecurityContextHolder中設置並獲取身份認證信息Authentication。
在這裏插入圖片描述

Example 53. 手動設置身份驗證信息

SecurityContext context = SecurityContextHolder.createEmptyContext(); 
Authentication authentication =
    new TestingAuthenticationToken("username", "password", "ROLE_USER"); 
context.setAuthentication(authentication);

SecurityContextHolder.setContext(context); 

Example 54. 獲取當前用戶

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

在這裏插入圖片描述

org.springframework.security.core.context.SecurityContextHolder 是 SecurityContext的存放容器,默認使用ThreadLocal (MODE_THREADLOCAL)存儲,意味SecurityContext在相同線程中的方法都可用。

private static void initialize() {
	if (!StringUtils.hasText(strategyName)) {
				// Set default
				strategyName = MODE_THREADLOCAL;
	}
	if (strategyName.equals(MODE_THREADLOCAL)) {
	    strategy = new ThreadLocalSecurityContextHolderStrategy();
	}
	……
}

成員變量 SecurityContextHolderStrategy strategy;
默認 ThreadLocalSecurityContextHolderStrategy
在這裏插入圖片描述

1.3 SecurityContext 10.2.

SecurityContextImpl實現接口SecurityContext 的兩個方法,存放身份驗證信息Authentication
在這裏插入圖片描述

2. 身份認證過程

一般在攔截器中會調用AuthenticationManager的authenticate方法。
在這裏插入圖片描述

2.1 AuthenticationManager 10.5

AuthenticationManager is the API that defines how Spring Security’s Filters perform authentication. The Authentication that is returned is then set on the SecurityContextHolder by the controller (i.e. Spring Security’s Filterss) that invoked the AuthenticationManager.

public interface AuthenticationManager {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}

authenticate()方法主要做三件事:

  1. 如果驗證通過,返回Authentication(通常帶上authenticated=true)。
  2. 認證失敗拋出AuthenticationException
  3. 如果無法確定,則返回null

AuthenticationManager的默認實現是ProviderManager,它委託一組AuthenticationProvider實例來實現認證 this.getProviders().iterator();
在這裏插入圖片描述

2.2 ProviderManager 10.6

在這裏插入圖片描述
在這裏插入圖片描述

ProviderManager包含一組AuthenticationProvider,執行authenticate時,遍歷Providers,然後調用supports,如果支持,則執行遍歷當前provider的authenticate方法,如果一個provider認證成功,則break,返回Authentication。

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
	for (AuthenticationProvider provider : getProviders()) {
		if (!provider.supports(toTest)) {
			continue;
		}
		result = provider.authenticate(authentication);
		if (result != null) {
			copyDetails(authentication, result);
			break;
		}
	}

	if (result == null && parent != null) {
		result = parentResult = parent.authenticate(authentication);
	}

	if (result != null) {
		if (eraseCredentialsAfterAuthentication
				&& (result instanceof CredentialsContainer)) {
			((CredentialsContainer) result).eraseCredentials();
		}
		if (parentResult == null) {
			eventPublisher.publishAuthenticationSuccess(result);
		}
		return result;
	}

}

從上面的代碼可以看出, ProviderManager有一個可選parent,如果parent不爲空,則調用parent.authenticate(authentication)

2.2.1 AuthenticationProvider 10.7

public interface AuthenticationProvider {
    Authentication authenticate(Authentication var1) throws    AuthenticationException;
    boolean supports(Class<?> var1);
}

Multiple AuthenticationProviders can be injected into ProviderManager.
Each AuthenticationProvider performs a specific type of authentication. For example, DaoAuthenticationProvider supports username/password based authentication while JwtAuthenticationProvider supports authenticating a JWT token.

AuthenticationProvider有多種實現,大家最關注的通常是DaoAuthenticationProvider,繼承於abstract class AbstractUserDetailsAuthenticationProvider,核心是通過UserDetails來實現認證,DaoAuthenticationProvider默認會自動加載,不用手動配。
在這裏插入圖片描述

2.2.2 AbstractUserDetailsAuthenticationProvider

public abstract class AbstractUserDetailsAuthenticationProvider implements
		AuthenticationProvider, InitializingBean, MessageSourceAware

核心的authenticate 僞代碼:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED": authentication.getName();
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");
				throw notFound;
			}
		}

		try {
			preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				cacheWasUsed = false;
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			else {
				throw exception;
			}
		}

		postAuthenticationChecks.check(user);

		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}

		return createSuccessAuthentication(principalToReturn, authentication, user);

  • preAuthenticationChecks.check(user); 查看用戶isAccountNonLocked,isEnabled,isAccountNonExpired
  • postAuthenticationChecks.check(user) 查看用戶isCredentialsNonExpired

抽象方法

  • retrieveUser(username,authentication) 獲取用戶信息
  • additionalAuthenticationChecks 驗證用戶信息
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;
	}

2.2.3 DaoAuthenticationProvider 10.10.9

在這裏插入圖片描述

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    this.prepareTimingAttackProtection();
    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
    if (loadedUser == null) {
        throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
    } else {
        return loadedUser;
    }
   
}
protected void additionalAuthenticationChecks(UserDetails userDetails,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
   String presentedPassword = authentication.getCredentials().toString();

   if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
      logger.debug("Authentication failed: password does not match stored value");
   }
}

所我們是從UserDetailsService.loadUserByUsername(username)來獲取用戶,
然後比較密碼是否相同

2.2.4 UserDetailsService 10.10.9

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

默認四種實現,獲取當前用戶名的用戶信息
在這裏插入圖片描述
可參考: Spring Security5 四種添加用戶的方式

本文都是通過userDetailsService這種方式來添加用戶

2.2.4.1 InMemoryUserDetailsManager 10.10.4

在這裏插入圖片描述
在配置文件中創建用戶user/abc,admin/abc

@Override
    protected void configure (AuthenticationManagerBuilder auth) throws Exception{
        auth.userDetailsService(users()).passwordEncoder(passwordEncoder());
    }
    @Bean
    public UserDetailsService users() {
        UserDetails user = User.builder()
                .username("user")
                .password("$2a$10$6Ed5ZXTH9DNwKzeFU07Ks.jGr3beIjU3o6mi12Jgh4Rh2t0goLbqO")
                .roles("USER")
                .build();
        UserDetails admin = User.builder()
                .username("admin")
                .password("$2a$10$6Ed5ZXTH9DNwKzeFU07Ks.jGr3beIjU3o6mi12Jgh4Rh2t0goLbqO")
                .roles("USER", "ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user, admin);
    }

用戶信息被存放在final Map<String, MutableUserDetails> users 中

public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException {
		UserDetails user = users.get(username.toLowerCase());

		if (user == null) {
			throw new UsernameNotFoundException(username);
		}

		return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
				user.isAccountNonExpired(), user.isCredentialsNonExpired(),
				user.isAccountNonLocked(), user.getAuthorities());
	}

然後用loadUserByUsername獲取

2.2.4.2 JdbcUserDetailsManager 10.10.5

JdbcUserDetailsManager繼承JdbcDaoImpl

 @Bean
 UserDetailsManager users() {
     UserDetails user = User.builder()
             .username("user")
             .password("$2a$10$6Ed5ZXTH9DNwKzeFU07Ks.jGr3beIjU3o6mi12Jgh4Rh2t0goLbqO")
             .roles("USER")
             .build();
     JdbcUserDetailsManager users = new JdbcUserDetailsManager(datasource);
     users.createUser(user);
     return users;
 }

users.createUser(user)會在數據庫中創建這個用戶和他的權限

public void createUser(final UserDetails user) {
		validateUserDetails(user);
        // 創建用戶
		getJdbcTemplate().update(createUserSql, ps -> {
			ps.setString(1, user.getUsername());
			ps.setString(2, user.getPassword());
			ps.setBoolean(3, user.isEnabled());

			int paramCount = ps.getParameterMetaData().getParameterCount();
			if (paramCount > 3) {
				//NOTE: acc_locked, acc_expired and creds_expired are also to be inserted
				ps.setBoolean(4, !user.isAccountNonLocked());
				ps.setBoolean(5, !user.isAccountNonExpired());
				ps.setBoolean(6, !user.isCredentialsNonExpired());
			}
		});
      
		if (getEnableAuthorities()) {
		    // 創建權限
			insertUserAuthorities(user);
		}
	}
2.2.4.3 CustomUserDetailsService 10.10.7

創建CustomUserDetailsService繼承UserDetailsService

@Override
protected void configure (AuthenticationManagerBuilder auth) throws Exception{
    auth.userDetailsService(customUserDetailsService()).passwordEncoder(passwordEncoder());
}
@Bean
CustomUserDetailsService customUserDetailsService() {
    return new CustomUserDetailsService();
}
2.2.4.4 LDAP Authentication 10.10.10.

3. 創建認證管理

3.1 AuthenticationManagerBuilder

在這裏插入圖片描述
從名字可以看出AuthenticationManagerBuilder就是用來創建AuthenticationManager的

@Override
protected void configure (AuthenticationManagerBuilder auth) throws Exception{
    auth.userDetailsService(customUserDetailsService()).passwordEncoder(passwordEncoder());
}
    public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(T userDetailsService) throws Exception {
        this.defaultUserDetailsService = userDetailsService;
        return (DaoAuthenticationConfigurer)this.apply(new DaoAuthenticationConfigurer(userDetailsService));
    }

使用userDetailsService()添加用戶時(假設CustomUserDetailsService)會返回

DaoAuthenticationConfigurer<AuthenticationManagerBuilder, CustomUserDetailsService> 對象
在這裏插入圖片描述

3.2 ProviderManagerBuilder

public interface ProviderManagerBuilder<B extends ProviderManagerBuilder<B>> extends SecurityBuilder<AuthenticationManager> {
    B authenticationProvider(AuthenticationProvider var1);
}

AuthenticationManagerBuilder的實現

public AuthenticationManagerBuilder authenticationProvider(AuthenticationProvider authenticationProvider) {
    this.authenticationProviders.add(authenticationProvider);
    return this;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章