跳到主要内容
版本:7.0.2

方法安全

DeepSeek V3 中英对照 Method Security

除了在请求级别进行授权建模之外,Spring Security 还支持在方法级别进行建模。

您可以通过在任意 @Configuration 类上添加 @EnableMethodSecurity 注解,或在 XML 配置文件中添加 <method-security> 元素来激活它,例如:

@EnableMethodSecurity

接着,你可以立即使用 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 注解来标注任何由 Spring 管理的类或方法,以授权方法调用,包括输入参数和返回值。

备注

Spring Boot Starter Security 默认不会激活方法级别的授权。

方法安全还支持许多其他用例,包括 AspectJ 支持自定义注解 以及多个配置点。建议了解以下用例:

方法安全的工作原理

Spring Security 的方法授权支持适用于以下场景:

  • 提取细粒度的授权逻辑;例如,当方法参数和返回值影响授权决策时。

  • 在服务层实施安全控制

  • 在风格上倾向于基于注解的配置,而非基于 HttpSecurity 的配置

由于方法安全是基于 Spring AOP 构建的,你可以利用其全部表达能力,根据需要覆盖 Spring Security 的默认设置。

如前所述,您可以通过在 @Configuration 类上添加 @EnableMethodSecurity 注解,或在 Spring XML 配置文件中使用 <sec:method-security/> 元素来开始配置。

备注

此注解与 XML 元素分别取代了 @EnableGlobalMethodSecurity<sec:global-method-security/>。它们提供了以下改进:

  1. 使用简化的 AuthorizationManager API 替代了元数据源、配置属性、决策管理器和投票器。这简化了重用和自定义过程。

  2. 倾向于基于 Bean 的直接配置,而无需通过继承 GlobalMethodSecurityConfiguration 来自定义 Bean。

  3. 基于原生的 Spring AOP 构建,移除了抽象层,允许您使用 Spring AOP 的构建块进行自定义。

  4. 检查冲突的注解,以确保安全配置的明确性。

  5. 符合 JSR-250 规范。

  6. 默认启用 @PreAuthorize@PostAuthorize@PreFilter@PostFilter

如果您正在使用 @EnableGlobalMethodSecurity<global-method-security/>,请注意它们现已弃用,建议您进行迁移。

方法授权是方法前与方法后授权的结合。考虑一个按以下方式注解的服务Bean:

@Service
public class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
public Customer readCustomer(String id) { ... }
}

当方法安全被激活时,对 MyCustomerService#readCustomer 的调用可能如下所示:

方法安全

  1. Spring AOP 为 readCustomer 方法调用其代理方法。在代理的其他通知器中,它会调用一个匹配 @PreAuthorize 切入点AuthorizationManagerBeforeMethodInterceptor

  2. 拦截器调用 PreAuthorizeAuthorizationManager#check

  3. 授权管理器使用 MethodSecurityExpressionHandler 来解析注解的 SpEL 表达式,并根据包含 Supplier<Authentication>MethodInvocationMethodSecurityExpressionRoot 构建相应的 EvaluationContext

  4. 拦截器使用此上下文来评估表达式;具体来说,它从 Supplier 中读取 Authentication 并检查其 权限 集合中是否包含 permission:read

  5. 如果评估通过,则 Spring AOP 继续调用该方法

  6. 如果未通过,拦截器会发布一个 AuthorizationDeniedEvent 并抛出 AccessDeniedException,该异常被 ExceptionTranslationFilter 捕获并向响应返回 403 状态码

  7. 方法返回后,Spring AOP 会调用一个匹配 @PostAuthorize 切入点AuthorizationManagerAfterMethodInterceptor,其操作与上述相同,但使用 PostAuthorizeAuthorizationManager

  8. 如果评估通过(在此情况下,返回值属于已登录用户),则处理正常继续

  9. 如果未通过,拦截器会发布一个 AuthorizationDeniedEvent 并抛出 AccessDeniedException,该异常被 ExceptionTranslationFilter 捕获并向响应返回 403 状态码

备注

如果方法不是在 HTTP 请求的上下文中被调用,你可能需要自行处理 AccessDeniedException

多个注解以串联方式计算

如上所示,如果一个方法调用涉及多个方法安全注解,这些注解会逐个被处理。这意味着它们可以被视为以"与"逻辑共同作用。换言之,要使调用获得授权,所有注解检查都必须通过授权。

不支持重复注解

需要注意的是,同一个方法上不支持重复使用相同的注解。例如,你不能在同一个方法上放置两次 @PreAuthorize 注解。

相反,请使用SpEL的布尔支持或其委托给独立bean的支持功能。

每个注解都有其专属切入点

每个注解都有其自己的切入点实例,该实例会在整个对象层次结构中寻找该注解或其元注解的对应项,从方法及其封闭类开始。

每个注解都有其专属的方法拦截器

每个注解都有其专用的方法拦截器。这样做的原因是为了提高组合性。例如,如果需要,你可以禁用Spring Security的默认设置,并仅发布@PostAuthorize方法拦截器

方法拦截器如下:

