当前位置: 首页 > news >正文

Spring Security 身份验证的基本类/架构

目录

1、SecurityContextHolder 核心类

2、SecurityContext 接口

3、Authentication 用户认证信息接口

4、GrantedAuthority 拥有权限接口

5、AuthenticationManager 身份认证管理器接口

6、ProviderManager 身份认证管理器的实现

7、AuthenticationProvider 特定类型的身份验证接口

8、AuthenticationEntryPoint 身份验证方案接口

9、AbstractAuthenticationProcessingFilter 用户凭据验证过滤器


        // Spring Security 基于 Servlet 的身份验证和身份验证的基本构件

  • SecurityContextHolder(类)——用来存储身份验证详细信息//验证流程核心类
  • SecurityContext(接口)——从SecurityContextHolder 中获取,包含当前认证用户的身份验证信息
  • Authentication(接口) ——该对象可以通过 AuthenticationManager 进行赋值,作为用户身份验证的凭据,然后通过 SecurityContext 获取 Authentication 的详细信息。
  • GrantedAuthority(接口)——用于对用户的授权
  • AuthenticationManager(接口)——用于定义 Spring Security 过滤器如何执行身份验证的API。
  • ProviderManager(类)——AuthenticationManager 最常见的实现。//常规实现
  • AuthenticationProvider(接口)——提供给ProviderManager,用于执行特定类型的身份验证//身份验证的具体细节由xxxProvider提供
  • AuthenticationEntryPoint(接口)——用于从客户端请求用户凭据(例如,重定向到登录页面,发送WWW-Authenticate响应等)
  • AbstractAuthenticationProcessingFilter(抽象类)——用于身份验证的基本过滤器。通过该类可以很好地了解身份验证流程及各个部分如何协同工作的过程。//所有身份验证的过滤器都会实现该类

1、SecurityContextHolder 核心类

        SecurityContextHolder 类是 Spring Security 身份验证模型的核心, 在该类中包含了存储用户认证信息的上下文 SecurityContext。

        下边是一个对象之间的包含关系图,它很重要,对于理解 Spring Security 中的对象关系非常有用:

        SecurityContextHolder 用来存储身份验证详细信息。Spring Security 并不关心SecurityContextHolder 是如何填充的。如果其中包含值,则使用该值作为当前通过认证的用户信息。//只要SecurityContextHolder有值就会被使用

        所以,指示用户已被认证的最简单方法就是直接设置 SecurityContextHolder:

SecurityContext context = SecurityContextHolder.createEmptyContext(); //1-创建Authentication -> Token接口
Authentication authentication = new TestingAuthenticationToken("username", "password", "ROLE_USER"); 
//2-设置用户信息
context.setAuthentication(authentication);SecurityContextHolder.setContext(context); 

        使用 TestingAuthenticationToken,是因为它非常简单。也可以使用 UsernamePasswordAuthenticationToken(UserDetails、密码、权限)等。

         // 定义一个Authentication -> 放入SecurityContext -> 放入 SecurityContextHolder

        在 SecurityContextHolder 上设置完 SecurityContextSpring Security 会使用此信息进行授权。如果要获取已验证用户的有关信息,可以通过SecurityContextHolder获得:

//1-获取Security上下文
SecurityContext context = SecurityContextHolder.getContext();//2-获取用户通过身份验证的对象
Authentication authentication = context.getAuthentication();//-通过Authentication获取用户名称(密码方式)
String username = authentication.getName(); 
//-通过Authentication被认证用户的身份信息。
Object principal = authentication.getPrincipal(); 
//-通过Authentication用户权限信息
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

        默认情况下,SecurityContextHolder 使用 ThreadLocal 来存储 SecurityContext,这意味着 SecurityContext 对于同一线程中的方法总是可用的,并不需要把 SecurityContext 作为参数显式地传递给这些方法。//SecurityContext 属于线程私有,如果我们不想显示传参,也可以这样用

    //包含SecurityContext的成员变量private static SecurityContextHolderStrategy strategy; //初始化方法private static void initialize() {//1-如果没有指定mode,使用MODE_THREADLOCALif (!StringUtils.hasText(strategyName)) { strategyName = "MODE_THREADLOCAL";}if (strategyName.equals("MODE_THREADLOCAL")) {//2-默认策略strategy = new ThreadLocalSecurityContextHolderStrategy(); } else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {strategy = new InheritableThreadLocalSecurityContextHolderStrategy();} else if (strategyName.equals("MODE_GLOBAL")) {strategy = new GlobalSecurityContextHolderStrategy();} else {try {Class<?> clazz = Class.forName(strategyName);Constructor<?> customStrategy = clazz.getConstructor();strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();} catch (Exception var2) {ReflectionUtils.handleReflectionException(var2);}}++initializeCount;}// MODE_THREADLOCAL策略final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {// 使用ThreadLocal保存SecurityContextprivate static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal();ThreadLocalSecurityContextHolderStrategy() {}//省略...}

        因为使用了 ThreadLocal 存储值,如果处理完当前用户请求后需要清除线程中的ThreadLocal,避免内存泄漏。Spring SecurityFilterChainProxy会确保SecurityContext总是被清除//FilterChainProxy是一个过滤器链的总代理,在总体架构那篇文章中提到过

    //在FilterChainProxy中的doFilter方法中清除public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;if (!clearContext) {this.doFilterInternal(request, response, chain);} else {try {request.setAttribute(FILTER_APPLIED, Boolean.TRUE);this.doFilterInternal(request, response, chain);} catch (RequestRejectedException var9) {this.requestRejectedHandler.handle((HttpServletRequest)request, (HttpServletResponse)response, var9);} finally {//处理完一个用户请求时,SecurityContext在这里被清除SecurityContextHolder.clearContext();   request.removeAttribute(FILTER_APPLIED);}}}

