目录
- 【8】Spring Boot 3 集成组件:安全组件 spring security
- Spring Security 简介
- 先决条件
- 引入依赖
- 身份验证
- 密码存储
- 密码存储历史
- DelegatingPasswordEncoder
- 密码存储格式
- 密码加解密类
- 自定义密码存储
- 体系结构 Architecture
- Servlet 过滤器
- DelegatingFilterProxy
- FilterChainProxy
- SecurityFilterChain
- Security Filter
- 添加自定义 Filter 到 Filter Chain
- 处理 Security 异常
- 保存认证之间的请求
- RequestCache
- 防止请求被保存
- 认证
- SecurityContextHolder
- SecurityContext
- Authentication
- AuthenticationManager
- ProviderManager
- AuthenticationProvider
- 用 AuthenticationEntryPoint 请求凭证
- :a: AbstractAuthenticationProcessingFilter
- UsernamePasswordAuthenticationFilter
- :b:DaoAuthenticationProvider
- UserDetailsService
- UserDetails
- PasswordEncoder
- 处理登出 Logout
- 授权
个人主页: 【⭐️个人主页】
需要您的【💖 点赞+关注】支持 💯
【8】Spring Boot 3 集成组件:安全组件 spring security
📖 本文核心知识点:
- spring security
- B
官网Doc : Spring Security
Spring Security 简介
Spring Security
是一个框架,它提供身份验证
、授权
和针对常见攻击的保护
。它具有保护命令式和响应式应用程序的一流支持,是保护基于spring的应用程序的事实上的标准。
先决条件
Spring Security
需要Java 8
或更高版本
的运行时环境
。
由于Spring Security旨在以自包含的方式
进行操作,因此不需要在Java运行时环境中放置任何特殊的配置文件。特别是,您不需要配置特殊的Java身份验证和授权服务(JAAS)策略文件,也不需要将Spring Security放入公共类路径位置。
引入依赖
Maven
<dependencies><!-- ... other dependency elements ... --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
</dependencies>
Gradle
implementation "org.springframework.boot:spring-boot-starter-security"
身份验证
密码存储
Spring Security的PasswordEncoder接口用于执行密码的单向转换,以便安全地存储密码。假设PasswordEncoder是单向转换,那么当密码转换需要是双向的(例如存储用于向数据库进行身份验证的凭据)时,它就没有用了。通常,PasswordEncoder用于存储需要在身份验证时与用户提供的密码进行比较的密码。
密码存储历史
-
密码以明文形式存储
多年来,存储密码的标准机制已经发生了变化。一开始,密码以明文形式存储。密码被认为是安全的,因为数据存储将密码保存在访问它所需的凭据中。然而,恶意用户能够通过使用SQL注入等攻击找到大量用户名和密码的“数据转储”。随着越来越多的用户凭证公开,安全专家意识到我们需要采取更多措施来保护用户的密码。
-
单向散列(例如SHA-256)运行密码后存储密码
当用户尝试进行身份验证时,将把散列密码与他们键入的密码的散列进行比较。这意味着系统只需要存储密码的单向散列。如果发生泄露,则只暴露密码的单向散列。由于哈希值是单向的,并且在计算上很难猜测给定哈希值的密码,因此不值得花费精力去找出系统中的每个密码。为了破解这个新系统,恶意用户决定创建名为“
彩虹表
”的查找表。他们不是每次都猜测每个密码,而是计算一次密码并将其存储在查找表中。 -
为了降低
彩虹表
的有效性,开发者被鼓励使用盐渍密码
。不是只使用密码作为哈希函数的输入,而是为每个用户的密码生成随机字节(称为salt)。盐和用户的密码将通过散列函数运行,以产生唯一的散列。盐将以明文形式与用户密码一起存储。然后,当用户尝试进行身份验证时,将把哈希后的密码与存储盐的哈希值和用户键入的密码进行比较。独特的盐意味着彩虹表不再有效,因为每个盐和密码组合的哈希值都不同。
在现代,我们意识到
加密散列(如SHA-256)不再安全
。原因在于,使用现代硬件,我们每秒可以执行数十亿次哈希计算。这意味着我们可以轻松地单独破解每个密码。 -
现在鼓励开发人员利用
自适应单向函数来存储密码
。使用自适应单向函数对密码进行验证是有意的资源密集型操作(它们有意使用大量CPU、内存或其他资源)。一个自适应单向功能允许配置一个“工作因素”,可以随着硬件变得更好而增长。我们建议将“工作因子”调整为大约一秒钟来验证系统上的密码。这种权衡是为了使攻击者难以破解密码,但代价不会太高,不会给您自己的系统带来过多的负担或激怒用户。Spring Security试图为“工作因素”提供一个良好的起点,但是我们鼓励用户为他们自己的系统定制“工作因素”,因为不同系统的性能差异很大。
应该使用的自适应单向函数的示例包括bcrypt、PBKDF2、scrypt和argon2
。由于自适应单向函数故意占用大量资源,因此为每个请求验证用户名和密码会显著降低应用程序的性能。Spring Security(或任何其他库)无法加快密码的验证速度,因为安全性是通过使验证资源密集来获得的
-
鼓励用户将长期凭证(即用户名和密码)交换为短期凭证(如会话和OAuth令牌等
)。短期凭证
可以快速验证
而不会损失任何安全性。
DelegatingPasswordEncoder
在Spring Security 5.0之前
,默认的PasswordEncoder
是NoOpPasswordEncoder
,它需要明文密码
。根据Password History部分,您可能期望默认的PasswordEncoder现在是类似于BCryptPasswordEncoder
的东西。然而,这忽略了现实世界中的三个问题:
-
许多应用程序使用不容易迁移的旧密码编码。
-
密码存储的最佳实践将再次更改。
-
作为一个框架,Spring Security不能频繁地进行破坏性更改。
相反,Spring Security引入了DelegatingPasswordEncoder
,它通过以下方式解决了所有问题:
-
确保使用当前密码存储建议对密码进行编码
-
允许以现代和遗留格式验证密码
-
允许将来升级编码
你可以使用PasswordEncoderFactories
很容易地构造一个DelegatingPasswordEncoder
的实例:
PasswordEncoder passwordEncoder =PasswordEncoderFactories.createDelegatingPasswordEncoder();
创建自定义DelegatingPasswordEncoder
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("sha256", new StandardPasswordEncoder());PasswordEncoder passwordEncoder =new DelegatingPasswordEncoder(idForEncode, encoders);
密码存储格式
密码的一般格式为:
DelegatingPasswordEncoder
存储格式
{id}encodedPassword
# 例子
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
id
是一个标识符,用于查找应该使用哪个PasswordEncoder
, encodedPassword是所选PasswordEncoder的原始编码密码。id
必须位于密码的开头,以{开始,以}
结束。如果找不到id,则将id设置为空。例如,下面可能是使用不同id值编码的密码列表。所有的原始密码都是密码。
密码加解密类
- BCryptPasswordEncoder
- Argon2PasswordEncoder
- Pbkdf2PasswordEncoder
- SCryptPasswordEncoder
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
自定义密码存储
Spring Security默认使用DelegatingPasswordEncoder
。但是,您可以通过将PasswordEncoder
公开为Spring bean
来定制它。
如果您正在从Spring Security 4.2迁移。通过公开NoOpPasswordEncoder bean,您可以恢复到以前的行为。
@Bean
public static NoOpPasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();
}
体系结构 Architecture
本节讨论基于Servlet的应用程序中的Spring Security高级体系结构。我们将在参考文献的身份验证、授权和防止漏洞利用保护部分中建立这种高层次的理解。
Servlet 过滤器
Spring Security 的 Servlet 支持基于 Servlet 过滤器
,因此首先了解过滤器的作用会很有帮助。 下图显示了单个 HTTP 请求的处理程序的典型分层。
Figure 1. FilterChain
客户端向应用程序发送一个请求,容器创建一个 FilterChain
,其中包含 Filter
实例和 Servlet
,应该根据请求URI的路径来处理 HttpServletRequest
。在Spring MVC应用程序中,Servlet
是 DispatcherServlet
的一个实例。一个 Servlet
最多可以处理一个 HttpServletRequest
和 HttpServletResponse
。然而,可以使用多个 Filter
来完成如下工作。
防止下游的 Filter 实例或 Servlet 被调用。在这种情况下,Filter 通常会使用 HttpServletResponse 对客户端写入响应。
修改下游的 Filter 实例和 Servlet 所使用的 HttpServletRequest 或 HttpServletResponse。
过滤器的执行顺序来自于传入它的 FilterChain。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {// do something before the rest of the applicationchain.doFilter(request, response); // invoke the rest of the application// do something after the rest of the application
}
由于一个 Filter
只影响下游的 Filter 实例
和 Servlet
,所以每个 Filter 的调用顺序
是非常重要的。
完整调用链类图
DelegatingFilterProxy
Spring 提供了一个名为 DelegatingFilterProxy
的 Filter 实现
,允许在 Servlet 容器的生命周期
和 Spring 的 ApplicationContext 之间建立桥梁
。Servlet容器允许通过使用自己的标准来注册 Filter 实例,但它不知道 Spring 定义的 Bean。你可以通过标准的Servlet容器机制来注册 DelegatingFilterProxy,但将所有工作委托给实现 Filter 的Spring Bean
。
DelegatingFilterProxy
从 ApplicationContext
查找 Bean Filter0
,然后调用 Bean Filter0
。下面的列表显示了 DelegatingFilterProxy 的伪代码。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {Filter delegate = getFilterBean(someBeanName); delegate.doFilter(request, response);
}
- 延迟地获取被注册为Spring Bean的 Filter。 对于 DelegatingFilterProxy 中的例子,delegate 是 Bean Filter0 的一个实例。
- 将工作委托给 Spring Bean。
🅰️ DelegatingFilterProxy
的另一个好处
是,它允许延迟查找 Filter Bean实例。
这一点很重要,因为在容器启动之前,容器需要注册 Filter 实例。然而, Spring 通常使用 ContextLoaderListener 来加载 Spring Bean,这在需要注册 Filter 实例之后才会完成。
FilterChainProxy
Spring Security 的 Servlet 支持包含在 FilterChainProxy
中。FilterChainProxy 是 Spring Security 提供的一个特殊的 Filter,允许通过 SecurityFilterChain
委托给许多 Filter 实例
。由于 FilterChainProxy
是一个Bean,它通常被包裹在 DelegatingFilterProxy
中。
FilterChainProxy 有很多优势。首先,它为 Spring Security 的所有 Servlet 支持提供了一个起点。由于这个原因,如果你试图对 Spring Security 的 Servlet 支持进行故障诊断,在 FilterChainProxy 中添加一个调试点是一个很好的开始。
其次,由于 FilterChainProxy 是 Spring Security 使用的核心,它可以执行一些不被视为可有可无的任务。 例如,它清除了 SecurityContext 以避免内存泄漏。它还应用Spring Security的 HttpFirewall 来保护应用程序免受某些类型的攻击。
此外,它在确定何时应该调用 SecurityFilterChain 方面提供了更大的灵活性。在Servlet容器中,Filter 实例仅基于URL被调用。 然而,FilterChainProxy 可以通过使用 RequestMatcher 接口,根据 HttpServletRequest 中的任何内容确定调用。
SecurityFilterChain
SecurityFilterChain 被 FilterChainProxy 用来确定当前请求应该调用哪些 Spring Security Filter 实例。
在 Multiple SecurityFilterChain 图中, FilterChainProxy 决定应该使用哪个 SecurityFilterChain。只有第一个匹配的 SecurityFilterChain 被调用。如果请求的URL是 /api/messages/,它首先与 /api/** 的 SecurityFilterChain0 模式匹配,所以只有 SecurityFilterChain0 被调用,尽管它也与 SecurityFilterChainn 匹配。如果请求的URL是 /messages/,它与 /api/** 的 SecurityFilterChain0 模式不匹配,所以 FilterChainProxy 继续尝试每个 SecurityFilterChain。假设没有其他 SecurityFilterChain 实例相匹配,则调用 SecurityFilterChainn。
请注意,SecurityFilterChain0 只配置了三个 security Filter 实例。然而,SecurityFilterChainn 却配置了四个 security Filter 实例。值得注意的是,每个 SecurityFilterChain 都可以是唯一的,并且可以单独配置。事实上,如果应用程序希望 Spring Security 忽略某些请求,那么一个 SecurityFilterChain 可能会有零个 security Filter 实例。
Security Filter
Security Filter 是通过 SecurityFilterChain API 插入 FilterChainProxy 中的。
这些 filter 可以用于许多不同的目的,如 认证、 授权、 漏洞保护
等等。filter 是按照特定的顺序执行的,以保证它们在正确的时间被调用,例如,执行认证的 Filter 应该在执行授权的 Filter 之前被调用。一般来说,没有必要知道 Spring Security 的 Filter 的顺序。但是,有些时候知道顺序是有好处的,如果你想知道它们,可以查看 FilterOrderRegistration 代码。
打印出 Security Filter
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]
添加自定义 Filter 到 Filter Chain
- 定义一个Filter
集成
Filter
,orOncePerRequestFilter
- 添加到 SecurityFilterChain
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http// ....addFilterBefore(new TenantFilter(), AuthorizationFilter.class); return http.build();
}
🅰️ 注意点: 多次调用Filter问题
当你把你的 filter 声明为 Spring Bean 时要小心,可以用 @Component 注解它,也可以在配置中把它声明为 Bean,因为 Spring Boot 会自动 在嵌入式容器中注册它。这可能会导致 filter 被调用两次,一次由容器调用,一次由 Spring Security 调用,而且顺序不同。
如果你仍然想把你的 filter 声明为 Spring Bean,以利用依赖注入,避免重复调用,你可以通过声明 FilterRegistrationBean Bean 并将其 enabled 属性设置为 false 来告诉 Spring Boot 不要向容器注册它:
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);registration.setEnabled(false);return registration;
}
处理 Security 异常
-
首先,ExceptionTranslationFilter 调用 FilterChain.doFilter(request, response) 来调用应用程序的其他部分。
-
如果用户没有被认证,或者是一个 AuthenticationException,那么就 开始认证。
-
SecurityContextHolder 被清理掉。
-
HttpServletRequest 被保存起来,这样一旦认证成功,它就可以用来重放原始请求。
-
AuthenticationEntryPoint 用于请求客户的凭证。例如,它可以重定向到一个登录页面或发送一个 WWW-Authenticate 头。
-
-
否则,如果是 AccessDeniedException,那么就是 Access Denied。 AccessDeniedHandler 被调用来处理拒绝访问(access denied)。
🅰️ 注意点:
如果应用程序没有抛出
AccessDeniedException
或AuthenticationException
,那么ExceptionTranslationFilter
就不会做任何事情
保存认证之间的请求
如在 处理 Security 异常
中所说明的,当一个请求没有认证,并且是针对需要认证的资源时,有必要保存认证资源的请求,以便在认证成功后重新请求。在Spring Security中,这是通过使用 RequestCache
实现来保存HttpServletRequest
的。
RequestCache
HttpServletRequest 被保存在 RequestCache。当用户成功认证后,RequestCache 被用来重放原始请求。RequestCacheAwareFilter 就是使用 RequestCache 来保存 HttpServletRequest 的。
默认情况
下,使用一个 HttpSessionRequestCache
。下面的代码演示了如何定制 RequestCache 的实现,如果名为 continue 的参数存在,它将用于检查 HttpSession 是否有保存的请求。
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {HttpSessionRequestCache requestCache = new HttpSessionRequestCache();requestCache.setMatchingRequestParameterName("continue");http// ....requestCache((cache) -> cache.requestCache(requestCache));return http.build();
}
防止请求被保存
有很多原因,你可能想不在 session 中存储用户的未经认证的请求。你可能想把这种存储卸载到用户的浏览器上,或者把它存储在数据库中。或者你可能想关闭这个功能,因为你总是想把用户重定向到主页,而不是他们登录前试图访问的页面。
要做到这一点,你可以使用 NullRequestCache
实现.
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {RequestCache nullRequestCache = new NullRequestCache();http// ....requestCache((cache) -> cache.requestCache(nullRequestCache));return http.build();
}
认证
Servlet 认证架构
- SecurityContextHolder -
SecurityContextHolder
是 Spring Security 存储 认证 用户细节的地方。 - SecurityContext - 是从
SecurityContextHolder
获得的,包含了当前认证用户的Authentication
(认证)。 - Authentication - 可以是
AuthenticationManager
的输入,以提供用户提供的认证凭证或来自SecurityContext
的当前用户。 - GrantedAuthority - 在
Authentication
(认证)中授予委托人的一种权限(即role、scope等)。 - AuthenticationManager - 定义 Spring Security 的 Filter 如何执行 认证 的API。
- ProviderManager - 最常见的
AuthenticationManager
的实现。 - AuthenticationProvider - 由
ProviderManager
用于执行特定类型的认证。 - 用
AuthenticationEntryPoint
请求凭证 - 用于从客户端请求凭证(即重定向到登录页面,发送WWW-Authenticate
响应,等等)。 - AbstractAuthenticationProcessingFilter - 一个用于认证的基本
Filter
。这也让我们很好地了解了认证的高层流程以及各部分是如何协作的。
认证官网学习:https://springdoc.cn/spring-security/servlet/authentication/architecture.html
SecurityContextHolder
SecurityContextHolder 是 Spring Security 存储用户 验证 细节的地方。Spring Security 并不关心 SecurityContextHolder 是如何被填充的。如果它包含一个值,它就被用作当前认证的用户。
默认情况下,SecurityContextHolder 使用 ThreadLocal 来存储这些细节,这意味着 SecurityContext 对同一线程中的方法总是可用的,即使 SecurityContext 没有被明确地作为参数传递给这些方法。如果你注意在处理完当前委托人的请求后清除该线程,以这种方式使用 ThreadLocal 是相当安全的。Spring Security 的 FilterChainProxy 确保 SecurityContext 总是被清空。
SecurityContext
SecurityContext 是从 SecurityContextHolder 中获得的。SecurityContext 包含一个 Authentication 对象。
Authentication
认证是一个用户在当前系统下,存在其身份后的 身份凭证,一般是 该身份的 名称和权限信息填充
认证(Authentication)包含了:
-
principal: 识别用户。当用用户名/密码进行认证时,这通常是 UserDetails 的一个实例。
-
credentials: 通常是一个密码。在许多情况下,这在用户被认证后被清除,以确保它不会被泄露。
-
authorities: GrantedAuthority 实例是用户被授予的高级权限。两个例子是角色(role)和作用域(scope)。
AuthenticationManager
这是一个接口定义。其具体实现·ProviderManager·提供。
AuthenticationManager 是定义 Spring Security 的 Filter 如何执行 认证 的API。返回的 认证是由调用 AuthenticationManager 的控制器(即 Spring Security的 Filter 实例)在 SecurityContextHolder 上设置的。如果你不与 Spring Security 的 Filter 实例集成,你可以直接设置 SecurityContextHolder,不需要使用 AuthenticationManager。
虽然 AuthenticationManager 的实现可以是任何东西,但最常见的实现是ProviderManager。
ProviderManager
-
ProviderManager是最常用的AuthenticationManager的实现。
-
ProviderManager
委托
给一个ListAuthenticationProvider
实例。每个 AuthenticationProvider 都有机会表明认证应该是成功的、失败的,或者表明它不能做出决定并允许下游的 AuthenticationProvider 来决定。如果配置的 AuthenticationProvider 实例中没有一个能进行认证,那么认证就会以
ProviderNotFoundException
而失败,这是一个特殊的AuthenticationException
,表明ProviderManager
没有被配置为支持被传入它的 Authentication 类型。在实践中,每个 AuthenticationProvider 都知道如何执行特定类型的认证。例如,一个 AuthenticationProvider 可能能够验证一个用户名/密码,而另一个可能能够验证一个 SAML 断言。这让每个 AuthenticationProvider 在支持多种类型的认证的同时,可以做一种非常具体的认证类型,并且只暴露一个 AuthenticationManager Bean。
ProviderManager 还允许配置一个可选的父级 AuthenticationManager,在没有 AuthenticationProvider 可以执行认证的情况下,可以参考它。父级可以是任何类型的 AuthenticationManager,但它通常是 ProviderManager 的一个实例。
事实上,多个 ProviderManager 实例可能共享同一个父级 AuthenticationManager。这在有多个 SecurityFilterChain 实例的场景中有些常见,这些实例有一些共同的认证(共享的父 AuthenticationManager),但也有不同的认证机制(不同的 ProviderManager 实例)。
默认情况下,ProviderManager 会尝试从 Authentication 对象中清除任何敏感的凭证信息,该对象由成功的认证请求返回。这可以防止密码等信息在 HttpSession 中保留超过必要的时间。
当你使用用户对象的缓存时,这可能会导致问题,例如,在一个无状态的应用程序中提高性能。如果 Authentication 包含对缓存中的一个对象的引用(比如 UserDetails 实例),而这个对象的凭证已经被删除,那么就不可能再针对缓存的值进行认证。如果你使用一个缓存,你需要考虑到这一点。一个明显的解决方案是,首先在缓存实现中或在创建返回的 Authentication 对象的 AuthenticationProvider 中制作一个对象的副本。另外,你可以禁用 ProviderManager 上的 eraseCredentialsAfterAuthentication 属性。参见 ProviderManager 类的Javadoc。
ProviderManager 也不是具体执行认证的类。ProviderManager 将具体认证职责,委托给内部的 ListAuthenticationProvider 列表遍历。
并且保证,只有前一个认证通过即可,后一个认证不通过,下一个继续匹配支持的认证进行认证操作
AuthenticationProvider
你可以在 ProviderManager 中注入多个 AuthenticationProvider 实例。每个 AuthenticationProvider 都执行一种特定类型的认证。例如, DaoAuthenticationProvider
支持基于用户名/密码的认证,而 JwtAuthenticationProvider
支持认证JWT令牌。
用 AuthenticationEntryPoint 请求凭证
AuthenticationEntryPoint 用于发送一个要求客户端提供凭证的HTTP响应。
简而言之,就是告诉 客户端,去进行登录操作
有时,客户端会主动包含凭证(如用户名和密码)来请求资源。在这些情况下,Spring Security 不需要提供要求客户端提供凭证的HTTP响应,因为这些凭证已经被包括在内。
在其他情况下,客户端向他们未被授权访问的资源发出未经认证的请求。在这种情况下, AuthenticationEntryPoint 的实现被用来请求客户端的凭证。 AuthenticationEntryPoint 的实现可能会执行 重定向到一个登录页面,用 WWW-Authenticate 头来响应,或采取其他行动。
🅰️ AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter 被用作验证用户凭证的基础 Filter。在认证凭证之前,Spring Security 通常通过使用AuthenticationEntryPoint 来请求凭证。
AbstractAuthenticationProcessingFilter 可以对提交给它的任何认证请求进行认证。
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter 是 AbstractAuthenticationProcessingFilter的实现类。
所以流程与 AbstractAuthenticationProcessingFilter的流程一致
🅱️DaoAuthenticationProvider
承接上节 AuthenticationManager
认证流程
读取用户名和密码部分的认证 Filter
将 UsernamePasswordAuthenticationToken
传递给 AuthenticationManager
,它由 ProviderManager
实现。
ProviderManager
被配置为使用一个 DaoAuthenticationProvider
类型的 AuthenticationProvider。
DaoAuthenticationProvider
从 UserDetailsService
中查找 UserDetails
。
DaoAuthenticationProvider
使用 PasswordEncoder
来验证上一步返回的 UserDetails
上的密码。
当认证成功时,返回的 Authentication
是 UsernamePasswordAuthenticationToken
类型,并且有一个委托人(principal)是由配置的 UserDetailsService
返回的 UserDetails
。最终,返回的 UsernamePasswordAuthenticationToken
被认证 Filter 设置在 SecurityContextHolder
上。
UserDetailsService
UserDetailsService 被 DaoAuthenticationProvider 用来检索用户名、密码和其他属性,以便用用户名和密码进行认证。Spring Security提供了 UserDetailsService 的 内存 和 JDBC 实现。
你可以通过暴露一个自定义的 UserDetailsService 作为一个bean来定义自定义认证。例如,假设 CustomUserDetailsService 实现了 UserDetailsService,那么下面的列表将自定义认证。
@Bean
CustomUserDetailsService customUserDetailsService() {return new CustomUserDetailsService();
}
UserDetails
UserDetails 由 UserDetailsService 返回。DaoAuthenticationProvider 验证 UserDetails,然后返回一个 Authentication,该 Authentication 的委托人(principal)是由配置的 UserDetailsService 返回的。
PasswordEncoder
Spring Security的servlet支持包括通过与 PasswordEncoder 集成来安全地存储密码。你可以通过 暴露一个 PasswordEncoder Bean 来定制Spring Security使用的 PasswordEncoder 实现。
处理登出 Logout
当你包含 spring-boot-starter-security 依赖或使用 @EnableWebSecurity 注解时,Spring Security 将添加其注销支持,并默认响应 GET /logout 和 POST /logout。
如果你请求 GET /logout,那么 Spring Security 会显示一个注销确认页面。除了为用户提供一个有价值的双重检查机制外,它还提供了一个简单的方法来为 POST /logout 提供 所需的 CSRF token。
如果你请求 POST /logout,那么它将使用一系列 LogoutHandler 执行以下默认操作:
-
使 HTTP session无效 (SecurityContextLogoutHandler)。
-
清理 SecurityContextHolderStrategy (SecurityContextLogoutHandler)。
-
清理 SecurityContextRepository (SecurityContextLogoutHandler)。
-
清理任何 RememberMe 认证 (TokenRememberMeServices / PersistentTokenRememberMeServices)。
-
清除任何已保存的 CSRF token (CsrfLogoutHandler)。
-
触发 LogoutSuccessEvent (LogoutSuccessEventPublishingLogoutHandler)
一旦完成,那么它将行使其默认的 LogoutSuccessHandler,它重定向到 /login?logout。
如果你使用Java配置,你可以通过调用 logout DSL 中的 addLogoutHandler 方法来添加自己的清理动作,像这样:
CookieClearingLogoutHandler cookies = new CookieClearingLogoutHandler("our-custom-cookie");
http.logout((logout) -> logout.addLogoutHandler(cookies))
使用 Clear-Site-Data 来注销用户
Clear-Site-Data HTTP header 是浏览器支持的一个指令,用于清除属于自己网站的cookie、storage和缓存。这是一个方便和安全的方法,以确保所有的东西,包括会话cookie,在注销时都被清理掉了。
HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter());
http.logout((logout) -> logout.addLogoutHandler(clearSiteData))
授权
重点查看 架构 章节,讲述了授权的相关类和图解
3.0 后
- AuthorizationManager 同时取代了 AccessDecisionManager 和 AccessDecisionVoter。
https://springdoc.cn/spring-security/servlet/authentication/architecture.html