跳到主要内容

授权 HttpServletRequests

QWen Max 中英对照 Authorize HTTP Requests Authorize HttpServletRequests

Spring Security 允许你在请求级别建模你的授权。例如,使用 Spring Security,你可以规定 /admin 下的所有页面都需要一种权限,而所有其他页面只需要认证即可。

默认情况下,Spring Security 要求每个请求都经过身份验证。也就是说,每当你使用 一个 HttpSecurity 实例 时,都需要声明你的授权规则。

每当您有一个 HttpSecurity 实例时,您至少应该执行以下操作:

http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
java

这告诉 Spring Security,应用程序中的任何端点都至少需要安全上下文通过身份验证才能允许访问。

在许多情况下,你的授权规则会比这更复杂,因此请考虑以下用例:

了解请求授权组件的工作原理

备注

本节在Servlet 架构和实现的基础上,深入探讨基于 Servlet 的应用程序中授权在请求级别是如何工作的。

authorizationfilter

图 1. 授权 HttpServletRequest

AuthorizationFilter 默认是最后执行的

AuthorizationFilter 默认位于 Spring Security 过滤器链 的最后。这意味着 Spring Security 的 认证过滤器漏洞防护 以及其他过滤器集成不需要授权。如果你在 AuthorizationFilter 之前添加了自己的过滤器,这些过滤器也不需要授权;否则,它们将需要授权。

通常这一点变得重要的是当你添加 Spring MVC 端点时。因为它们是由 DispatcherServlet 执行的,而这个过程发生在 AuthorizationFilter 之后,所以你的端点需要包含在 authorizeHttpRequests 中才能被允许访问

所有调度均被授权

AuthorizationFilter 不仅在每个请求上运行,还在每个调度上运行。这意味着 REQUEST 调度需要授权,而且 FORWARDERRORINCLUDE 也需要授权。

例如,Spring MVC 可以 FORWARD 请求到一个视图解析器,该解析器渲染一个 Thymeleaf 模板,如下所示:

@Controller
public class MyController {
@GetMapping("/endpoint")
public String endpoint() {
return "endpoint";
}
}
java

在这种情况下,授权发生两次;一次是授权 /endpoint,一次是转发到 Thymeleaf 以渲染 "endpoint" 模板。

因此,你可能想要允许所有FORWARD分发

这个原则的另一个例子是 Spring Boot 如何处理错误。如果容器捕获了一个异常,比如说像下面这样的:

@Controller
public class MyController {
@GetMapping("/endpoint")
public String endpoint() {
throw new UnsupportedOperationException("unsupported");
}
}
java

然后 Boot 会将其分派到 ERROR 分发。

在这种情况下,授权也会发生两次;一次是针对 /endpoint 的授权,一次是用于派发错误的授权。

因此,你可能想要允许所有 ERROR 派发

Authentication 查找被推迟

这在使用 authorizeHttpRequests 时很重要,当请求总是被允许或总是被拒绝时。在这些情况下,认证不会被查询,从而使请求更快。

授权端点

你可以通过按优先级顺序添加更多规则来配置 Spring Security 以具有不同的规则。

如果你希望 /endpoint 仅对具有 USER 权限的终端用户可访问,那么你可以这样做:

@Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/endpoint").hasAuthority("USER")
.anyRequest().authenticated()
)
// ...

return http.build();
}
java

如你所见,声明可以被拆分成模式/规则对。