2、SecurityContext 接口

        SecurityContext 从 SecurityContextHolder 中获取。SecurityContext 包含一个 Authentication 对象

// SecurityContext接口
public interface SecurityContext extends Serializable {//接口很简单,只包含对Authentication的get和set方法Authentication getAuthentication();void setAuthentication(Authentication var1);
}

        SecurityContext 的实现类 SecurityContextImpl 也很简单,也就是对 Authentication 对象 get 和 set 方法的实现,所以重点还是 Authentication 对象。

3、Authentication 用户认证信息接口

        在 Spring Security 中,Authentication 接口有两个主要用途:

  1. 作为 AuthenticationManager 对象 authenticate方法的入参,用于对用户进行身份验证。在此场景中,isAuthenticated() 返回 false。
  2. 获取已经过身份验证的用户信息,可以从 SecurityContext 中获取当前的用户信息。

        一旦请求通过了AuthenticationManager#authenticate,Authentication 通常会存储在线程本地的 SecurityContext 中,由 SecurityContextHolder 进行管理。//Authentication->SecurityContext

        注意,除非 Authentication 将 isAuthenticated 属性设置为 true,否则安全拦截器仍然会对其进行身份验证。

        Authentication 接口详情://每个方法的注释都值得好好去读

public interface Authentication extends Principal, Serializable {//1-权限:由AuthenticationManager设置,用于指示已授予主体的权限。认证后不能为空(即无权限)Collection<? extends GrantedAuthority> getAuthorities();//2-密码:证明委托人正确的凭据。通常是密码Object getCredentials();//3-用户详情:存储有关身份验证请求的其他详细信息。如IP地址,证书序列号等Object getDetails();//4-用户名:被认证主体的身份。通常是用户名,许多身份验证提供程序将创建UserDetails对象作为主体Object getPrincipal();//用于指示AbstractSecurityInterceptor是否应该向AuthenticationManager提供身份验证令牌。//如果令牌已经过身份验证,并且AbstractSecurityInterceptor不需要再次向AuthenticationManager提供令牌以进行重新身份验证,则为true。boolean isAuthenticated();//如果令牌应该被信任(这可能导致异常),则为true;如果令牌不应该被信任,则为falsevoid setAuthenticated(boolean var1) throws IllegalArgumentException;
}

        所以,综合上述,Authentication 包含三方面信息://总结起来就是:用户信息+权限

  1. Principal:用户主体信息,该信息通常是 UserDetails 的一个实例。
  2. Credentials:认证凭据,通常是密码。在许多情况下,用户通过身份验证后会被ProviderManager清除该属性,以确保凭据不会泄露。
  3. Authorities:用户权限,由 GrantedAuthority 进行授予。

4、GrantedAuthority 拥有权限接口

        表示授予身份验证对象的权限。该接口的内容提供给授权管理器AuthorizationManager使用,以确定身份验证是否有权访问特定对象。//保存的是用户角色

