Spring Security认证机制源码层探究

作者:T.Y.Bao 时间:2022-07-27 19:05:26 

Spring Security提供如下几种认证机制

  • Username & Password

  • OAuth2.0 Login

  • SAML 2.0 Login

  • Remember Me

  • JAAS Authentication

  • Pre-authentication Scenarios

  • X509 Authentication

这里使用Spring Boot 2.7.4版本,对应Spring Security 5.7.3版本

Servlet Authentication Architecture

首先明确两个概念:

  • Principle : This interface represents the abstract notion of a principal, which can be used to represent any entity, such as an individual, a corporation, and a login id. 简单来说 可以认为是 唯一确定用户的 一个 userId ,但这个Principle是一个接口,具体参考 java.security.Principal

  • Credential : 通常就是一个密码,但他不是接口,而是通过接口Authentication#getCredentials来获取,返回一个Object类型。

Spring Security提供了以下几个核心类:

  • SecurityContextHolder : Spring Security用来保存被认证用户的详细信息(默认使用ThreadLocal保存);

  • SecurityContext : 从 SecurityContextHolder 获取得到,该接口提供被认证用户的 Authentication信息;

  • Authentication : 代表"认证",其中包含Principle和Credential,通过AuthenticationManager#authenticate来认证一个Authentication,该方法接受一个未认证的Authentication并返回一个认证后的Authentication。

  • GrantedAuthority : 认证后的Principle包含的权限

  • AuthenticationManager : 实施认证行为的接口,该类为函数式接口,只有一个方法: Authentication authenticate(Authentication authentication) throws AuthenticationException;

  • ProviderManager : the most common implementation of AuthenticationManager.

  • AuthenticationProvider : 提供认证方式的接口,组合在ProviderManager中。

Spring Security认证机制源码层探究

SecurityContextHolder

先来看一个使用SecurityContextHolder和SecurityContext完成认证的案例:

SecurityContext context = SecurityContextHolder.createEmptyContext();
// 手动生成一个Authentication,实际一般是通过数据库查出来生成的
Authentication authentication =
   new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);

默认 情况下 SecurityContext存储使用ThreadLocal方式,这个就是ThreadLocal的一个用处,避免函数间参数传递的复杂性,只要处于一个线程,就可以直接获取而不需要通过函数参数返回值来传递。

注意:在使用ThreadLocal时,由于键是其this本身,是一个弱引用,而值只能是强引用,所以ThreadLocal不用时需要手动clear。而Spring Security中,在 FilterChainProxy中完成这一清除工作,如下:

public class FilterChainProxy extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
...
try {
...
// 执行Spring Security 的 SecurityFilterChain
doFilterInternal(request, response, chain);
}
catch (Exception ex) {
...
}
finally {
// ***************
// 清除ThreadLocal
// ***************
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
}

但有些情形是用不了ThreadLocal的,For example, a Swing client might want all threads in a Java Virtual Machine to use the same security context 。针对其他情况,SecurityContextHolder提供了4种模式,当然也可以自定义:

  • MODE_THREADLOCAL

  • MODE_GLOBAL

  • MODE_INHERITABLETHREADLOCAL

  • MODE_PRE_INITIALIZED

可以通过2种方式去修改模式:

  • set a system property

  • a static method on SecurityContextHolder

public class SecurityContextHolder {
// 存储SecurityContext的模式,默认 ThreadLocal存储
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
// 通过系统参数指定 存储模式
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
// 实际存储SecurityContext的接口(类)
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
static {
// 初始化
initialize();
}
private static void initialize() {
initializeStrategy();
initializeCount++;
}
private static void initializeStrategy() {
...
if (!StringUtils.hasText(strategyName)) {
// Set default 默认 ThreadLocal
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
return;
}
...
}
// 修改Strategy
public static void setStrategyName(String strategyName) {
SecurityContextHolder.strategyName = strategyName;
initialize();
}
// 自定义Strategy
public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED;
SecurityContextHolder.strategy = strategy;
initialize();
}
public static SecurityContext getContext() {
return strategy.getContext();
}
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
public static void clearContext() {
strategy.clearContext();
}

可以看到,SecurityContextHolder实际上是一个门面,具体的Context存储在SecurityContextHolderStrategy中,来看看该接口默认的实现 ThreadLocalSecurityContextHolderStrategy :

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
// ThreadLocal
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
@Override
public void clearContext() {
contextHolder.remove();
}
@Override
public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
@Override
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(context);
}
@Override
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}

AuthenticationManager

public interface AuthenticationManager {
// 传入一个待认证的Authentication
// 返回 a fully authenticated object including credentials
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

AuthenticationManager的最常用实现类 ProviderManager,ProviderManager调用多个AuthenticationProvider来认证传入的Authentication,只要有一个认证成功即可返回(返回nonNull),否则会抛出异常,AuthenticationManager默认支持三种异常:

  • DisabledException : 用户账号被disabled

  • LockedException : 用户账号被locked

  • BadCredentialsException : credentials错误(密码错误)

/**
* ProviderManager中的List<AuthenticationProvider>会按顺序认证,知道有一个返回非空。
* 如果后面的 AuthenticationProvider返回非空认证结果,前面抛出的异常统统清除;如果后面还有异常,以第一个异常为准
*
* 该类中有一个 parent 的字段,类型也为AuthenticationManager,
* 如果该类中的List<AuthenticationProvider>都不能认证,会调用parent认证,这个不常用。
*
* 事件发布:
* ProviderManager中认证事件发布委托给 AuthenticationEventPublisher 实现,默认是空实现。
* parent 的 ProviderManager中不要实现 Publisher,否则会重复发布。
**/
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
// 事件发布,默认空实现
private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
// 实际认证的AuthenticationProvider
private List<AuthenticationProvider> providers = Collections.emptyList();
// 父级认证Manager
private AuthenticationManager parent;
@Override
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;
}
...
}
// 该类中AuthenticationManager都判断完了,结果还是空,调用parent开始认证
if (result == null && this.parent != null) {
// Allow the parent to try.
parentResult = this.parent.authenticate(authentication);
}
...
// AbstractAuthenticationToken就是Authentication接口的实现类,模板模式
// 常用的UsernamePasswordAuthenticationToken和OAuth2AuthenticationToken都extends这个Abstract类
private void copyDetails(Authentication source, Authentication dest) {
if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
token.setDetails(source.getDetails());
}
}
}

可以看到 ProviderManager implements AuthenticationManager将认证工作进一步分配给 AuthenticationProvider,而这个AuthenticationProvider会根据支持的认证方式来认证,所以这个接口除了认证方法还有一个是否支持认证的判断方法:

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

AuthenticationProvider常见的实现类有:

  • DaoAuthenticationProvider : An AuthenticationProvider implementation that retrieves user details from a UserDetailsService. 用于UsernamePassword认证方式

  • OAuth2AuthorizationCodeAuthenticationProvider : 在授权服务器上认证authorization_code,拿着code换accessToken

  • OAuth2LoginAuthenticationProvider : 在授权服务器上认证authorization_code,拿着code换accessToken(这一步委托给上面的 OAuth2AuthorizationCodeAuthenticationProvider执行了),此外,还会拿着accessToken换取UserInfo,属于上面的加强版,OAuth2.0 Login一般都是调用这个Provider。

  • OidcAuthorizationCodeAuthenticationProvider : 同上,不过在OAuth2.0基础上加了OpenID Connect协议。

来源:https://blog.csdn.net/weixin_41866717/article/details/128874722

标签:Spring,Security,认证机制
0
投稿

猜你喜欢

  • Java.try catch finally 的执行顺序说明

    2022-07-06 00:30:25
  • javax.persistence中@Column定义字段类型方式

    2021-12-03 21:21:44
  • Spring源码解析之BeanPostProcessor知识总结

    2022-04-07 22:13:34
  • 分享java中设置代理的两种方式

    2023-10-28 10:48:52
  • java最新版本连接mysql失败的解决过程

    2022-05-21 17:29:58
  • 使用@RequestBody配合@Valid校验入参参数

    2023-05-04 22:36:09
  • Spring Cloud Hystrix 服务降级限流策略详解

    2022-05-02 15:20:27
  • Java编程接口回调一般用法代码解析

    2023-11-11 06:55:11
  • 微信开发之使用java获取签名signature

    2022-08-01 10:47:01
  • c#用for语句输出一个三角形的方法

    2023-12-17 05:46:53
  • 使用fastjson中的JSONPath处理json数据的方法

    2021-12-14 09:09:58
  • Swagger实现动态条件注入与全局拦截功能详细流程

    2023-11-23 13:41:05
  • Spark调优多线程并行处理任务实现方式

    2023-08-21 15:43:53
  • 一篇文章带你入门Java数据类型

    2022-06-10 09:25:44
  • Java数组索引异常产生及解决方案

    2023-11-05 16:52:27
  • spring如何动态指定具体实现类

    2022-04-13 07:52:21
  • android使用flutter的ListView实现滚动列表的示例代码

    2023-06-26 09:00:13
  • Java Swing实现扫雷源码

    2023-11-10 08:16:20
  • Spring MVC接口防数据篡改和重复提交

    2023-11-29 15:02:11
  • spring cloud将spring boot服务注册到Eureka Server上的方法

    2023-12-08 19:42:09
  • asp之家 软件编程 m.aspxhome.com