授权 HttpServletRequests
Spring Security 允许您在请求级别建模您的授权。例如,通过 Spring Security,您可以声明 /admin 下的所有页面需要一个权限,而所有其他页面只需进行身份验证。
默认情况下,Spring Security 要求每个请求都经过身份验证。也就是说,无论何时使用HttpSecurity 实例,都必须声明您的授权规则。
只要您有一个 HttpSecurity 实例,您至少应该这样做:
-
Java
-
Kotlin
-
Xml
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
}
<http>
<intercept-url pattern="/**" access="authenticated"/>
</http>
这告诉 Spring Security,您应用程序中的任何端点都要求安全上下文至少经过身份验证才能允许访问。
在许多情况下,您的授权规则将比这更复杂,因此请考虑以下用例:
-
我有一个使用
authorizeRequests的应用程序,我想将其迁移到authorizeHttpRequests -
我想匹配请求,并且我将 Spring MVC 映射到非默认 Servlet 的东西
-
我想授权请求
-
我想将请求授权委托给策略代理
理解请求授权组件的工作原理
| 本节在Servlet 架构和实现的基础上,深入探讨了基于 Servlet 的应用程序中授权在请求级别的工作原理。 |
-
首先,AuthorizationFilter构建一个Supplier,该Supplier从SecurityContextHolder检索Authentication。 -
其次,它将 Supplier<Authentication>和HttpServletRequest传递给AuthorizationManager。AuthorizationManager将请求与authorizeHttpRequests中的模式匹配,并运行相应的规则。-
如果授权被拒绝,将发布 AuthorizationDeniedEvent,并抛出AccessDeniedException。在这种情况下,ExceptionTranslationFilter处理AccessDeniedException。 -
如果访问被授予,将发布 AuthorizationGrantedEvent,并且AuthorizationFilter继续执行FilterChain,从而允许应用程序正常处理。
-
AuthorizationFilter 默认位于末尾
AuthorizationFilter 默认位于Spring Security 过滤器链的末尾。这意味着 Spring Security 的身份验证过滤器、漏洞防护和其他过滤器集成不需要授权。如果您在 AuthorizationFilter 之前添加自己的过滤器,它们也不需要授权;否则,它们将需要授权。
这通常在您添加 Spring MVC 端点时变得很重要。因为它们由 DispatcherServlet 执行,并且位于 AuthorizationFilter 之后,所以您的端点需要包含在 authorizeHttpRequests 中才能被允许。
所有分派都已授权
AuthorizationFilter 不仅在每个请求上运行,而且在每个分派上运行。这意味着 REQUEST 分派需要授权,FORWARD、ERROR 和 INCLUDE 也需要授权。
例如,Spring MVC 可以将请求 FORWARD 到一个视图解析器,该解析器渲染一个 Thymeleaf 模板,如下所示:
-
Java
-
Kotlin
@Controller
public class MyController {
@GetMapping("/endpoint")
public String endpoint() {
return "endpoint";
}
}
@Controller
class MyController {
@GetMapping("/endpoint")
fun endpoint(): String {
return "endpoint"
}
}
在这种情况下,授权发生两次;一次是授权 /endpoint,另一次是转发到 Thymeleaf 渲染“endpoint”模板。
因此,您可能希望允许所有 FORWARD 分派。
这个原则的另一个例子是 Spring Boot 如何处理错误。如果容器捕获到一个异常,例如以下情况:
-
Java
-
Kotlin
@Controller
public class MyController {
@GetMapping("/endpoint")
public String endpoint() {
throw new UnsupportedOperationException("unsupported");
}
}
@Controller
class MyController {
@GetMapping("/endpoint")
fun endpoint(): String {
throw UnsupportedOperationException("unsupported")
}
}
那么 Boot 将其分派到 ERROR 分派。
在这种情况下,授权也发生两次;一次是授权 /endpoint,另一次是分派错误。
因此,您可能希望允许所有 ERROR 分派。
Authentication 查找被推迟
当请求始终被允许或始终被拒绝时,这与 authorizeHttpRequests 相关。在这些情况下,Authentication 不会被查询,从而加快了请求速度。
授权端点
您可以配置 Spring Security,通过按优先级顺序添加更多规则来拥有不同的规则。
如果您希望 /endpoint 只能由具有 USER 权限的最终用户访问,那么您可以这样做:
-
Java
-
Kotlin
-
Xml
@Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/endpoint").hasAuthority("USER")
.anyRequest().authenticated()
)
// ...
return http.build();
}
@Bean
fun web(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/endpoint", hasAuthority("USER"))
authorize(anyRequest, authenticated)
}
}
return http.build()
}
<http>
<intercept-url pattern="/endpoint" access="hasAuthority('USER')"/>
<intercept-url pattern="/**" access="authenticated"/>
</http>
如您所见,声明可以分解为模式/规则对。
AuthorizationFilter 按列出的顺序处理这些对,只将第一个匹配项应用于请求。这意味着即使 /** 也将匹配 /endpoint,上述规则也不是问题。上述规则的解释是:“如果请求是 /endpoint,则需要 USER 权限;否则,只需进行身份验证”。
Spring Security 支持多种模式和多种规则;您也可以以编程方式创建自己的模式和规则。
一旦获得授权,您可以使用 Security 的测试支持,按以下方式进行测试:
-
Java
@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());
}
匹配请求
上面您已经看到了两种匹配请求的方式。
您看到的第一个是最简单的,即匹配任何请求。
使用 Ant 匹配
Ant 是 Spring Security 用于匹配请求的默认语言。
您可以使用它来匹配单个端点或目录,甚至可以捕获占位符以供以后使用。您还可以将其细化以匹配特定的 HTTP 方法集。
假设您不想匹配 /endpoint 端点,而是想匹配 /resource 目录下的所有端点。在这种情况下,您可以执行以下操作:
-
Java
-
Kotlin
-
Xml
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/resource/**").hasAuthority("USER")
.anyRequest().authenticated()
)
http {
authorizeHttpRequests {
authorize("/resource/**", hasAuthority("USER"))
authorize(anyRequest, authenticated)
}
}
<http>
<intercept-url pattern="/resource/**" access="hasAuthority('USER')"/>
<intercept-url pattern="/**" access="authenticated"/>
</http>
其解读方式是:“如果请求是 /resource 或其某个子目录,则需要 USER 权限;否则,只需进行身份验证”。
您还可以从请求中提取路径值,如下所示:
-
Java
-
Kotlin
-
Xml
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/resource/{name}").access(new WebExpressionAuthorizationManager("#name == authentication.name"))
.anyRequest().authenticated()
)
http {
authorizeHttpRequests {
authorize("/resource/{name}", WebExpressionAuthorizationManager("#name == authentication.name"))
authorize(anyRequest, authenticated)
}
}
<http>
<intercept-url pattern="/resource/{name}" access="#name == authentication.name"/>
<intercept-url pattern="/**" access="authenticated"/>
</http>
一旦获得授权,您可以使用 Security 的测试支持,按以下方式进行测试:
-
Java
@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());
}
| Spring Security 只匹配路径。如果您想匹配查询参数,您将需要一个自定义请求匹配器。 |
使用正则表达式匹配
Spring Security 支持根据正则表达式匹配请求。如果您想应用比子目录上的 ** 更严格的匹配条件,这会派上用场。
例如,考虑一个包含用户名的路径,以及所有用户名必须是字母数字的规则。您可以使用 RegexRequestMatcher 来遵守此规则,如下所示:
-
Java
-
Kotlin
-
Xml
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(RegexRequestMatcher.regexMatcher("/resource/[A-Za-z0-9]+")).hasAuthority("USER")
.anyRequest().denyAll()
)
http {
authorizeHttpRequests {
authorize(RegexRequestMatcher.regexMatcher("/resource/[A-Za-z0-9]+"), hasAuthority("USER"))
authorize(anyRequest, denyAll)
}
}
<http>
<intercept-url request-matcher="regex" pattern="/resource/[A-Za-z0-9]+" access="hasAuthority('USER')"/>
<intercept-url pattern="/**" access="denyAll"/>
</http>
按 HTTP 方法匹配
您还可以通过 HTTP 方法匹配规则。这在通过授予的权限(例如授予 read 或 write 权限)进行授权时非常方便。
要要求所有 GET 请求具有 read 权限,所有 POST 请求具有 write 权限,您可以这样做:
-
Java
-
Kotlin
-
Xml
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(HttpMethod.GET).hasAuthority("read")
.requestMatchers(HttpMethod.POST).hasAuthority("write")
.anyRequest().denyAll()
)
http {
authorizeHttpRequests {
authorize(HttpMethod.GET, hasAuthority("read"))
authorize(HttpMethod.POST, hasAuthority("write"))
authorize(anyRequest, denyAll)
}
}
<http>
<intercept-url http-method="GET" pattern="/**" access="hasAuthority('read')"/>
<intercept-url http-method="POST" pattern="/**" access="hasAuthority('write')"/>
<intercept-url pattern="/**" access="denyAll"/>
</http>
这些授权规则应解读为:“如果请求是 GET,则需要 read 权限;否则,如果请求是 POST,则需要 write 权限;否则,拒绝请求”。
| 默认拒绝请求是一种健康的安全性做法,因为它将规则集转换为允许列表。 |
一旦获得授权,您可以使用 Security 的测试支持,按以下方式进行测试:
-
Java
@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());
}
按调度器类型匹配
| 此功能目前不支持 XML |
如前所述,Spring Security 默认授权所有调度器类型。尽管在 REQUEST 调度上建立的安全上下文会延续到后续调度,但细微的不匹配有时会导致意外的 AccessDeniedException。
为了解决这个问题,您可以配置 Spring Security Java 配置以允许 FORWARD 和 ERROR 等调度器类型,如下所示:
-
Java
-
Kotlin
http
.authorizeHttpRequests((authorize) -> authorize
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.requestMatchers("/endpoint").permitAll()
.anyRequest().denyAll()
)
http {
authorizeHttpRequests {
authorize(DispatcherTypeRequestMatcher(DispatcherType.FORWARD), permitAll)
authorize(DispatcherTypeRequestMatcher(DispatcherType.ERROR), permitAll)
authorize("/endpoint", permitAll)
authorize(anyRequest, denyAll)
}
}
按 Servlet 路径匹配
一般而言,您可以使用 requestMatchers(String),如上所示。
但是,如果您有来自多个 servlet 的授权规则,则需要指定这些规则:
-
Java
-
Kotlin
-
Xml
import static org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher.withDefaults;
@Bean
SecurityFilterChain appEndpoints(HttpSecurity http) {
PathPatternRequestMatcher.Builder mvc = withDefaults().basePath("/spring-mvc");
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(mvc.matcher("/admin/**")).hasAuthority("admin")
.requestMatchers(mvc.matcher("/my/controller/**")).hasAuthority("controller")
.anyRequest().authenticated()
);
return http.build();
}
@Bean
fun appEndpoints(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/spring-mvc", "/admin/**", hasAuthority("admin"))
authorize("/spring-mvc", "/my/controller/**", hasAuthority("controller"))
authorize(anyRequest, authenticated)
}
}
}
<http>
<intercept-url servlet-path="/spring-mvc" pattern="/admin/**" access="hasAuthority('admin')"/>
<intercept-url servlet-path="/spring-mvc" pattern="/my/controller/**" access="hasAuthority('controller')"/>
<intercept-url pattern="/**" access="authenticated"/>
</http>
这是因为 Spring Security 要求所有 URI 都是绝对的(减去上下文路径)。
|
还有其他几个组件为您创建请求匹配器,例如 |
使用自定义匹配器
| 此功能目前不支持 XML |
在 Java 配置中,您可以创建自己的 RequestMatcher 并将其提供给 DSL,如下所示:
-
Java
-
Kotlin
RequestMatcher printview = (request) -> request.getParameter("print") != null;
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(printview).hasAuthority("print")
.anyRequest().authenticated()
)
val printview: RequestMatcher = { (request) -> request.getParameter("print") != null }
http {
authorizeHttpRequests {
authorize(printview, hasAuthority("print"))
authorize(anyRequest, authenticated)
}
}
由于 RequestMatcher 是一个函数式接口,您可以将其作为 lambda 表达式提供给 DSL。但是,如果您想从请求中提取值,则需要一个具体类,因为这需要重写一个 default 方法。 |
一旦获得授权,您可以使用 Security 的测试支持,按以下方式进行测试:
-
Java
@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());
}
授权请求
一旦请求匹配,您可以通过已经看到的几种方式授权它,例如 permitAll、denyAll 和 hasAuthority。
快速总结一下,以下是 DSL 中内置的授权规则:
-
permitAll- 请求不需要授权,是一个公共端点;请注意,在这种情况下,Authentication永远不会从会话中检索。 -
denyAll- 在任何情况下都不允许请求;请注意,在这种情况下,Authentication永远不会从会话中检索。 -
hasAuthority- 请求要求Authentication具有与给定值匹配的GrantedAuthority。 -
hasRole-hasAuthority的快捷方式,其前缀为ROLE_或配置为默认前缀的任何内容。 -
hasAnyAuthority- 请求要求Authentication具有与给定值中任何一个匹配的GrantedAuthority。 -
hasAnyRole-hasAnyAuthority的快捷方式,其前缀为ROLE_或配置为默认前缀的任何内容*hasAnyAuthority- 请求要求Authentication具有与给定值中任何一个匹配的GrantedAuthority。 -
hasAllRoles-hasAllAuthorities的快捷方式,其前缀为ROLE_或配置为默认前缀的任何内容。 -
hasAllAuthorities- 请求要求Authentication具有与所有给定值匹配的GrantedAuthority。 -
access- 请求使用此自定义AuthorizationManager来确定访问权限。
现在您已经了解了模式、规则以及它们如何组合在一起,您应该能够理解这个更复杂的示例中发生的事情:
-
Java
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/**").hasAllAuthorities("db", "ROLE_ADMIN") (5)
.anyRequest().denyAll() (6)
);
return http.build();
}
| 1 | 指定了多个授权规则。每个规则按声明的顺序考虑。 |
| 2 | 允许 FORWARD 和 ERROR 分派,以允许 Spring MVC 渲染视图和 Spring Boot 渲染错误。 |
| 3 | 我们指定了多个任何用户都可以访问的 URL 模式。具体来说,如果 URL 以“/static/”开头、等于“/signup”或等于“/about”,则任何用户都可以访问该请求。 |
| 4 | 任何以“/admin/”开头的 URL 都将限制为具有“ROLE_ADMIN”角色的用户。您会注意到,由于我们调用的是 hasRole 方法,因此无需指定“ROLE_”前缀。 |
| 5 | 任何以“/db/”开头的 URL 都要求用户同时被授予“db”权限并具有“ROLE_ADMIN”角色。您会注意到,由于我们使用 hasAllAuthorities 表达式,因此必须指定“ROLE_”前缀。 |
| 6 | 任何尚未匹配的 URL 都将被拒绝访问。如果您不想意外地忘记更新授权规则,这是一个很好的策略。 |
自定义授权管理器
当您使用 authorizeHttpRequests DSL 时,Spring Security 会为您创建适当的 AuthorizationManager 实例。在某些情况下,您可能希望自定义创建内容,以完全控制框架级别的授权决策。
为了控制创建用于授权 HTTP 请求的 AuthorizationManager 实例,您可以创建自定义的 AuthorizationManagerFactory。例如,假设您想创建一个约定,即经过身份验证的用户必须经过身份验证 *AND* 拥有 USER 角色。为此,您可以为 HTTP 请求创建一个自定义实现,如以下示例所示:
-
Java
-
Kotlin
@Component
public class CustomHttpRequestsAuthorizationManagerFactory
implements AuthorizationManagerFactory<RequestAuthorizationContext> {
private final AuthorizationManagerFactory<RequestAuthorizationContext> delegate =
new DefaultAuthorizationManagerFactory<>();
@Override
public AuthorizationManager<RequestAuthorizationContext> authenticated() {
return AuthorizationManagers.allOf(
this.delegate.authenticated(),
this.delegate.hasRole("USER")
);
}
}
@Component
class CustomHttpRequestsAuthorizationManagerFactory : AuthorizationManagerFactory<RequestAuthorizationContext> {
private val delegate = DefaultAuthorizationManagerFactory<RequestAuthorizationContext>()
override fun authenticated(): AuthorizationManager<RequestAuthorizationContext> {
return AuthorizationManagers.allOf(
delegate.authenticated(),
delegate.hasRole("USER")
)
}
}
现在,无论何时您需要身份验证,Spring Security 都会自动调用您的自定义工厂来创建需要身份验证 *AND* USER 角色的 AuthorizationManager 实例。
我们将其用作创建自定义 AuthorizationManagerFactory 的简单示例,但也可以(而且通常更简单)仅为特定请求替换特定的 AuthorizationManager。有关示例,请参阅使用授权数据库、策略代理或其他服务。 |
使用 SpEL 表达授权
虽然建议使用具体的 AuthorizationManager,但在某些情况下,表达式是必需的,例如在 <intercept-url> 或 JSP Taglibs 中。因此,本节将重点关注这些领域的示例。
鉴于此,让我们更深入地了解 Spring Security 的 Web Security Authorization SpEL API。
Spring Security 将其所有授权字段和方法封装在一组根对象中。最通用的根对象称为 SecurityExpressionRoot,它构成了 WebSecurityExpressionRoot 的基础。Spring Security 在准备评估授权表达式时将此根对象提供给 StandardEvaluationContext。
使用授权表达式字段和方法
这提供的第一件事是为您的 SpEL 表达式提供一组增强的授权字段和方法。以下是最常用方法的快速概述:
-
permitAll- 请求不需要授权即可调用;请注意,在这种情况下,Authentication永远不会从会话中检索。 -
denyAll- 在任何情况下都不允许请求;请注意,在这种情况下,Authentication永远不会从会话中检索。 -
hasAuthority- 请求要求Authentication具有与给定值匹配的GrantedAuthority。 -
hasRole-hasAuthority的快捷方式,其前缀为ROLE_或配置为默认前缀的任何内容。 -
hasAnyAuthority- 请求要求Authentication具有与给定值中任何一个匹配的GrantedAuthority。 -
hasAnyRole-hasAnyAuthority的快捷方式,其前缀为ROLE_或配置为默认前缀的任何内容。 -
hasPermission- 用于执行对象级别授权的PermissionEvaluator实例的钩子。
以下是最常用字段的简要概述:
-
authentication- 与此方法调用关联的Authentication实例。 -
principal- 与此方法调用关联的Authentication#getPrincipal。
现在您已经了解了模式、规则以及它们如何组合在一起,您应该能够理解这个更复杂的示例中发生的事情:
-
Xml
<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>
| 1 | 我们指定了一个任何用户都可以访问的 URL 模式。具体来说,如果 URL 以“/static/”开头,则任何用户都可以访问该请求。 |
| 2 | 任何以“/admin/”开头的 URL 都将限制为具有“ROLE_ADMIN”角色的用户。您会注意到,由于我们调用的是 hasRole 方法,因此无需指定“ROLE_”前缀。 |
| 3 | 任何以“/db/”开头的 URL 都要求用户同时被授予“db”权限并具有“ROLE_ADMIN”角色。您会注意到,由于我们使用 hasRole 表达式,因此无需指定“ROLE_”前缀。 |
| 4 | 任何尚未匹配的 URL 都将被拒绝访问。如果您不想意外地忘记更新授权规则,这是一个很好的策略。 |
使用路径参数
此外,Spring Security 提供了一种发现路径参数的机制,因此它们也可以在 SpEL 表达式中访问。
例如,您可以通过以下方式在 SpEL 表达式中访问路径参数:
-
Xml
<http>
<intercept-url pattern="/resource/{name}" access="#name == authentication.name"/>
<intercept-url pattern="/**" access="authenticated"/>
</http>
此表达式指的是 /resource/ 后的路径变量,并要求它等于 Authentication#getName。
使用授权数据库、策略代理或其他服务
如果您想配置 Spring Security 以使用单独的服务进行授权,您可以创建自己的 AuthorizationManager 并将其与 anyRequest 匹配。
首先,您的 AuthorizationManager 可能看起来像这样:
-
Java
@Component
public final class OpenPolicyAgentAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
@Override
public AuthorizationResult authorize(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
// make request to Open Policy Agent
}
}
然后,您可以通过以下方式将其连接到 Spring Security:
-
Java
@Bean
SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> authz) throws Exception {
http
// ...
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().access(authz)
);
return http.build();
}
推荐 permitAll 而非 ignoring
当您拥有静态资源时,配置过滤器链忽略这些值可能很诱人。更安全的方法是使用 permitAll 允许它们,如下所示:
-
Java
-
Kotlin
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/css/**").permitAll()
.anyRequest().authenticated()
)
http {
authorizeHttpRequests {
authorize("/css/**", permitAll)
authorize(anyRequest, authenticated)
}
}
它更安全,因为即使是静态资源,编写安全头部也很重要,如果请求被忽略,Spring Security 就无法做到这一点。
过去,这会带来性能上的权衡,因为 Spring Security 在每个请求上都会查询会话。然而,从 Spring Security 6 开始,除非授权规则需要,否则不再对会话进行 ping 操作。由于性能影响现已解决,Spring Security 建议对所有请求至少使用 permitAll。
从 authorizeRequests 迁移
AuthorizationFilter 取代了 FilterSecurityInterceptor。为了保持向后兼容,FilterSecurityInterceptor 仍然是默认值。本节讨论 AuthorizationFilter 的工作原理以及如何覆盖默认配置。 |
AuthorizationFilter 为 HttpServletRequest 提供授权。它作为安全过滤器之一插入到FilterChainProxy中。
当您声明 SecurityFilterChain 时,您可以覆盖默认设置。不再使用 authorizeRequests,而是使用 authorizeHttpRequests,如下所示:
-
Java
@Bean
SecurityFilterChain web(HttpSecurity http) throws AuthenticationException {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated();
)
// ...
return http.build();
}
这在许多方面改进了 authorizeRequests:
-
使用简化的
AuthorizationManagerAPI,而不是元数据源、配置属性、决策管理器和投票器。这简化了重用和定制。 -
延迟
Authentication查找。不再需要为每个请求查找身份验证,它只会查找那些需要身份验证才能做出授权决策的请求。 -
基于 Bean 的配置支持。
当使用 authorizeHttpRequests 而不是 authorizeRequests 时,将使用 AuthorizationFilter 而不是 FilterSecurityInterceptor。
迁移表达式
在可能的情况下,建议您使用类型安全的授权管理器而不是 SpEL。对于 Java 配置,WebExpressionAuthorizationManager 可用于帮助迁移旧版 SpEL。
要使用 WebExpressionAuthorizationManager,您可以使用您要迁移的表达式构建一个,如下所示:
-
Java
-
Kotlin
.requestMatchers("/test/**").access(new WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')"))
.requestMatchers("/test/**").access(WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')"))
要迁移多个,您可以使用 WebExpressionAuthorizationManager#withDefaults
-
Java
-
Kotlin
WebExpressionAuthorizationManager.Builder authz = WebExpressionAuthorizationManager.withDefaults();
.requestMatchers("/test/**").access(authz.expression("hasRole('ADMIN') && hasRole('USER')"))
.requestMatchers("/test/**").access(authz.expression("permitAll"))
var authz = WebExpressionAuthorizationManager.withDefaults()
.requestMatchers("/test/**").access(authz.expression("hasRole('ADMIN') && hasRole('USER')"))
.requestMatchers("/test/**").access(authz.expression("permitAll"))
如果您在表达式中引用 bean,例如:@webSecurity.check(authentication, request),建议您直接调用 bean,这将类似于以下内容:
-
Java
-
Kotlin
.requestMatchers("/test/**").access((authentication, context) ->
new AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest())))
.requestMatchers("/test/**").access((authentication, context): AuthorizationManager<RequestAuthorizationContext> ->
AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest())))
对于包含 Bean 引用以及其他表达式的复杂指令,建议您将它们更改为实现 AuthorizationManager,并通过调用 .access(AuthorizationManager) 来引用它们。
如果无法做到这一点,您可以将 WebExpressionAuthorizationManager.Builder 发布为 bean:
-
Java
-
Kotlin
@Bean
WebExpressionAuthorizationManager.Builder authz() {
return WebExpressionAuthorizationManager.withDefaults();
}
@Bean
fun authz(): WebExpressionAuthorizationManager.Builder {
return WebExpressionAuthorizationManager.withDefaults()
}
然后,传递给该构建器的表达式将能够引用 bean。
安全匹配器
RequestMatcher 接口用于确定请求是否匹配给定的规则。我们使用 securityMatchers 来确定给定的 HttpSecurity 是否应该应用于给定的请求。同样,我们可以使用 requestMatchers 来确定我们应该应用于给定请求的授权规则。请看以下示例:
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") (1)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/api/user/**").hasRole("USER") (2)
.requestMatchers("/api/admin/**").hasRole("ADMIN") (3)
.anyRequest().authenticated() (4)
)
.formLogin(withDefaults());
return http.build();
}
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
@Bean
open fun web(http: HttpSecurity): SecurityFilterChain {
http {
securityMatcher("/api/**") (1)
authorizeHttpRequests {
authorize("/api/user/**", hasRole("USER")) (2)
authorize("/api/admin/**", hasRole("ADMIN")) (3)
authorize(anyRequest, authenticated) (4)
}
}
return http.build()
}
}
| 1 | 配置 HttpSecurity 仅应用于以 /api/ 开头的 URL。 |
| 2 | 允许具有 USER 角色的用户访问以 /api/user/ 开头的 URL。 |
| 3 | 允许具有 ADMIN 角色的用户访问以 /api/admin/ 开头的 URL。 |
| 4 | 任何不符合上述规则的其他请求都将需要身份验证。 |
如果可用,securityMatcher(s) 和 requestMatcher(s) 方法将使用 PathPatternRequestMatcher.Builder Bean 构造 RequestMatcher。您可以在此处阅读有关 Spring MVC 集成的更多信息。
如果您想使用特定的 RequestMatcher,只需将实现传递给 securityMatcher 和/或 requestMatcher 方法:
-
Java
-
Kotlin
import static org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher.withDefaults; (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(withDefaults().matcher("/api/user/**")).hasRole("USER") (3)
.requestMatchers(regexMatcher("/api/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) {
// ...
}
}
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher.withDefaults (1)
import org.springframework.security.web.util.matcher.RegexRequestMatcher.regexMatcher
@Configuration
@EnableWebSecurity
open class SecurityConfig {
@Bean
open fun web(http: HttpSecurity): SecurityFilterChain {
http {
securityMatcher(antMatcher("/api/**")) (2)
authorizeHttpRequests {
authorize(withDefaults().matcher("/api/user/**"), hasRole("USER")) (3)
authorize(regexMatcher("/api/admin/**"), hasRole("ADMIN")) (4)
authorize(MyCustomRequestMatcher(), hasRole("SUPERVISOR")) (5)
authorize(anyRequest, authenticated)
}
}
return http.build()
}
}
| 1 | 从 PathPatternRequestMatcher 和 RegexRequestMatcher 导入静态工厂方法以创建 RequestMatcher 实例。 |
| 2 | 使用 PathPatternRequestMatcher 配置 HttpSecurity 仅应用于以 /api/ 开头的 URL。 |
| 3 | 使用 PathPatternRequestMatcher 允许具有 USER 角色的用户访问以 /api/user/ 开头的 URL。 |
| 4 | 使用 RegexRequestMatcher 允许具有 ADMIN 角色的用户访问以 /api/admin/ 开头的 URL。 |
| 5 | 使用自定义 RequestMatcher 允许具有 SUPERVISOR 角色的用户访问匹配 MyCustomRequestMatcher 的 URL。 |