//GrantedAuthority接口
public interface GrantedAuthority extends Serializable {String getAuthority(); //返回的是一个字符串 String role
}

        // 所以,GrantedAuthority,这个玩意是用来授权的,存储已认证用户具备的访问权限。

        通过 Authentication.getAuthorities() 方法可以获取 GrantedAuthority 的实例。此方法提供了 GrantedAuthority 对象的集合。毫无疑问,GrantedAuthority 是授予用户的权限。这些权限通常是“角色”信息,例如 ROLE_ADMINISTRATOR ROLE_HR_SUPERVISOR。这些角色将配置给 Web 授权、方法授权和域对象授权使用。当使用基于用户名/密码的身份验证时,GrantedAuthority 实例通常由 UserDetailsService 加载。

        通常,GrantedAuthority 对象是应用程序范围的权限。它们并不特定于给定的域对象。因此,不太可能使用 GrantedAuthority 来表示一个对象的具体权限,因为如果有数千个这样的权限,将很快耗尽内存(或者,会导致应用程序花费很长时间来进行验证)。// 其意思是说,GrantedAuthority 中的权限是一类权限(角色),而不是某个具体的功能,比如管理员权限能操作所有模块,而所有模块的功能汇聚在一起可能有好几百个。

5、AuthenticationManager 身份认证管理器接口

        AuthenticationManager 定义 Spring Security 的过滤器如何执行身份验证的 API。用于处理身份验证请求

public interface AuthenticationManager {//尝试对传递的身份验证对象进行身份验证,//如果成功,则返回一个完全填充的身份验证对象(包括授予的权限)。Authentication authenticate(Authentication var1) throws AuthenticationException;
}

        Spring Security 的 Filters 实例会调用 AuthenticationManager 获取在 SecurityContextHolder上设置的 Authentication 对象进行身份验证。

        如果直接设置 SecurityContextHolder,就不需要使用 AuthenticationManager。//直接填充身份信息,跳过认证步骤,由此可以见所有的认证流程也只是去SecurityContextHolder中设置一个Authentication而已

        AuthenticationManager 可以有很多种实现,其中最常见的实现是 ProviderManager

6、ProviderManager 身份认证管理器的实现

        ProviderManager 是 AuthenticationManager 最常用的实现。ProviderManager 对用户的认证委托给一个 AuthenticationProvider 实例的列表。

//具体进行认证操作的实例列表
private List<AuthenticationProvider> providers;

        每个 AuthenticationProvider 都有机会表明身份验证应该是成功的、失败的,或者表明它不能做出决定,并允许下游的 AuthenticationProvider 做出决定。

        如果配置的 AuthenticationProvider 实例都不能进行身份验证,则身份验证失败并产生一个ProviderNotFoundException,这是一个特殊的 AuthenticationException,表明 ProviderManager 没有配置支持此 Authentication 对象认证的 Provider 类型。

        实际中,每个 AuthenticationProvider 都会去执行一个特定类型的身份验证。例如,一个AuthenticationProvider 可能能够验证用户名/密码,而另一个 AuthenticationProvider 可能能够验证 SAML 断言。Spring Security 同时支持多种身份验证类型,并且只公开一个通用的 AuthenticationManager Bean。//对应各种功能的过滤器

        ProviderManager 还允许配置一个父类 AuthenticationManager(可选),它在没有AuthenticationProvider 可以执行身份验证的情况下被使用。父类可以是任何类型的 AuthenticationManager,通常也是一个 ProviderManager 实例。

        多个 ProviderManager 实例可以共享同一个父 AuthenticationManager。该方式在存在多个 SecurityFilterChain 的场景中比较常见,这些安全过滤器链有一些共同的身份验证(共享的父AuthenticationManager),但也有不同的身份验证机制(不同的 ProviderManager 实例)。

        默认情况下,ProviderManager 会尝试从成功的身份验证请求返回的 Authentication 对象中清除敏感的凭据信息,防止信息(如密码)在 HttpSession 中保留的时间超过所需的时间。// Authentication 认证成功后,ProviderManager 会清除其中的认证凭据

        // 因为ProviderManager会清除Authentication认证凭据,所以缓存Authentication信息时应该注意此问题。

        解决缓存问题的两种方案:创建 Authentication 对象的副本或者禁用 ProviderManager 上的 eraseCredentialsAfterAuthentication 属性。

        【ProviderManager.authenticate 源码分析】:

//执行认证的AuthenticationProvider列表
private List<AuthenticationProvider> providers;  //认证流程
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;int currentPosition = 0;int size = this.providers.size();//1-获取Provider列表迭代器Iterator var9 = this.getProviders().iterator(); //2-开始循环遍历Provider列表while(var9.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var9.next();if (provider.supports(toTest)) {//省略...try {//3-使用其中一个provider进行身份验证result = provider.authenticate(authentication);if (result != null) {this.copyDetails(authentication, result);break;}} catch (InternalAuthenticationServiceException | AccountStatusException var14) {this.prepareException(var14, authentication);throw var14;} catch (AuthenticationException var15) {lastException = var15;}}}//省略...(这里省略了付实例的认证,也很简单,可查看源码)//4-如果认证成功,在这里擦除密码/认证凭据等信息if (result != null) { if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {((CredentialsContainer)result).eraseCredentials(); //擦除认证凭据}if (parentResult == null) {this.eventPublisher.publishAuthenticationSuccess(result);}return result;} else {//5-抛出providerNotFound异常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;}}

7、AuthenticationProvider 特定类型的身份验证接口

        用于处理特定身份验证的统一接口。

        通常可以将多个 AuthenticationProvider 实例注入到 ProviderManager 中。每个AuthenticationProvider 执行特定类型的身份验证。例如,DaoAuthenticationProvider 支持基于用户名/密码的身份验证,而 JwtAuthenticationProvider 支持验证 JWT 令牌。

public interface AuthenticationProvider {//执行身份验证Authentication authenticate(Authentication var1) throws AuthenticationException;//如果此AuthenticationProvider支持指定的Authentication对象,则返回true。boolean supports(Class<?> var1);
}

8、AuthenticationEntryPoint 身份验证方案接口

        AuthenticationEntryPoint 作用:由 ExceptionTranslationFilter 用于启动一个身份验证方案

public interface AuthenticationEntryPoint {//启动认证方案void commence(HttpServletRequest var1, HttpServletResponse var2, AuthenticationException var3) throws IOException, ServletException;
}

        ExceptionTranslationFilter 将填充 AbstractAuthenticationProcessingFilter HttpSession 属性。在调用此方法之前,使用所请求的目标URL

        实现根据需要修改 ServletResponse 上的报文头,以启动身份验证过程。AuthenticationEntryPoint 的实现可能会执行重定向到登录页面、使用 WWW-Authenticate 头响应或采取其他操作。