一般来说,你可以将以下列表视为添加 @EnableMethodSecurity 时,Spring Security 发布的拦截器的典型代表:

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor preAuthorizeMethodInterceptor() {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor postAuthorizeMethodInterceptor() {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor preFilterMethodInterceptor() {
return AuthorizationManagerBeforeMethodInterceptor.preFilter();
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor postFilterMethodInterceptor() {
return AuthorizationManagerAfterMethodInterceptor.postFilter();
}

优先使用授权机构而非复杂的SpEL表达式

通常,引入复杂的SpEL表达式会很有吸引力,例如:

@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')")

不过,你也可以将 permission:read 授予拥有 ROLE_ADMIN 的用户。一种方法是使用 RoleHierarchy,如下所示:

@Bean
static RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > permission:read");
}

然后将其设置在 MethodSecurityExpressionHandler 实例中。这样您就可以使用更简单的 @PreAuthorize 表达式,例如:

@PreAuthorize("hasAuthority('permission:read')")

或者,在可能的情况下,在登录时将特定于应用程序的授权逻辑适配为授予的权限。

比较请求级与方法级授权

何时应优先选择方法级授权而非请求级授权?部分取决于个人偏好;但你可以参考以下各项的优势列表来帮助决策。

请求级别方法级别
授权类型粗粒度细粒度
配置位置在配置类中声明在方法声明处本地配置
配置风格DSL注解
授权定义编程式SpEL

主要的权衡点似乎在于你希望授权规则存放在何处。

备注

请务必注意,当您使用基于注解的方法安全时,未标注注解的方法将不受保护。为防止这种情况,请在您的 HttpSecurity 实例中声明 一个全局捕获授权规则

使用注解进行授权

Spring Security 实现方法级授权支持的主要方式是通过注解,您可以将这些注解添加到方法、类和接口上。

使用 @PreAuthorize 进行方法调用授权

方法安全机制启用时,你可以使用@PreAuthorize注解来标注方法,示例如下:

@Component
public class BankService {
@PreAuthorize("hasRole('ADMIN')")
public Account readAccount(Long id) {
// ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority
}
}

这表明只有在提供的表达式 hasRole('ADMIN') 通过时,该方法才能被调用。

然后,你可以测试该类来确认它是否正在强制执行授权规则,如下所示:

@Autowired
BankService bankService;

@WithMockUser(roles="ADMIN")
@Test
void readAccountWithAdminRoleThenInvokes() {
Account account = this.bankService.readAccount("12345678");
// ... assertions
}

@WithMockUser(roles="WRONG")
@Test
void readAccountWithWrongRoleThenAccessDenied() {
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(
() -> this.bankService.readAccount("12345678"));
}
提示

@PreAuthorize 同样可以作为元注解使用,可以在类或接口级别进行定义,并且支持使用SpEL 授权表达式

虽然 @PreAuthorize 在声明所需权限方面非常有用,但它也可用于评估涉及方法参数的更复杂表达式

使用 @PostAuthorize 的授权方法结果

当方法安全机制启用时,你可以使用 @PostAuthorize 注解来标注一个方法,如下所示:

@Component
public class BankService {
@PostAuthorize("returnObject.owner == authentication.name")
public Account readAccount(Long id) {
// ... is only returned if the `Account` belongs to the logged in user
}
}

这表明该方法仅在提供的表达式 returnObject.owner == authentication.name 通过时才能返回值。returnObject 代表要返回的 Account 对象。

然后,你可以测试该类以确认其正在强制执行授权规则:

@Autowired
BankService bankService;

@WithMockUser(username="owner")
@Test
void readAccountWhenOwnedThenReturns() {
Account account = this.bankService.readAccount("12345678");
// ... assertions
}

@WithMockUser(username="wrong")
@Test
void readAccountWhenNotOwnedThenAccessDenied() {
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(
() -> this.bankService.readAccount("12345678"));
}
提示

@PostAuthorize 同样可以作为元注解使用,可以在类或接口级别进行定义,并且支持使用SpEL 授权表达式

@PostAuthorize 在防御不安全的直接对象引用时特别有用。实际上,它可以被定义为一个元注解,如下所示:

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PostAuthorize("returnObject.owner == authentication.name")
public @interface RequireOwnership {}

允许您以如下方式对服务进行注解:

@Component
public class BankService {
@RequireOwnership
public Account readAccount(Long id) {
// ... is only returned if the `Account` belongs to the logged in user
}
}

结果是,只有当Accountowner属性与当前登录用户的name匹配时,上述方法才会返回该Account。否则,Spring Security 将抛出AccessDeniedException并返回 403 状态码。

备注

请注意,不建议在涉及数据库写入操作的类中使用 @PostAuthorize 注解,因为这通常意味着在安全检查之前已经对数据库进行了修改。一个常见的例子是在同一个方法上同时使用 @Transactional@PostAuthorize 注解。相反,应该先读取数据,在读取操作上使用 @PostAuthorize 进行授权检查,如果读取操作被授权,再执行数据库写入操作。如果必须这样做,你可以确保 @EnableTransactionManagement@EnableMethodSecurity 之前启用

使用 @PreFilter 过滤方法参数

当方法安全机制启用时,你可以使用 @PreFilter 注解来标注一个方法,如下所示:

@Component
public class BankService {
@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Account... accounts) {
// ... `accounts` will only contain the accounts owned by the logged-in user
return updated;
}
}

此操作旨在过滤掉 accounts 中所有不满足表达式 filterObject.owner == authentication.name 的值。filterObject 代表 accounts 中的每一个 account,用于对每个 account 进行测试。

然后,你可以通过以下方式测试该类,以确认其正在强制执行授权规则:

@Autowired
BankService bankService;

@WithMockUser(username="owner")
@Test
void updateAccountsWhenOwnedThenReturns() {
Account ownedBy = ...
Account notOwnedBy = ...
Collection<Account> updated = this.bankService.updateAccounts(ownedBy, notOwnedBy);
assertThat(updated).containsOnly(ownedBy);
}
提示

@PreFilter 同样可以作为元注解使用,可以在类或接口级别定义,并且支持使用SpEL 授权表达式

@PreFilter 支持数组、集合、映射以及流(只要流仍处于打开状态)。

例如,上述 updateAccounts 声明将与其他以下四种方式具有相同的功能:

@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Account[] accounts)

@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Collection<Account> accounts)

@PreFilter("filterObject.value.owner == authentication.name")
public Collection<Account> updateAccounts(Map<String, Account> accounts)

@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Stream<Account> accounts)

结果是,上述方法将只返回那些owner属性与登录用户name相匹配的Account实例。

使用 @PostFilter 过滤方法结果

当方法安全机制启用时,你可以使用 @PostFilter 注解来标注一个方法,如下所示:

@Component
public class BankService {
@PostFilter("filterObject.owner == authentication.name")
public Collection<Account> readAccounts(String... ids) {
// ... the return value will be filtered to only contain the accounts owned by the logged-in user
return accounts;
}
}

此操作旨在过滤掉返回值中所有不满足表达式 filterObject.owner == authentication.name 的值。其中,filterObject 代表 accounts 中的每一个 account,用于对每个 account 进行测试。

