二SpringSecurity核心组件类--2SecurityContextUserDetailsServiceAuthenticationManager

二 SpringSecurity核心组件类--2SecurityContext/UserDetailsService/AuthenticationManager

2.1 SecurityContext

SecurityContext是安全的上下文,在用户通过spring security的校验之后,所有的验证信息数据都是保存到SecurityContext中。

public interface SecurityContext extends Serializable {
    Authentication getAuthentication(); //可以获取Authentication

    void setAuthentication(Authentication var1);
}

2.2 Authentication

表示当前的认证情况。

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
		//密码
    Object getCredentials();
		//获取用户信息,userDetails
    Object getDetails();
		//获取用户:未认证,为username;已认证,为userDetails
    Object getPrincipal();
		//是否已经认证过
    boolean isAuthenticated();

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

注意:
Object getPrincipal(); //获取用户:未认证,为username;已认证,为userDetails。

这四个属性及其意义如下:

getAuthorities: 获取用户权限,一般情况下获取到的是用户的角色信息。
getCredentials: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。
getDetails: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)
getPrincipal: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (UserDetails也是一个接口,里边的方法有getUsername,getPassword等)。
isAuthenticated: 获取当前 Authentication 是否已认证。
setAuthenticated: 设置当前 Authentication 是否已认证(true or false)

2.3 SecurityContextHolder---默认是线程变量保存securityContext

SecurityContextHolder用来获取SecurityContext中保存的数据的工具。通过使用静态方法获取SecurityContext的相对应的数据。

SecurityContext context = SecurityContextHolder.getContext();