9、AbstractAuthenticationProcessingFilter 用户凭据验证过滤器

        AbstractAuthenticationProcessingFilter 用作验证用户凭据的基本过滤器。在对凭证进行身份验证之前,Spring Security 通常会使用 AuthenticationEntryPoint 来请求凭证。

        【AbstractAuthenticationProcessingFilter.doFilter 源码】//总流程

    //身份认证过滤器的处理流程private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {if (!this.requiresAuthentication(request, response)) {chain.doFilter(request, response);} else {try {//1- 尝试进行身份验证Authentication authenticationResult = this.attemptAuthentication(request, response);if (authenticationResult == null) {return;}//2-如果验证成功,创建新会话,更新sessionIdthis.sessionStrategy.onAuthentication(authenticationResult, request, response);if (this.continueChainBeforeSuccessfulAuthentication) {chain.doFilter(request, response);}//3-调用认证成功后处理流程this.successfulAuthentication(request, response, chain, authenticationResult);} catch (InternalAuthenticationServiceException var5) {this.logger.error("An internal error occurred while trying to authenticate the user.", var5);//4-调用认证失败后处理流程this.unsuccessfulAuthentication(request, response, var5);} catch (AuthenticationException var6) {//4-调用认证失败后处理流程this.unsuccessfulAuthentication(request, response, var6);}}}

        接下来,AbstractAuthenticationProcessingFilter 可以对提交给它的任何身份验证请求进行身份验证

    //过滤器中尝试进行身份验证的抽象方法public abstract Authentication attemptAuthentication(HttpServletRequest var1, HttpServletResponse var2) throws AuthenticationException, IOException, ServletException;//一个具体实现类:UsernamePasswordAuthenticationFilter的实现逻辑public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {if (this.postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());} else {String username = this.obtainUsername(request);username = username != null ? username : "";username = username.trim();String password = this.obtainPassword(request);password = password != null ? password : "";//使用用户名和密码创建一个Authentication对象UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);this.setDetails(request, authRequest);//调用特定的认证管理器进行认证,通常为(ProviderManager)return this.getAuthenticationManager().authenticate(authRequest);}}

        AbstractAuthenticationProcessingFilter 进行认证的流程图示:

        当用户提交凭据时,AbstractAuthenticationProcessingFilter HttpServletRequest 创建一个要进行身份验证的 Authentication 对象。创建的身份验证类型取决于AbstractAuthenticationProcessingFilter 的子类。例如,UsernamePasswordAuthenticationFilter HttpServletRequest 中提交的用户名和密码中创建 UsernamePasswordAuthenticationToken//看上边的源码展示

        接下来,将 Authentication 对象 传递到 AuthenticationManager 中进行身份验证。

        如果身份验证失败,则执行 Failure 流程://失败流程,有许多的拓展点

        (1)调用 SecurityContextHolder.clearContext 方法清除 Security 上下文。//ThreadLocal.remove

        (2)调用 RememberMeServices.loginFail 方法。有两个实现类 NullRememberMe 和 RememberMe。 //RememberMe会失效Cookie

        (3)调用 AuthenticationFailureHandler.onAuthenticationFailure 方法。//抛出错误信息或者重定向

        AbstractAuthenticationProcessingFilter.unsuccessfulAuthentication 源码】

    //认证失败处理流程protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {//1-清除SecurityContextHolderSecurityContextHolder.clearContext();this.logger.trace("Failed to process authentication request", failed);this.logger.trace("Cleared SecurityContextHolder");this.logger.trace("Handling authentication failure");//2-调用RememberMeServices.loginFailthis.rememberMeServices.loginFail(request, response);//3-调用AuthenticationFailureHandlerthis.failureHandler.onAuthenticationFailure(request, response, failed);}

        如果身份验证成功,则执行 Success 流程:

        (1)SessionAuthenticationStrategy.onAuthentication 会收到新的登录通知。

        (2)在 SecurityContextHolder 中设置通过认证的用户信息,方便后续取用。

        (3)调用 RememberMeServices.loginSuccess 方法。//通过此方法可自定义成功后的处理方式。

        (4)调用 ApplicationEventPublisher.publishEvent 方法发布一个InteractiveAuthenticationSuccessEvent 事件。//用户可通过监听此事件来做进一步操作

        (5)调用 AuthenticationSuccessHandler.onAuthenticationSuccess 方法。//处理url重定向等

        【AbstractAuthenticationProcessingFilter.successfulAuthentication 源码】

    //认证成功处理流程protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {//1-在 SecurityContextHolder 中设置通过认证的用户信息,方便后续取用SecurityContextHolder.getContext().setAuthentication(authResult);if (this.logger.isDebugEnabled()) {this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));}//2-调用 RememberMeServices.loginSuccess 方法this.rememberMeServices.loginSuccess(request, response, authResult);if (this.eventPublisher != null) {//3-发布认证成功事件this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));}//4-调用AuthenticationSuccessHandler.onAuthenticationSuccess方法this.successHandler.onAuthenticationSuccess(request, response, authResult);}

        最后,熟知用户的认证流程,对熟练使用 Spring Security 非常重要。需要多学习,多总结。

        至此,全文到此结束。

http://www.lryc.cn/news/98840.html

相关文章:

  • 市值超300亿美金,SaaS独角兽Veeva如何讲好中国故事?
  • 编译内联导致内存泄漏的问题定位修复
  • 基于WebSocket实现的后台服务
  • Go语言中的结构体详解
  • pytest自动化测试指定执行测试用例
  • 英伟达 H100 vs. 苹果M2,大模型训练,哪款性价比更高?
  • var、let和const的区别
  • (css)AI智能问答页面布局
  • 【Pytorch学习】pytorch中的isinstance() 函数
  • (树) 剑指 Offer 07. 重建二叉树 ——【Leetcode每日一题】
  • Gitlab 合并分支与请求合并
  • 【Matter】基于Ubuntu 22.04 编译chip-tool工具
  • 将 MongoDB 的 List<Document> 转换为对象列表
  • 【Linux下6818开发板(ARM)】SecureCRT串口和交叉编译工具(巨细版!)
  • 应届生如何快速找Java开发工程师,先学会这17个基础问题
  • 数学建模学习(5):数学建模各类题型及解题方案
  • 【学习笔记】视频检测方法调研
  • idea terminal npm指令无效
  • 低代码开发平台源码
  • 【UE5 多人联机教程】04-加入游戏
  • 自然语言处理从入门到应用——LangChain:模型(Models)-[大型语言模型(LLMs):缓存LLM的调用结果]
  • Python 算法基础篇之图的遍历算法:深度优先搜索和广度优先搜索
  • 文本缩略 文本超出显示省略号 控制超出省略的行数
  • 云原生架构
  • Java 生成随机数据
  • 基于OpenCV的红绿灯识别
  • JavaScript快速入门:ComPDFKit PDF SDK 快速构建 Web端 PDF阅读器
  • Flutter 网络请求
  • 吃透《西瓜书》第三章 线性模型:多元线性回归
  • 数据结构【排序】