然后,你可以像这样测试该类,以确认它正在强制执行授权规则:

@Autowired
BankService bankService;

@WithMockUser(username="owner")
@Test
void readAccountsWhenOwnedThenReturns() {
Collection<Account> accounts = this.bankService.updateAccounts("owner", "not-owner");
assertThat(accounts).hasSize(1);
assertThat(accounts.get(0).getOwner()).isEqualTo("owner");
}
提示

@PostFilter 同样可以作为元注解使用,可以在类或接口级别定义,并且支持使用SpEL 授权表达式

@PostFilter 支持数组、集合、映射以及流(只要流仍处于打开状态)。

例如,上述 readAccounts 声明将以下列其他三种方式中的任何一种方式运行:

@PostFilter("filterObject.owner == authentication.name")
public Collection<Account> readAccounts(String... ids)

@PostFilter("filterObject.owner == authentication.name")
public Account[] readAccounts(String... ids)

@PostFilter("filterObject.value.owner == authentication.name")
public Map<String, Account> readAccounts(String... ids)

@PostFilter("filterObject.owner == authentication.name")
public Stream<Account> readAccounts(String... ids)

结果是,上述方法将返回那些owner属性与当前登录用户的name相匹配的Account实例。

备注

内存中的过滤操作可能会带来显著的开销,因此请仔细考虑是否更适合在数据层对数据进行过滤

使用 @Secured 进行方法调用授权

@Secured 是用于授权调用的传统选项。@PreAuthorize 已取代它,建议使用后者。

要使用 @Secured 注解,你首先需要修改你的方法安全声明以启用它,如下所示:

@EnableMethodSecurity(securedEnabled = true)

这将导致 Spring Security 发布相应的方法拦截器,用于对带有 @Secured 注解的方法、类和接口进行授权。

使用 JSR-250 注解进行方法调用授权

若您希望使用 JSR-250 注解,Spring Security 同样提供支持。但 @PreAuthorize 具备更强的表达能力,因此我们推荐使用它。

要使用JSR-250注解,你首先需要修改方法安全声明以启用它们,如下所示:

@EnableMethodSecurity(jsr250Enabled = true)

这将导致 Spring Security 发布对应的方法拦截器,该拦截器会对使用 @RolesAllowed@PermitAll@DenyAll 注解的方法、类和接口进行授权处理。

在类或接口级别声明注解

同样支持在类和接口级别使用方法安全注解。

如果是在类级别,如下所示:

@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
@GetMapping("/endpoint")
public String endpoint() { ... }
}

那么所有方法都会继承类级别的行为。

或者,如果在类和方法级别都像下面这样声明:

@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
@GetMapping("/endpoint")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String endpoint() { ... }
}

那么,声明了该注解的方法将覆盖类级别的注解。

接口的情况也是如此,唯一的例外是:如果一个类从两个不同的接口继承了相同的注解,那么启动过程将会失败。这是因为 Spring Security 无法确定您希望使用哪一个接口的注解。

在这种情况下,你可以通过向具体方法添加注解来解决歧义。

使用元注解

方法安全支持元注解。这意味着您可以使用任何注解,并根据应用程序特定的使用场景提升代码的可读性。

例如,你可以将 @PreAuthorize("hasRole('ADMIN')") 简化为 @IsAdmin,如下所示:

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {}

这样一来,在受保护的方法中,你现在可以执行以下操作:

@Component
public class BankService {
@IsAdmin
public Account readAccount(Long id) {
// ... is only returned if the `Account` belongs to the logged in user
}
}

这使得方法定义更加易读。

模板化元注解表达式

您也可以选择使用元注解模板,这能实现更强大的注解定义。

首先,发布以下bean:

@Bean
static AnnotationTemplateExpressionDefaults templateExpressionDefaults() {
return new AnnotationTemplateExpressionDefaults();
}

现在,你可以创建一个更强大的装饰器,比如 @HasRole,来替代 @IsAdmin,如下所示:

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('{value}')")
public @interface HasRole {
String value();
}

这样一来,在受保护的方法中,你现在可以执行以下操作:

@Component
public class BankService {
@HasRole("ADMIN")
public Account readAccount(Long id) {
// ... is only returned if the `Account` belongs to the logged in user
}
}

请注意,这同样适用于方法变量和所有注解类型,不过你需要小心处理引号,以确保生成的SpEL表达式正确无误。

例如,考虑以下 @HasAnyRole 注解:

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole({roles})")
public @interface HasAnyRole {
String[] roles();
}

在这种情况下,你会注意到不应该在表达式中使用引号,而应该像这样在参数值中使用:

@Component
public class BankService {
@HasAnyRole(roles = { "'USER'", "'ADMIN'" })
public Account readAccount(Long id) {
// ... is only returned if the `Account` belongs to the logged in user
}
}

这样,替换后,表达式就变成了 @PreAuthorize("hasAnyRole('USER', 'ADMIN')")

启用特定注解

你可以关闭 @EnableMethodSecurity 的预配置,并用你自己的配置来替换。如果你想要自定义 AuthorizationManagerPointcut,或者你只是想启用特定的注解,比如 @PostAuthorize,那么你可能会选择这样做。

你可以通过以下方式实现:

@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize() {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
}
}

上述代码片段通过首先禁用方法安全性的预配置,然后发布 @PostAuthorize 拦截器 本身来实现这一功能。

使用 <intercept-methods> 进行授权

在使用Spring Security时,虽然基于注解的方法安全支持是首选方案,但你同样可以通过XML来声明Bean的授权规则。

如果你需要在 XML 配置中声明,可以使用 <intercept-methods>,如下所示:

<bean class="org.mycompany.MyController">
<intercept-methods>
<protect method="get*" access="hasAuthority('read')"/>
<protect method="*" access="hasAuthority('write')"/>
</intercept-methods>
</bean>
备注

此功能仅支持通过前缀或名称匹配方法。如果你的需求比这更复杂,请使用注解支持

以编程方式授权方法

正如您已经看到的,有几种方法可以使用方法安全SpEL表达式来指定非平凡的授权规则。