AuthorizationFilter 按照列出的顺序处理这些配对,仅将第一个匹配项应用于请求。这意味着即使 /** 也会匹配 /endpoint,但上述规则不会造成问题。解读上述规则的方式是“如果请求是 /endpoint,则要求 USER 权限;否则,仅要求认证”。

Spring Security 支持多种模式和规则;您也可以以编程方式创建自己的模式和规则。

一旦授权,您可以使用Security 的测试支持以如下方式测试:

@WithMockUser(authorities="USER")
@Test
void endpointWhenUserAuthorityThenAuthorized() {
this.mvc.perform(get("/endpoint"))
.andExpect(status().isOk());
}

@WithMockUser
@Test
void endpointWhenNotUserAuthorityThenForbidden() {
this.mvc.perform(get("/endpoint"))
.andExpect(status().isForbidden());
}

@Test
void anyWhenUnauthenticatedThenUnauthorized() {
this.mvc.perform(get("/any"))
.andExpect(status().isUnauthorized());
}
java

匹配请求

上面你已经看到了两种匹配请求的方法

你看到的第一个是最简单的,即匹配任何请求。

第二种是通过URI模式进行匹配。Spring Security支持两种URI模式匹配语言:Ant(如上所示)和正则表达式

使用 Ant 匹配

Ant 是 Spring Security 用于匹配请求的默认语言。

你可以用它来匹配单个端点或目录,甚至可以捕获占位符以供后续使用。你还可以进一步细化以匹配特定的一组HTTP方法。

假设你不是想匹配 /endpoint 端点,而是想匹配 /resource 目录下的所有端点。在这种情况下,你可以这样做:

http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/resource/**").hasAuthority("USER")
.anyRequest().authenticated()
)
java

这句话的解读是“如果请求是 /resource 或其子目录,则需要 USER 权限;否则,只需要认证即可。”

你也可以从请求中提取路径值,如下所示:

http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/resource/{name}").access(new WebExpressionAuthorizationManager("#name == authentication.name"))
.anyRequest().authenticated()
)
java

一旦授权,您可以使用Security 的测试支持以如下方式测试:

@WithMockUser(authorities="USER")
@Test
void endpointWhenUserAuthorityThenAuthorized() {
this.mvc.perform(get("/endpoint/jon"))
.andExpect(status().isOk());
}

@WithMockUser
@Test
void endpointWhenNotUserAuthorityThenForbidden() {
this.mvc.perform(get("/endpoint/jon"))
.andExpect(status().isForbidden());
}

@Test
void anyWhenUnauthenticatedThenUnauthorized() {
this.mvc.perform(get("/any"))
.andExpect(status().isUnauthorized());
}
java
备注

Spring Security 只匹配路径。如果你想要匹配查询参数,你需要一个自定义的请求匹配器。

使用正则表达式匹配

Spring Security 支持将请求与正则表达式进行匹配。如果您希望对子目录应用比 ** 更严格的匹配标准,这会非常有用。

例如,考虑一个包含用户名的路径,并规定所有用户名必须是字母数字。你可以使用 RegexRequestMatcher 来遵守这一规则,如下所示:

http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(RegexRequestMatcher.regexMatcher("/resource/[A-Za-z0-9]+")).hasAuthority("USER")
.anyRequest().denyAll()
)
java

按 Http 方法匹配

您还可以通过 HTTP 方法来匹配规则。当根据授予的权限进行授权时,这一点非常有用,比如被授予 readwrite 权限。

要要求所有的 GET 请求具有 read 权限,所有的 POST 请求具有 write 权限,你可以这样做:

http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(HttpMethod.GET).hasAuthority("read")
.requestMatchers(HttpMethod.POST).hasAuthority("write")
.anyRequest().denyAll()
)
java

这些授权规则应该理解为:“如果请求是 GET,则要求 read 权限;否则,如果请求是 POST,则要求 write 权限;否则,拒绝请求。”

提示

默认情况下拒绝请求是一种健康的的安全实践,因为它将规则集变成了一个允许列表。

一旦授权,你可以通过以下方式使用Security 的测试支持进行测试:

@WithMockUser(authorities="read")
@Test
void getWhenReadAuthorityThenAuthorized() {
this.mvc.perform(get("/any"))
.andExpect(status().isOk());
}

@WithMockUser
@Test
void getWhenNoReadAuthorityThenForbidden() {
this.mvc.perform(get("/any"))
.andExpect(status().isForbidden());
}

@WithMockUser(authorities="write")
@Test
void postWhenWriteAuthorityThenAuthorized() {
this.mvc.perform(post("/any").with(csrf()))
.andExpect(status().isOk());
}

@WithMockUser(authorities="read")
@Test
void postWhenNoWriteAuthorityThenForbidden() {
this.mvc.perform(get("/any").with(csrf()))
.andExpect(status().isForbidden());
}
java

按调度程序类型匹配

备注

此功能目前在 XML 中不受支持

正如前面所述,Spring Security 默认授权所有调度类型。尽管在 REQUEST 调度上建立的安全上下文会延续到后续的调度中,但细微的不匹配有时会导致意外的 AccessDeniedException

为了解决这个问题,你可以配置 Spring Security 的 Java 配置以允许像 FORWARDERROR 这样的调度器类型,如下所示:

http
.authorizeHttpRequests((authorize) -> authorize
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.requestMatchers("/endpoint").permitAll()
.anyRequest().denyAll()
)
java

使用 MvcRequestMatcher

一般来说,你可以像上面演示的那样使用 requestMatchers(String)

但是,如果你将 Spring MVC 映射到不同的 servlet 路径,那么你需要在安全配置中考虑到这一点。

例如,如果 Spring MVC 被映射到 /spring-mvc 而不是 /(默认值),那么你可能有一个像 /spring-mvc/my/controller 的端点需要授权。

你需要在配置中使用 MvcRequestMatcher 来拆分 servlet 路径和控制器路径,如下所示:

@Bean
MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
return new MvcRequestMatcher.Builder(introspector).servletPath("/spring-mvc");
}

@Bean
SecurityFilterChain appEndpoints(HttpSecurity http, MvcRequestMatcher.Builder mvc) {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(mvc.pattern("/my/controller/**")).hasAuthority("controller")
.anyRequest().authenticated()
);

return http.build();
}
java

这种需求可以通过至少两种不同的方式产生:

  • 如果你使用 spring.mvc.servlet.path Boot 属性将默认路径(/)更改为其他路径

  • 如果你注册了多个 Spring MVC DispatcherServlet(因此需要其中一个不使用默认路径)

使用自定义匹配器

备注

此功能当前在 XML 中不受支持

在 Java 配置中,你可以创建自己的 RequestMatcher,并像下面这样将其提供给 DSL:

RequestMatcher printview = (request) -> request.getParameter("print") != null;
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(printview).hasAuthority("print")
.anyRequest().authenticated()
)
java
提示

因为 RequestMatcher 是一个函数式接口,你可以在 DSL 中以 lambda 的形式提供它。但是,如果你想从请求中提取值,你需要有一个具体的类,因为这需要重写一个 default 方法。

一旦授权,你可以通过以下方式使用Security 的测试支持进行测试:

@WithMockUser(authorities="print")
@Test
void printWhenPrintAuthorityThenAuthorized() {
this.mvc.perform(get("/any?print"))
.andExpect(status().isOk());
}

@WithMockUser
@Test
void printWhenNoPrintAuthorityThenForbidden() {
this.mvc.perform(get("/any?print"))
.andExpect(status().isForbidden());
}
java

授权请求

一旦请求匹配成功,你可以通过多种方式对其进行授权已见,如 permitAlldenyAllhasAuthority

作为一个快速总结,以下是内置在DSL中的授权规则:

  • permitAll - 请求不需要授权,是一个公开的端点;请注意,在这种情况下,认证 从未从会话中检索

  • denyAll - 请求在任何情况下都不允许;请注意,在这种情况下,Authentication 从未从会话中检索

  • hasAuthority - 请求要求 Authentication 具有与给定值匹配的 GrantedAuthority

  • hasRole - hasAuthority 的快捷方式,前缀为 ROLE_ 或配置的默认前缀

  • hasAnyAuthority - 请求要求 Authentication 具有与给定值中的任何一个匹配的 GrantedAuthority

  • hasAnyRole - hasAnyAuthority 的快捷方式,前缀为 ROLE_ 或配置的默认前缀

  • access - 请求使用这个自定义的 AuthorizationManager 来确定访问权限

现在你已经学习了模式、规则以及它们如何组合在一起,你应该能够理解这个更复杂的例子中发生了什么:

import static jakarta.servlet.DispatcherType.*;

import static org.springframework.security.authorization.AuthorizationManagers.allOf;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;

@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
// ...
.authorizeHttpRequests(authorize -> authorize 1
.dispatcherTypeMatchers(FORWARD, ERROR).permitAll() 2
.requestMatchers("/static/**", "/signup", "/about").permitAll() 3
.requestMatchers("/admin/**").hasRole("ADMIN") 4
.requestMatchers("/db/**").access(allOf(hasAuthority("db"), hasRole("ADMIN"))) 5
.anyRequest().denyAll() 6
);

return http.build();
}
java
  • 指定了多个授权规则。每个规则按照声明的顺序进行考虑。

  • 允许 FORWARDERROR 调度,以便 Spring MVC 渲染视图和 Spring Boot 渲染错误。

  • 我们指定了多个任何用户都可以访问的 URL 模式。具体来说,如果 URL 以 "/static/" 开头、等于 "/signup" 或等于 "/about",则任何用户都可以访问该请求。

  • 任何以 "/admin/" 开头的 URL 将限制为具有 "ROLE_ADMIN" 角色的用户。请注意,由于我们调用了 hasRole 方法,因此不需要指定 "ROLE_" 前缀。

  • 任何以 "/db/" 开头的 URL 需要用户同时被授予 "db" 权限并具有 "ROLE_ADMIN" 角色。请注意,由于我们使用了 hasRole 表达式,因此不需要指定 "ROLE_" 前缀。

  • 任何尚未匹配的 URL 都会被拒绝访问。如果您不想意外忘记更新您的授权规则,这是一个好策略。

使用SpEL表达授权

虽然建议使用具体的 AuthorizationManager,但在某些情况下,比如使用 <intercept-url> 或 JSP 标签库时,表达式是必需的。因此,本节将重点介绍这些领域的示例。

鉴于此,让我们更深入地了解一下 Spring Security 的 Web 安全授权 SpEL API。

Spring Security 将其所有的授权字段和方法封装在一组根对象中。最通用的根对象称为 SecurityExpressionRoot,它是 WebSecurityExpressionRoot 的基础。当准备评估授权表达式时,Spring Security 会将此根对象提供给 StandardEvaluationContext

使用授权表达式字段和方法

这首先为您的SpEL表达式提供了一组增强的授权字段和方法。以下是最常用方法的快速概述:

  • permitAll - 请求不需要任何授权即可调用;请注意,在这种情况下,认证 从未从会话中检索

  • denyAll - 在任何情况下都不允许请求;请注意,在这种情况下,Authentication 从未从会话中检索

  • hasAuthority - 请求要求 Authentication 具有与给定值匹配的 GrantedAuthority

  • hasRole - 是 hasAuthority 的快捷方式,它添加了 ROLE_ 前缀或配置为默认前缀的内容

  • hasAnyAuthority - 请求要求 Authentication 具有与给定值中的任何一个匹配的 GrantedAuthority

  • hasAnyRole - 是 hasAnyAuthority 的快捷方式,它添加了 ROLE_ 前缀或配置为默认前缀的内容

  • hasPermission - 一个钩子,用于您的 PermissionEvaluator 实例进行对象级授权

以下是最常见的字段简介:

  • authentication - 与此方法调用关联的 Authentication 实例

  • principal - 与此方法调用关联的 Authentication#getPrincipal

现在你已经学习了模式、规则以及它们如何组合在一起,你应该能够理解这个更复杂的例子中发生了什么:

<http>
<intercept-url pattern="/static/**" access="permitAll"/> 1
<intercept-url pattern="/admin/**" access="hasRole('ADMIN')"/> 2
<intercept-url pattern="/db/**" access="hasAuthority('db') and hasRole('ADMIN')"/> 3
<intercept-url pattern="/**" access="denyAll"/> 4
</http>
java
  • 我们指定了一个任何用户都可以访问的 URL 模式。具体来说,如果 URL 以 "/static/" 开头,则任何用户都可以访问该请求。

  • 任何以 "/admin/" 开头的 URL 都将限制为具有 "ROLE_ADMIN" 角色的用户。你会注意到,由于我们调用了 hasRole 方法,因此不需要指定 "ROLE_" 前缀。

  • 任何以 "/db/" 开头的 URL 都要求用户同时被授予 "db" 权限以及是 "ROLE_ADMIN"。你会注意到,由于我们使用了 hasRole 表达式,因此不需要指定 "ROLE_" 前缀。

  • 任何尚未匹配的 URL 都会被拒绝访问。如果你不想因为忘记更新授权规则而导致意外情况,这是一个好策略。

使用路径参数

此外,Spring Security 提供了一种机制来发现路径参数,因此也可以在 SpEL 表达式中访问它们。

例如,你可以通过以下方式在你的SpEL表达式中访问路径参数:

<http>
<intercept-url pattern="/resource/{name}" access="#name == authentication.name"/>
<intercept-url pattern="/**" access="authenticated"/>
</http>
xml

此表达式引用 /resource/ 之后的路径变量,并要求其等于 Authentication#getName

使用授权数据库、策略代理或其他服务

如果你想要配置 Spring Security 以使用单独的服务进行授权,你可以创建自己的 AuthorizationManager 并将其匹配到 anyRequest

首先,您的 AuthorizationManager 可能看起来像这样:

@Component
public final class OpenPolicyAgentAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
// make request to Open Policy Agent
}
}
java

然后,你可以通过以下方式将其与 Spring Security 连接起来:

@Bean
SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> authz) throws Exception {
http
// ...
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().access(authz)
);

return http.build();
}
java

优先使用 permitAll 而不是 ignoring

当你有静态资源时,可能会倾向于配置过滤器链来忽略这些值。一个更安全的方法是使用 permitAll 允许它们,如下所示:

http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/css/**").permitAll()
.anyRequest().authenticated()
)
java

它更安全,因为即使对于静态资源,编写安全的头部信息也很重要,而如果请求被忽略,Spring Security 也无法做到这一点。

在过去,这伴随着性能上的权衡,因为 Spring Security 会在每次请求时都检查会话。然而,从 Spring Security 6 开始,除非授权规则需要,否则不会再 ping 会话。由于性能影响现在已得到解决,Spring Security 建议对所有请求至少使用 permitAll

authorizeRequests 迁移

备注

AuthorizationFilter 取代了 FilterSecurityInterceptor。为了保持向后兼容,FilterSecurityInterceptor 仍然是默认配置。本节讨论 AuthorizationFilter 的工作原理以及如何覆盖默认配置。

AuthorizationFilterHttpServletRequest 提供授权。它被插入到 FilterChainProxy 中,作为 Security Filters 之一。

你可以覆盖默认设置,在声明 SecurityFilterChain 时,不要使用 authorizeRequests,而是使用 authorizeHttpRequests,如下所示:

@Bean
SecurityFilterChain web(HttpSecurity http) throws AuthenticationException {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated();
)
// ...

return http.build();
}
java

这在很多方面改进了 authorizeRequests

  1. 使用简化的 AuthorizationManager API,而不是元数据源、配置属性、决策管理器和投票者。这简化了重用和自定义。

  2. 延迟 Authentication 查找。不需要在每个请求中都查找身份验证,而只在需要身份验证的授权决策请求中进行查找。

  3. 基于 Bean 的配置支持。

当使用 authorizeHttpRequests 而不是 authorizeRequests 时,会使用 AuthorizationFilter 而不是 FilterSecurityInterceptor

迁移表达式

如果可能,建议使用类型安全的授权管理器而不是 SpEL。对于 Java 配置,可以使用 WebExpressionAuthorizationManager 来帮助迁移遗留的 SpEL。

要使用 WebExpressionAuthorizationManager,你可以用你正在尝试迁移的表达式来构造一个,如下所示:

.requestMatchers("/test/**").access(new WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')"))
java

如果你在表达式中引用了一个 bean,如下所示:@webSecurity.check(authentication, request),建议你直接调用该 bean,看起来会像下面这样:

.requestMatchers("/test/**").access((authentication, context) ->
new AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest())))
java

对于包含 bean 引用以及其他表达式的复杂指令,建议你将它们改为实现 AuthorizationManager,并通过调用 .access(AuthorizationManager) 来引用它们。

如果你无法做到这一点,你可以配置一个 DefaultHttpSecurityExpressionHandler 并使用 bean 解析器,然后将其提供给 WebExpressionAuthorizationManager#setExpressionhandler

安全匹配器

RequestMatcher接口用于确定请求是否与给定的规则匹配。我们使用securityMatchers来确定给定的HttpSecurity是否应应用于某个请求。同样地,我们可以使用requestMatchers来确定应应用于某个请求的授权规则。请看下面的例子:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") 1
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/user/**").hasRole("USER") 2
.requestMatchers("/admin/**").hasRole("ADMIN") 3
.anyRequest().authenticated() 4
)
.formLogin(withDefaults());
return http.build();
}
}
java
  • 配置 HttpSecurity 仅应用于以 /api/ 开头的 URL

  • 允许具有 USER 角色的用户访问以 /user/ 开头的 URL

  • 允许具有 ADMIN 角色的用户访问以 /admin/ 开头的 URL

  • 任何其他不匹配上述规则的请求,都将需要身份验证

securityMatcher(s)requestMatcher(s) 方法将决定哪个 RequestMatcher 实现最适合您的应用程序:如果 Spring MVC 在类路径中,则使用 MvcRequestMatcher,否则,将使用 AntPathRequestMatcher。您可以在此处阅读有关 Spring MVC 集成的更多信息这里

如果你想要使用特定的 RequestMatcher,只需将实现传递给 securityMatcher 和/或 requestMatcher 方法:

import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; 1
import static org.springframework.security.web.util.matcher.RegexRequestMatcher.regexMatcher;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher(antMatcher("/api/**")) 2
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(antMatcher("/user/**")).hasRole("USER") 3
.requestMatchers(regexMatcher("/admin/.*")).hasRole("ADMIN") 4
.requestMatchers(new MyCustomRequestMatcher()).hasRole("SUPERVISOR") 5
.anyRequest().authenticated()
)
.formLogin(withDefaults());
return http.build();
}
}

public class MyCustomRequestMatcher implements RequestMatcher {

@Override
public boolean matches(HttpServletRequest request) {
// ...
}
}
java
  • AntPathRequestMatcherRegexRequestMatcher 导入静态工厂方法以创建 RequestMatcher 实例。

  • 配置 HttpSecurity 仅应用于以 /api/ 开头的 URL,使用 AntPathRequestMatcher

  • 允许具有 USER 角色的用户访问以 /user/ 开头的 URL,使用 AntPathRequestMatcher

  • 允许具有 ADMIN 角色的用户访问以 /admin/ 开头的 URL,使用 RegexRequestMatcher

  • 允许具有 SUPERVISOR 角色的用户访问与 MyCustomRequestMatcher 匹配的 URL,使用自定义的 RequestMatcher

进一步阅读

现在你已经保护了应用程序的请求,可以考虑保护其方法。你还可以进一步阅读有关测试你的应用程序的内容,或者阅读有关将 Spring Security 与应用程序的其他方面(如数据层跟踪和指标)集成的内容。