在典型的web应用程序中,用户登录一次,然后由其会话ID标识。服务器缓存持续时间会话的主体信息。在Spring Security中,在请求之间存储SecurityContext的责任落在SecurityContextPersistenceFilter上,默认情况下,该上下文将上下文存储为HTTP请求之间的HttpSession属性。它会为每个请求恢复上下文SecurityContextHolder,并且最重要的是,在请求完成时清除SecurityContextHolder`。SecurityContextHolder是一个类,他的功能方法都是静态的(static)。

默认情况下,使用线程变量保存securityContext上下文

public class SecurityContextHolder {
	// ~ Static fields/initializers
	// =====================================================================================

	public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL"; //本地线程变量的mode模式
	public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
	public static final String MODE_GLOBAL = "MODE_GLOBAL";
	public static final String SYSTEM_PROPERTY = "spring.security.strategy";
	private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
	private static SecurityContextHolderStrategy strategy;
	private static int initializeCount = 0;
  
  	static {
		initialize();
	}
  
  	private static void initialize() {
		if (!StringUtils.hasText(strategyName)) {
			// Set default
			strategyName = MODE_THREADLOCAL; //默认模式时本地线程变量
		}

		if (strategyName.equals(MODE_THREADLOCAL)) {
			strategy = new ThreadLocalSecurityContextHolderStrategy(); //默认
		}
		else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
			strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
		}
		else if (strategyName.equals(MODE_GLOBAL)) {
			strategy = new GlobalSecurityContextHolderStrategy();
		}
		else {
			// Try to load a custom strategy
			try {
				Class<?> clazz = Class.forName(strategyName);
				Constructor<?> customStrategy = clazz.getConstructor();
				strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
			}
			catch (Exception ex) {
				ReflectionUtils.handleReflectionException(ex);
			}
		}

		initializeCount++;
	}
  
  //获取SecurityContext
 	public static SecurityContext getContext() {
		return strategy.getContext();
	}
final class ThreadLocalSecurityContextHolderStrategy implements
		SecurityContextHolderStrategy {

	private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
	
		public SecurityContext getContext() {
		SecurityContext ctx = contextHolder.get();

		if (ctx == null) {
			ctx = createEmptyContext();
			contextHolder.set(ctx);
		}

		return ctx;
	}
2.3.1 获取当前用户信息

如下:

	@RequestMapping("/info")
	@ResponseBody
	public String productInfo(){
		String currentUser = "";
		Object principl = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
		if(principl instanceof UserDetails) {
			currentUser = ((UserDetails)principl).getUsername();
		}else {
			currentUser = principl.toString();
		}
		return " some product info,currentUser is: "+currentUser;
	}

这里,我们通过SecurityContextHolder来获取了用户信息,并拼接成字符串输。

2.4 userDetails

Spring Security不会出于安全目的直接使用实现。它们只是存储用户信息,这些信息稍后封装到Authentication对象中。这允许非安全相关的用户信息(如电子邮件地址,电话号码等)存储在一个方便的位置。

 public interface UserDetails extends Serializable {
 
   Collection<? extends GrantedAuthority> getAuthorities();
   String getPassword();
   String getUsername();
   boolean isAccountNonExpired();
   boolean isAccountNonLocked();
   boolean isCredentialsNonExpired();
   boolean isEnabled();
 }

2.5 UserDetailsService

这个方法,可以自行实现,从redis、db中获取用户信息,然后封装为userDetails。需要封装username、password、List三个对象。

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

该接口提供给spring security作用户验证。

提到了UserDetails就必须得提到UserDetailsService, UserDetailsService也是一个接口,且只有一个方法loadUserByUsername,他可以用来获取UserDetails。

通常在spring security应用中,我们会自定义一个CustomUserDetailsService来实现UserDetailsService接口,并实现其public UserDetails loadUserByUsername(final String login);方法。我们在实现loadUserByUsername方法的时候,就可以通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息,然后组装成一个UserDetails,(通常是一个org.springframework.security.core.userdetails.User,它继承自UserDetails) 并返回。

在实现loadUserByUsername方法的时候,如果我们通过查库没有查到相关记录,需要抛出一个异常来告诉spring security来“善后”。这个异常是org.springframework.security.core.userdetails.UsernameNotFoundException

2.6 AuthenticationManager

该认证管理器,处理authentication认证的request请求。

AuthenticationManager 的作用就是校验Authentication,如果验证失败会抛出AuthenticationException异常。AuthenticationException是一个抽象类,因此代码逻辑并不能实例化一个AuthenticationException异常并抛出,实际上抛出的异常通常是其实现类,如DisabledException,LockedException,BadCredentialsException等。BadCredentialsException可能会比较常见,即密码错误的时候。

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

image-20230815201847155

该认证,由ProviderManager默认实现

2.6.1 ProviderManager(实现类)

主要作用,是实现AuthenticationManager的authenticate方法。其内初始化了List providers,在认证时,遍历该providers,找到支持当前认证的provider,然后调用AuthenticationProvider.authenticate方法认证

ProviderManager 是的最常用的 AuthenticationManager 实现类。

ProviderManager 管理了一个 AuthenticationProvider 列表,每个 AuthenticationProvider 都是一个认证器,不同的 AuthenticationProvider 用来处理不同的 Authentication 对象的认证。一次完整的身份认证流程可能会经过多个 AuthenticationProvider。

ProviderManager 相当于代理了多个 AuthenticationProvider,他们的关系如下图:

image-20230822233157521

源码如下:

 public class ProviderManager implements AuthenticationManager, MessageSourceAware,
     InitializingBean {
 
       //AuthenticationProvider集合
   private List<AuthenticationProvider> providers = Collections.emptyList();
   
   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();
 
     for (AuthenticationProvider provider : getProviders()) {
       if (!provider.supports(toTest)) { //遍历providers,找到支持当前authentication的provider
         continue;
       }
 
       if (debug) {
         logger.debug("Authentication attempt using "
             + provider.getClass().getName());
       }
 
       try {
         //调用AuthenticationProvider.authenticate,并返回authentication
         result = provider.authenticate(authentication);
 
         if (result != null) {
           copyDetails(authentication, result);
           break;
         }
       }
       catch (AccountStatusException e) {
   //.........省略无关代码
   }
2.6.2 AuthenticationProvider(多个AP被authenticationManager管理)

image-20230822233523033

AuthenticationProvider 定义了 Spring Security 中的验证逻辑,我们来看下 AuthenticationProvider 的定义:

 public interface AuthenticationProvider {
 
   /** 参数:authentication -身份验证请求对象。
   返回:包含凭据的完全认证对象。如果AuthenticationProvider无法支持通过的身份验证对象的身份验证,则可能返回null。在这种情况下,将尝试支持所提供的身份验证类的下一个AuthenticationProvider。*/
   Authentication authenticate(Authentication authentication) throws AuthenticationException;
   
   boolean supports(Class<?> authentication);

可以看到,AuthenticationProvider 中就两个方法:

  • authenticate 方法用来做验证,就是验证用户身份。
  • supports 则用来判断当前的 AuthenticationProvider 是否支持对应的 Authentication。

在一次完整的认证中,可能包含多个 AuthenticationProvider,而这多个 AuthenticationProvider 则由 ProviderManager 进行统一管理。

最常用的 AuthenticationProvider 实现类是 DaoAuthenticationProvider

2.6.3 Parent--(全局的authenticationManager,可为多个ap配置同一个parent)

每一个 ProviderManager 管理多个 AuthenticationProvider,同时每一个 ProviderManager 都可以配置一个 parent,如果当前的 ProviderManager 中认证失败了,还可以去它的 parent 中继续执行认证,所谓的 parent 实例,一般也是 ProviderManager,也就是 ProviderManager 的 parent 还是 ProviderManager。可以参考如下架构图:

image-20230822233957136

从上面的分析中大家可以看出,AuthenticationManager 的初始化会分为两块,一个全局的 AuthenticationManager,也就是 parent,另一个则是局部的 AuthenticationManager。先给大家一个结论,一个系统中,我们可以配置多个 HttpSecurity,而每一个 HttpSecurity 都有一个对应的 AuthenticationManager 实例(局部 AuthenticationManager),这些局部的 AuthenticationManager 实例都有一个共同的 parent,那就是全局的 AuthenticationManager。

2.7 AuthenticationManagerBuilder(创建AM,并是securityBuilder的子类)

image-20230822234350464

在关于securityBuilder的内容分析时,已经分析过其子类和父类包含的方法,所以AuthenticationManagerBuilder也具有父类功能。

查看其关键方法:

 public class AuthenticationManagerBuilder extends
     AbstractConfiguredSecurityBuilder<AuthenticationManager, AuthenticationManagerBuilder>
     implements ProviderManagerBuilder<AuthenticationManagerBuilder> {
   
   public AuthenticationManagerBuilder(ObjectPostProcessor<Object> objectPostProcessor) {
     super(objectPostProcessor, true);
   }
   
   //给AuthenticationManager设置parent
   public AuthenticationManagerBuilder parentAuthenticationManager( AuthenticationManager authenticationManager) {
     if (authenticationManager instanceof ProviderManager) {
       eraseCredentials(((ProviderManager) authenticationManager)
           .isEraseCredentialsAfterAuthentication());
     }
     this.parentAuthenticationManager = authenticationManager;
     return this;
   }
   
 /**…………………………………………………………………………………………………………………………配置数据源………………………………………………………………………………………… */
   //调用apply,创建securityConfigurer
   public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication()
       throws Exception {
     return apply(new InMemoryUserDetailsManagerConfigurer<>());
   }
   
   public JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> jdbcAuthentication()
       throws Exception {
     return apply(new JdbcUserDetailsManagerConfigurer<>());
   }
   
   public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(
       T userDetailsService) throws Exception {
     //设置userDetailsService用户搜索service
     this.defaultUserDetailsService = userDetailsService;
     return apply(new DaoAuthenticationConfigurer<>(
         userDetailsService));
   }
   
   //该performBuild用来创建ProviderManager(认证管理器)
   //注意:由于securityBuilder使用了泛型,所以不同于httpSecurity.performBuild返回httpSecurity,这里返回的是ProviderManager,都是securityBuilder的子类
   @Override
   protected ProviderManager performBuild() throws Exception {
     if (!isConfigured()) {
       logger.debug("No authenticationProviders and no parentAuthenticationManager defined. Returning null.");
       return null;
     }
     //创建ProviderManager
     ProviderManager providerManager = new ProviderManager(authenticationProviders,
         parentAuthenticationManager);
     if (eraseCredentials != null) {
       providerManager.setEraseCredentialsAfterAuthentication(eraseCredentials);
     }
     if (eventPublisher != null) {
       providerManager.setAuthenticationEventPublisher(eventPublisher);
     }
     //将providerManager注册入spring的容器中
     providerManager = postProcess(providerManager);
     return providerManager;
   }
 }
  1. 首先,我们可以通过调用 parentAuthenticationManager 方法来给一个 AuthenticationManager 设置 parent。
  2. inMemoryAuthentication、jdbcAuthentication 以及 userDetailsService 几个方法作用就是为了配置数据源,这里就不再赘述。
  3. 最后就是 performBuild 方法,这个方法的作用就是根据当前 AuthenticationManagerBuilder 来构建一个 AuthenticationManager 出来,AuthenticationManager 本身是一个接口,它的默认实现是 ProviderManager,所以这里构建的就是 ProviderManager。在构建 ProviderManager 时,一方面传入 authenticationProviders,就是该 ProviderManager 所管理的所有的 AuthenticationProvider,另一方面传入 ProviderManager 的 parent(其实也是一个 ProviderManager)。

不过自己配置有一个问题就是我们没有配置 ProviderManager 的 parent,没有配置的话,如果当前 ProviderManager 中认证失败的话,就直接抛出失败,而不会去 parent 中再次进行认证了(一般来说也不需要,如果系统比较复杂的话,可能需要)。

AuthenticationManagerBuilder 还有一个实现类叫做 DefaultPasswordEncoderAuthenticationManagerBuilder,作为内部类分别定义在 WebSecurityConfigurerAdapter 和 AuthenticationConfiguration 中,不过 DefaultPasswordEncoderAuthenticationManagerBuilder 的内容比较简单,重写了父类 AuthenticationManagerBuilder 的几个方法,配置了新的 PasswordEncoder,无他,所以这里我就不列出这个的源码了,感兴趣的小伙伴可以自行查看。但是这并不是说 DefaultPasswordEncoderAuthenticationManagerBuilder 就不重要了,因为在后面的使用中,基本上都是使用 DefaultPasswordEncoderAuthenticationManagerBuilder 来构建 AuthenticationManagerBuilder。

好啦,这就是 AuthenticationManagerBuilder。

那么是什么时候通过 AuthenticationManagerBuilder 来构建 AuthenticationManager 的呢?

这就涉及到我们的老熟人 WebSecurityConfigurerAdapter 了。当然,关于 WebSecurityConfigurerAdapter 本身的初始化过程,后面会介绍,今天我们主要来看下如何在 WebSecurityConfigurerAdapter 中开启 AuthenticationManager 的初始化的。

2.7.1 authenticationManager初始化流程--todo

2.8 PasswordEncoder

密码加密器。通常是自定义指定。

 BCryptPasswordEncoder:哈希算法加密
 
 NoOpPasswordEncoder:不使用加密 
posted @ 2023-08-28 22:56  LeasonXue  阅读(72)  评论(0编辑  收藏  举报