您可以通过多种方式将逻辑实现从基于SpEL转换为基于Java。这使我们能够利用完整的Java语言功能,从而提升可测试性和流程控制能力。

在 SpEL 中使用自定义 Bean

以编程方式授权方法的第一种方式是一个两步过程。

首先,声明一个包含接收 MethodSecurityExpressionOperations 实例方法的 Bean,如下所示:

@Component("authz")
public class AuthorizationLogic {
public boolean decide(MethodSecurityExpressionOperations operations) {
// ... authorization logic
}
}

然后,在您的注解中按以下方式引用该bean:

@Controller
public class MyController {
@PreAuthorize("@authz.decide(#root)")
@GetMapping("/endpoint")
public String endpoint() {
// ...
}
}

Spring Security 将在每次方法调用时,对该 bean 调用给定的方法。

这样做的好处在于,所有的授权逻辑都集中在一个独立的类中,可以独立进行单元测试并验证其正确性。同时,它还能充分利用完整的 Java 语言功能。

提示

除了返回 Boolean 之外,你还可以返回 null 来表示代码选择弃权,不做出决定。

如果你想包含更多关于决策性质的信息,可以改为返回一个自定义的 AuthorizationDecision,如下所示:

@Component("authz")
public class AuthorizationLogic {
public AuthorizationDecision decide(MethodSecurityExpressionOperations operations) {
// ... authorization logic
return new MyAuthorizationDecision(false, details);
}
}

或者抛出一个自定义的 AuthorizationDeniedException 实例。但请注意,返回一个对象是更推荐的做法,因为这不会产生生成堆栈跟踪的开销。

然后,当您自定义授权结果的处理方式时,即可访问自定义详细信息。

提示

此外,你可以直接返回一个 AuthorizationManager 实例。这在统一自定义 Web 授权规则与方法安全规则时非常有用,因为 Web 安全默认要求指定一个 AuthorizationManager 实例。

使用自定义授权管理器

第二种以编程方式授权方法的方式是创建一个自定义的 AuthorizationManager

首先,声明一个授权管理器实例,或许像这样:

@Component
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
@Override
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocation invocation) {
// ... authorization logic
}

@Override
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
// ... authorization logic
}
}

然后,发布一个方法拦截器,并配置一个切入点,以确定你希望该 AuthorizationManager 何时运行。例如,你可以像这样替换 @PreAuthorize@PostAuthorize 的工作方式:

@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorize(MyAuthorizationManager manager) {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize(MyAuthorizationManager manager) {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
}
}
提示

你可以使用 AuthorizationInterceptorsOrder 中指定的顺序常量,将你的拦截器放置在 Spring Security 方法拦截器之间。

自定义表达式处理

或者,第三,你可以自定义每个SpEL表达式的处理方式。为此,你可以暴露一个自定义的 MethodSecurityExpressionHandler,如下所示:

@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
return handler;
}
提示

我们通过 static 方法暴露 MethodSecurityExpressionHandler,以确保 Spring 在初始化 Spring Security 的方法安全 @Configuration 类之前发布它。

你也可以继承 DefaultMessageSecurityExpressionHandler,在默认表达式之外添加自定义的授权表达式。

使用 AOT

Spring Security 会扫描应用上下文中的所有 Bean,查找使用 @PreAuthorize@PostAuthorize 注解的方法。当找到这样的方法时,它会解析安全表达式中使用的所有 Bean,并为该 Bean 注册相应的运行时提示。如果发现某个方法使用了 @AuthorizeReturnObject 注解,它会递归地在该方法的返回类型中搜索 @PreAuthorize@PostAuthorize 注解,并相应地注册它们。

例如,考虑以下Spring Boot应用程序:

@Service
public class AccountService { 1

@PreAuthorize("@authz.decide()") 2
@AuthorizeReturnObject 3
public Account getAccountById(String accountId) {
// ...
}

}

public class Account {

private final String accountNumber;

// ...

@PreAuthorize("@accountAuthz.canViewAccountNumber()") 4
public String getAccountNumber() {
return this.accountNumber;
}

@AuthorizeReturnObject 5
public User getUser() {
return new User("John Doe");
}

}

public class User {

private final String fullName;

// ...

@PostAuthorize("@myOtherAuthz.decide()") 6
public String getFullName() {
return this.fullName;
}

}
  • Spring Security 找到 AccountService bean

  • 找到一个使用了 @PreAuthorize 的方法,它将解析表达式内部使用的任何 bean 名称(本例中是 authz),并为该 bean 类注册运行时提示

  • 找到一个使用了 @AuthorizeReturnObject 的方法,它将检查该方法的返回类型中是否有 @PreAuthorize@PostAuthorize

  • 然后,它找到另一个带有 bean 名称 accountAuthz@PreAuthorize;同样会为该 bean 类注册运行时提示

  • 找到另一个 @AuthorizeReturnObject,它将再次检查该方法的返回类型

  • 现在,找到一个使用了另一个 bean 名称 myOtherAuthz@PostAuthorize;同样会为该 bean 类注册运行时提示

在许多情况下,Spring Security 无法提前确定方法的实际返回类型,因为它可能隐藏在泛型擦除后的类型中。

考虑以下服务:

@Service
public class AccountService {

@AuthorizeReturnObject
public List<Account> getAllAccounts() {
// ...
}

}

在这种情况下,泛型类型被擦除,因此Spring Security无法提前明确需要访问Account类来检查@PreAuthorize@PostAuthorize注解。

为了解决这个问题,你可以发布一个 PrePostAuthorizeExpressionBeanHintsRegistrar,具体操作如下:

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static SecurityHintsRegistrar registerTheseToo() {
return new PrePostAuthorizeExpressionBeanHintsRegistrar(Account.class);
}

使用 AspectJ 进行授权

通过自定义切点匹配方法

基于Spring AOP构建,您可以声明与注解无关的模式,类似于请求级授权。这具有集中管理方法级授权规则的潜在优势。

例如,您可以发布自己的 Advisor 或使用 <protect-pointcut> 来匹配 AOP 表达式,从而为您的服务层应用授权规则,如下所示:

import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor protectServicePointcut() {
AspectJExpressionPointcut pattern = new AspectJExpressionPointcut()
pattern.setExpression("execution(* com.mycompany.*Service.*(..))")
return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER"))
}

集成 AspectJ 字节码编织

在某些情况下,通过使用AspectJ将Spring Security通知织入到Bean的字节码中,可以提升性能。

在设置好AspectJ之后,你可以很简单地在@EnableMethodSecurity注解或<method-security>元素中声明你正在使用AspectJ:

@EnableMethodSecurity(mode=AdviceMode.ASPECTJ)

结果将是,Spring Security 会将其通知器作为 AspectJ 通知发布,以便它们能够被相应地织入。

指定顺序

如前所述,每个注解都对应一个Spring AOP方法拦截器,并且每个拦截器在Spring AOP通知器链中都有一个特定的位置。

具体来说,@PreFilter 方法拦截器的顺序为 100,@PreAuthorize 的顺序为 200,依此类推。

您可以在 @EnableMethodSecurity 上使用 offset 参数,将所有拦截器整体移动,以便在方法调用的更早或更晚阶段提供其建议。

使用 SpEL 表达授权

你已经看到了几个使用SpEL的示例,现在让我们更深入地了解一下这个API。

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

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

首先,它提供了一套增强的授权字段和方法,可用于您的SpEL表达式。以下是这些最常见方法的快速概览:

  • permitAll - 调用该方法无需授权;请注意,在这种情况下,永远不会从会话中获取 Authentication

  • denyAll - 在任何情况下都不允许调用该方法;请注意,在这种情况下,永远不会从会话中获取 Authentication

  • hasAuthority - 该方法要求 Authentication 拥有与给定值匹配的 GrantedAuthority

  • hasRole - hasAuthority 的快捷方式,会自动添加 ROLE_ 前缀或配置的默认前缀

  • hasAnyAuthority - 该方法要求 Authentication 拥有与任意给定值匹配的 GrantedAuthority

  • hasAnyRole - hasAnyAuthority 的快捷方式,会自动添加 ROLE_ 前缀或配置的默认前缀

  • hasAllAuthorities - 该方法要求 Authentication 拥有与所有给定值匹配的 GrantedAuthority

  • hasAllRoles - hasAllAuthorities 的快捷方式,会自动添加 ROLE_ 前缀或配置的默认前缀

  • hasPermission - 连接到 PermissionEvaluator 实例的钩子,用于执行对象级授权

以下是几个最常见字段的简要介绍:

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

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

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

@Component
public class MyService {
@PreAuthorize("denyAll") 1
MyResource myDeprecatedMethod(...);

@PreAuthorize("hasRole('ADMIN')") 2
MyResource writeResource(...)

@PreAuthorize("hasAuthority('db') and hasRole('ADMIN')") 3
MyResource deleteResource(...)

@PreAuthorize("principal.claims['aud'] == 'my-audience'") 4
MyResource readResource(...);

@PreAuthorize("@authz.check(authentication, #root)")
MyResource shareResource(...);
}
  • 此方法禁止任何人在任何情况下调用

  • 此方法仅可由被授予 ROLE_ADMIN 权限的 Authentication 调用

  • 此方法仅可由被授予 dbROLE_ADMIN 权限的 Authentication 调用

  • 此方法仅可由 aud 声明等于 "my-audience" 的 Principal 调用

  • 仅当 bean authzcheck 方法返回 true 时,此方法才可被调用

提示

你可以使用像上面提到的 authz 这样的 bean 来添加编程式授权

使用方法参数

此外,Spring Security 还提供了一种发现方法参数的机制,使得这些参数也可以在 SpEL 表达式中被访问。

作为完整参考,Spring Security 使用 DefaultSecurityParameterNameDiscoverer 来发现参数名称。默认情况下,会为方法尝试以下选项。

  1. 如果 Spring Security 的 @P 注解出现在方法的单个参数上,则使用该值。以下示例使用 @P 注解:

    import org.springframework.security.access.method.P;

    ...

    @PreAuthorize("hasPermission(#c, 'write')")
    public void updateContact(@P("c") Contact contact);

    此表达式的意图是要求当前的 Authentication 对这个特定的 Contact 实例拥有 write 权限。

    在幕后,这是通过使用 AnnotationParameterNameDiscoverer 实现的,您可以自定义它以支持任何指定注解的 value 属性。

  2. 如果 Spring Data@Param 注解出现在方法的至少一个参数上,则使用该值。以下示例使用 @Param 注解:

    import org.springframework.data.repository.query.Param;

    ...

    @PreAuthorize("#n == authentication.name")
    Contact findContactByName(@Param("n") String name);

    此表达式的意图是要求 name 等于 Authentication#getName 才能授权调用。

    在幕后,这是通过使用 AnnotationParameterNameDiscoverer 实现的,您可以自定义它以支持任何指定注解的 value 属性。

  3. 如果您使用 -parameters 参数编译代码,则使用标准 JDK 反射 API 来发现参数名称。这适用于类和接口。

  4. 最后,如果您使用调试符号编译代码,则通过使用调试符号来发现参数名称。这不适用于接口,因为它们没有关于参数名称的调试信息。对于接口,必须使用注解或 -parameters 方法。

自定义授权管理器

当您将SpEL表达式与@PreAuthorize@PostAuthorize@PreFilter@PostFilter结合使用时,Spring Security会负责为您创建相应的AuthorizationManager实例。在某些情况下,您可能希望自定义创建过程,以便完全控制在框架层面如何做出授权决策。

为了控制为前置和后置注解创建 AuthorizationManager 实例的过程,你可以创建一个自定义的 AuthorizationManagerFactory。例如,假设你希望在任何需要其他角色时,都允许拥有 ADMIN 角色的用户。为此,你可以为方法安全创建一个自定义实现,如下例所示:

@Component
public class CustomMethodInvocationAuthorizationManagerFactory
implements AuthorizationManagerFactory<MethodInvocation> {

private final AuthorizationManagerFactory<MethodInvocation> delegate =
new DefaultAuthorizationManagerFactory<>();

@Override
public AuthorizationManager<MethodInvocation> hasRole(String role) {
return AuthorizationManagers.anyOf(
this.delegate.hasRole(role),
this.delegate.hasRole("ADMIN")
);
}

@Override
public AuthorizationManager<MethodInvocation> hasAnyRole(String... roles) {
return AuthorizationManagers.anyOf(
this.delegate.hasAnyRole(roles),
this.delegate.hasRole("ADMIN")
);
}

}

现在,每当你在使用 @PreAuthorize 注解时配合 hasRolehasAnyRole,Spring Security 将自动调用你的自定义工厂来创建一个 AuthorizationManager 实例,该实例允许访问给定角色 ADMIN 角色。

提示

我们以此作为创建自定义 AuthorizationManagerFactory 的简单示例,尽管通过角色层级也能实现相同效果。请根据实际情况选择最适合的方法。

授权任意对象

Spring Security 也支持包装任何带有方法安全注解的对象。

实现这一点的最简单方法是用 @AuthorizeReturnObject 注解标记任何返回你希望授权对象的方法。

例如,考虑以下 User 类:

public class User {
private String name;
private String email;

public User(String name, String email) {
this.name = name;
this.email = email;
}

public String getName() {
return this.name;
}

@PreAuthorize("hasAuthority('user:read')")
public String getEmail() {
return this.email;
}
}

给定如下接口:

public class UserRepository {
@AuthorizeReturnObject
Optional<User> findByName(String name) {
// ...
}
}

那么,任何从 findById 返回的 User 都将像其他受 Spring Security 保护的组件一样被安全处理:

@Autowired
UserRepository users;

@Test
void getEmailWhenProxiedThenAuthorizes() {
Optional<User> securedUser = users.findByName("name");
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> securedUser.get().getEmail());
}

在类级别使用 @AuthorizeReturnObject

@AuthorizeReturnObject 可以放置在类级别。但请注意,这意味着 Spring Security 将尝试代理任何返回对象,包括 StringInteger 等类型。这通常不是您想要的效果。

如果你想在类或接口上使用 @AuthorizeReturnObject,而这些类或接口的方法返回值类型为 intStringDouble 或这些类型的集合,那么你也应该发布相应的 AuthorizationAdvisorProxyFactory.TargetVisitor,如下所示:

import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;

// ...

@Bean
static TargetVisitor skipValueTypes() {
return TargetVisitor.defaultsSkipValueTypes();
}
提示

你可以设置自己的 AuthorizationAdvisorProxyFactory.TargetVisitor 来自定义对任意类型集合的代理行为

以编程方式代理

你也可以通过编程方式代理给定的对象。

为此,你可以自动装配提供的 AuthorizationProxyFactory 实例,该实例基于你已配置的方法安全拦截器。如果你正在使用 @EnableMethodSecurity,那么默认情况下,它将包含用于 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 的拦截器。

你可以通过以下方式代理一个用户实例:

@Autowired
AuthorizationProxyFactory proxyFactory;

@Test
void getEmailWhenProxiedThenAuthorizes() {
User user = new User("name", "email");
assertThat(user.getEmail()).isNotNull();
User securedUser = proxyFactory.proxy(user);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail);
}

手动构建

如果你需要不同于Spring Security默认配置的实例,也可以自定义自己的实例。

例如,如果你这样定义一个 AuthorizationProxyFactory 实例:

import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import static org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize;
// ...

AuthorizationProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
// and if needing to skip value types
proxyFactory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());

然后,你可以按如下方式包装任何 User 实例:

@Test
void getEmailWhenProxiedThenAuthorizes() {
AuthorizationProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
User user = new User("name", "email");
assertThat(user.getEmail()).isNotNull();
User securedUser = proxyFactory.proxy(user);
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail);
}

代理集合

AuthorizationProxyFactory 支持通过代理元素类型来处理 Java 集合、流、数组、可选类型和迭代器,并通过代理值类型来处理映射。

这意味着在代理一个 List 对象时,以下方法同样适用:

@Test
void getEmailWhenProxiedThenAuthorizes() {
AuthorizationProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
List<User> users = List.of(ada, albert, marie);
List<User> securedUsers = proxyFactory.proxy(users);
securedUsers.forEach((securedUser) ->
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail));
}

代理类

在有限的情况下,代理一个 Class 本身可能很有价值,AuthorizationProxyFactory 也支持这一点。这大致相当于在 Spring Framework 创建代理的支持中调用 ProxyFactory#getProxyClass

这在需要提前构建代理类时非常方便,例如在使用Spring AOT时。

对所有方法安全注解的支持

AuthorizationProxyFactory 支持应用程序中启用的任何方法安全注解。它基于所有作为 bean 发布的 AuthorizationAdvisor 类。

由于 @EnableMethodSecurity 默认会发布 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 的切面,因此通常你无需进行任何操作即可激活这些功能。

备注

使用 returnObjectfilterObject 的 SpEL 表达式位于代理之后,因此可以完全访问该对象。

自定义建议

如果你有希望应用的安全建议,可以像这样发布你自己的 AuthorizationAdvisor

@EnableMethodSecurity
class SecurityConfig {
@Bean
static AuthorizationAdvisor myAuthorizationAdvisor() {
return new AuthorizationAdvisor();
}
}

而Spring Security会将该通知器添加到AuthorizationProxyFactory在代理对象时添加的通知集合中。

使用 Jackson

该功能的一个强大用途是从控制器返回一个安全值,如下所示:

@RestController
public class UserController {
@Autowired
AuthorizationProxyFactory proxyFactory;

@GetMapping
User currentUser(@AuthenticationPrincipal User user) {
return this.proxyFactory.proxy(user);
}
}
@Component
public class Null implements MethodAuthorizationDeniedHandler {
@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return null;
}
}

// ...

@HandleAuthorizationDenied(handlerClass = Null.class)
public class User {
...
}

接着,根据用户的授权级别,你会看到不同的 JSON 序列化结果。如果用户没有 user:read 权限,那么他们将看到:

{
"name" : "name",
"email" : null
}

如果他们确实拥有该权限,他们将看到:

{
"name" : "name",
"email" : "email"
}
提示

你也可以添加 Spring Boot 属性 spring.jackson.default-property-inclusion=non_null 来排除序列化中的 null 值,如果你同样不希望向未授权用户暴露 JSON 键的话。

使用 AOT

Spring Security 会扫描应用上下文中的所有 Bean,查找使用 @AuthorizeReturnObject 注解的方法。当它找到一个这样的方法时,会提前创建并注册相应的代理类。同时,它还会递归地搜索其他同样使用了 @AuthorizeReturnObject 的嵌套对象,并相应地注册它们。

例如,考虑以下Spring Boot应用程序:

@SpringBootApplication
public class MyApplication {
@RestController
public static class MyController { 1
@GetMapping
@AuthorizeReturnObject
Message getMessage() { 2
return new Message(someUser, "hello!");
}
}

public static class Message { 3
User to;
String text;

// ...

@AuthorizeReturnObject
public User getTo() { 4
return this.to;
}

// ...
}

public static class User { 5
// ...
}

public static void main(String[] args) {
SpringApplication.run(MyApplication.class);
}
}
  • 首先,Spring Security 找到 MyController bean

  • 发现一个使用了 @AuthorizeReturnObject 的方法,它代理了返回值 Message,并将该代理类注册到 RuntimeHints

  • 然后,它遍历 Message 以检查是否使用了 @AuthorizeReturnObject

  • 发现一个使用了 @AuthorizeReturnObject 的方法,它代理了返回值 User,并将该代理类注册到 RuntimeHints

  • 最后,它遍历 User 以检查是否使用了 @AuthorizeReturnObject;未发现任何使用,算法完成

很多时候,Spring Security 无法提前确定代理类,因为它可能隐藏在泛型擦除的类型中。

考虑对 MyController 进行的以下更改:

@RestController
public static class MyController {
@GetMapping
@AuthorizeReturnObject
List<Message> getMessages() {
return List.of(new Message(someUser, "hello!"));
}
}

在这种情况下,泛型类型被擦除,因此Spring Security无法提前预知Message在运行时需要被代理。

为此,你可以像这样发布 AuthorizeProxyFactoryHintsRegistrar

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static SecurityHintsRegsitrar registerTheseToo(AuthorizationProxyFactory proxyFactory) {
return new AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message.class);
}

Spring Security 将注册该类,然后像之前一样遍历其类型。

当授权被拒绝时提供后备值

在某些场景下,当方法在没有所需权限的情况下被调用时,您可能不希望抛出 AuthorizationDeniedException。相反,您可能希望返回一个经过后处理的结果,例如一个脱敏后的结果,或者在调用方法之前发生授权拒绝时返回一个默认值。

Spring Security 通过使用 @HandleAuthorizationDenied 注解,为处理方法调用时的授权拒绝提供了支持。该处理器适用于发生在 @PreAuthorize 和 @PostAuthorize 注解 中的授权拒绝,也适用于方法调用本身抛出的 AuthorizationDeniedException

让我们考虑上一节中的示例,但这次我们不创建 AccessDeniedExceptionInterceptor 来将 AccessDeniedException 转换为 null 返回值,而是使用 @HandleAuthorizationDenied 注解的 handlerClass 属性:

public class NullMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { 1

@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return null;
}

}

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

@Bean 2
public NullMethodAuthorizationDeniedHandler nullMethodAuthorizationDeniedHandler() {
return new NullMethodAuthorizationDeniedHandler();
}

}

public class User {
// ...

@PreAuthorize(value = "hasAuthority('user:read')")
@HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler.class)
public String getEmail() {
return this.email;
}
}
  • 创建一个返回 null 值的 MethodAuthorizationDeniedHandler 实现

  • NullMethodAuthorizationDeniedHandler 注册为 Bean

  • 使用 @HandleAuthorizationDenied 注解方法,并将 NullMethodAuthorizationDeniedHandler 传递给 handlerClass 属性

然后你可以验证返回的是一个 null 值,而不是 AccessDeniedException

提示

你也可以使用 @Component 注解来标注你的类,而不是创建一个 @Bean 方法

@Autowired
UserRepository users;

@Test
void getEmailWhenProxiedThenNullEmail() {
Optional<User> securedUser = users.findByName("name");
assertThat(securedUser.get().getEmail()).isNull();
}

使用方法调用返回的拒绝结果

在某些场景下,你可能希望返回一个基于被拒绝结果的安全衍生结果。例如,如果用户未被授权查看电子邮件地址,你可能希望对原始电子邮件地址应用一些掩码处理,即 useremail@example.com 会变成 use******@example.com

对于这些场景,你可以重写 MethodAuthorizationDeniedHandler 中的 handleDeniedInvocationResult 方法,该方法以 MethodInvocationResult 作为参数。让我们继续之前的例子,但这次不返回 null,而是返回一个经过掩码处理的邮箱值:

public class EmailMaskingMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { 1

@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return "***";
}

@Override
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) {
String email = (String) methodInvocationResult.getResult();
return email.replaceAll("(^[^@]{3}|(?!^)\\G)[^@]", "$1*");
}

}

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

@Bean 2
public EmailMaskingMethodAuthorizationDeniedHandler emailMaskingMethodAuthorizationDeniedHandler() {
return new EmailMaskingMethodAuthorizationDeniedHandler();
}

}

public class User {
// ...

@PostAuthorize(value = "hasAuthority('user:read')")
@HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler.class)
public String getEmail() {
return this.email;
}
}
  • 创建一个 MethodAuthorizationDeniedHandler 的实现,返回未经授权的结果值的掩码版本

  • EmailMaskingMethodAuthorizationDeniedHandler 注册为一个 bean

  • 使用 @HandleAuthorizationDenied 注解该方法,并将 EmailMaskingMethodAuthorizationDeniedHandler 传递给 handlerClass 属性

然后你可以验证返回的是一个经过掩码处理的邮箱,而不是 AccessDeniedException

:::警告
由于您可以访问原始的拒绝值,请确保正确处理它,不要将其返回给调用者。
:::

@Autowired
UserRepository users;

@Test
void getEmailWhenProxiedThenMaskedEmail() {
Optional<User> securedUser = users.findByName("name");
// email is useremail@example.com
assertThat(securedUser.get().getEmail()).isEqualTo("use******@example.com");
}

在实现 MethodAuthorizationDeniedHandler 时,你可以选择返回以下几种类型:

  • 一个 null 值。

  • 一个非空值,需符合方法的返回类型。

  • 抛出一个异常,通常是 AuthorizationDeniedException 的实例。这是默认行为。

  • 对于响应式应用,返回一个 Mono 类型。

请注意,由于处理器必须在应用程序上下文中注册为Bean,因此如果需要更复杂的逻辑,可以向它们注入依赖项。除此之外,您还可以使用 MethodInvocationMethodInvocationResult,以及 AuthorizationResult 来获取与授权决策相关的更多详细信息。

基于可用参数决定返回内容

考虑这样一种场景:不同的方法可能需要不同的掩码值。虽然为每个方法单独创建处理程序完全可行,但这并非最高效的做法。在这种情况下,我们可以利用通过参数传递的信息来决定具体操作。例如,我们可以创建一个自定义的 @Mask 注解,并编写一个能检测该注解的处理程序,从而决定返回何种掩码值:

import org.springframework.core.annotation.AnnotationUtils;

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Mask {

String value();

}

public class MaskAnnotationDeniedHandler implements MethodAuthorizationDeniedHandler {

@Override
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
return mask.value();
}

}

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

@Bean
public MaskAnnotationDeniedHandler maskAnnotationDeniedHandler() {
return new MaskAnnotationDeniedHandler();
}

}

@Component
public class MyService {

@PreAuthorize(value = "hasAuthority('user:read')")
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
@Mask("***")
public String foo() {
return "foo";
}

@PreAuthorize(value = "hasAuthority('user:read')")
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
@Mask("???")
public String bar() {
return "bar";
}

}

现在,当访问被拒绝时,返回值将根据 @Mask 注解来决定:

@Autowired
MyService myService;

@Test
void fooWhenDeniedThenReturnStars() {
String value = this.myService.foo();
assertThat(value).isEqualTo("***");
}

@Test
void barWhenDeniedThenReturnQuestionMarks() {
String value = this.myService.foo();
assertThat(value).isEqualTo("???");
}

结合元注解支持

您还可以将 @HandleAuthorizationDenied 与其他注解结合使用,以减少和简化方法中的注解。让我们考虑上一节的示例,并将 @HandleAuthorizationDenied@Mask 合并:

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
public @interface Mask {

String value();

}

@Mask("***")
public String myMethod() {
// ...
}

现在,当您需要在方法中使用掩码行为时,无需再记住同时添加两个注解。请务必阅读 元注解支持 部分,以获取有关用法的更多详细信息。

@EnableGlobalMethodSecurity 迁移

如果你正在使用 @EnableGlobalMethodSecurity,应该迁移到 @EnableMethodSecurity

如果您目前无法迁移,请将 spring-security-access 模块作为依赖项包含进来,如下所示:

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-access</artifactId>
</dependency>

<global-method-security> 替换为 <method-security>

@EnableGlobalMethodSecurity<global-method-security> 已被弃用,建议分别改用 @EnableMethodSecurity<method-security>。新的注解和 XML 元素默认启用 Spring 的前置-后置注解,并在内部使用 AuthorizationManager

这意味着以下两个代码清单在功能上是等价的:

@EnableGlobalMethodSecurity(prePostEnabled = true)

和:

@EnableMethodSecurity

对于未使用前置后置注解的应用程序,请确保将其关闭,以避免触发不必要的行为。

例如,一个列表如下:

@EnableGlobalMethodSecurity(securedEnabled = true)

应更改为:

@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)

使用自定义 @Bean 替代子类化 DefaultMethodSecurityExpressionHandler

作为一项性能优化,MethodSecurityExpressionHandler 引入了一个新方法,该方法接收一个 Supplier<Authentication> 参数,而不是直接接收 Authentication

这使得Spring Security能够推迟对Authentication的查找,并且当你使用@EnableMethodSecurity而非@EnableGlobalMethodSecurity时,这一特性会被自动利用。

然而,假设你的代码继承了 DefaultMethodSecurityExpressionHandler 并重写了 createSecurityExpressionRoot(Authentication, MethodInvocation) 方法,以返回一个自定义的 SecurityExpressionRoot 实例。这种做法将不再有效,因为 @EnableMethodSecurity 所设置的配置会调用 createEvaluationContext(Supplier<Authentication>, MethodInvocation) 方法,而非前者。

幸运的是,通常并不需要如此高度的定制化。相反,您可以创建一个包含所需授权方法的自定义 Bean。

例如,假设你需要对 @PostAuthorize("hasAuthority('ADMIN')") 进行自定义评估。你可以创建一个自定义的 @Bean,如下所示:

class MyAuthorizer {
boolean isAdmin(MethodSecurityExpressionOperations root) {
boolean decision = root.hasAuthority("ADMIN");
// custom work ...
return decision;
}
}

然后在注解中像这样引用它:

@PreAuthorize("@authz.isAdmin(#root)")

我仍然倾向于继承 DefaultMethodSecurityExpressionHandler

如果你必须继续子类化 DefaultMethodSecurityExpressionHandler,仍然可以这样做。只需像这样重写 createEvaluationContext(Supplier<Authentication>, MethodInvocation) 方法:

@Component
class MyExpressionHandler extends DefaultMethodSecurityExpressionHandler {
@Override
public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication, MethodInvocation mi) {
StandardEvaluationContext context = (StandardEvaluationContext) super.createEvaluationContext(authentication, mi);
MethodSecurityExpressionOperations delegate = (MethodSecurityExpressionOperations) context.getRootObject().getValue();
MySecurityExpressionRoot root = new MySecurityExpressionRoot(delegate);
context.setRootObject(root);
return context;
}
}

延伸阅读

现在你已经保护了应用程序的请求,如果尚未完成,请保护其请求。你也可以进一步阅读关于测试应用程序的内容,或者了解如何将 Spring Security 与其他方面集成,例如数据层追踪与指标