方法安全性
方法安全性
- 方法安全的工作原理
- 多个注解按顺序计算
- 不支持重复的注解
- 每个注解都有自己的切点
- 每个注解都有自己的方法拦截器
- 优先授予权限而不是复杂的 SpEL 表达式
- 比较请求级与方法级授权
- 使用注解进行授权
- 使用 @PreAuthorize 授权方法调用
- 使用 @PostAuthorize 授权方法结果
- 使用 @PreFilter 过滤方法参数
- 使用 @PostFilter 过滤方法结果
- 使用 @Secured 授权方法调用
- 使用 JSR-250 注解授权方法调用
- 在类或接口级别声明注解
- 使用元注解
- 启用某些注解
- 使用 <intercept-methods> 进行授权
- 以编程方式授权方法
- 在 SpEL 中使用自定义 Bean
- 使用自定义授权管理器
- 自定义表达式处理
- 与 AOT 协同工作
- 使用 AspectJ 进行授权
- 使用自定义切点匹配方法
- 集成 AspectJ 字节编织
- 指定顺序
- 使用 SpEL 表达授权
- 使用授权表达式的字段和方法
- 使用方法参数
- 授权任意对象
- 在类级别使用 @AuthorizeReturnObject
- 以编程方式代理
- 手动构建
- 代理集合
- 代理类
- 支持所有方法安全注解
- 自定义通知
- 与 Jackson 协同工作
- 与 AOT 协同工作
- 在授权被拒绝时提供回退值
- 使用方法调用的拒绝结果
- 基于可用参数决定返回什么
- 结合元注解支持
- 从 @EnableGlobalMethodSecurity 迁移
- 将全局方法安全替换为方法安全
- 使用自定义 @Bean 而不是继承 DefaultMethodSecurityExpressionHandler
- 进一步阅读
除了在请求级别建模授权之外,Spring Security 还支持在方法级别进行建模。
您可以在应用程序中通过在任何 @Configuration 类上添加 @EnableMethodSecurity 注解或在任何 XML 配置文件中添加 <method-security> 来激活它,如下所示:
- Java
- Kotlin
- Xml
@EnableMethodSecurity
@EnableMethodSecurity
<sec:method-security/>
然后,您可以立即使用 @PreAuthorize、@PostAuthorize、@PreFilter 和 @PostFilter 来注解任何 Spring 管理的类或方法,以授权方法调用,包括输入参数和返回值。
Spring Boot Starter Security 默认不激活方法级别的授权。
方法安全性还支持许多其他用例,包括 AspectJ 支持、自定义注解 以及几个配置点。请考虑了解以下用例:
- 
了解方法安全的工作原理及其使用原因 
- 
使用 @PreAuthorize 和 @PostAuthorize 授权方法 
- 
在授权被拒绝时提供后备值 
- 
使用 @PreFilter 和 @PostFilter 过滤方法 
- 
使用 JSR-250 注解授权方法 
- 
使用 AspectJ 表达式授权方法 
- 
与 AspectJ 字节码编织集成 
- 
自定义 SpEL 表达式处理 
- 
与 自定义授权系统集成 
方法安全性的工作原理
Spring Security的方法级授权支持适用于以下情况:
- 
提取细粒度的授权逻辑;例如,当方法参数和返回值对授权决策有贡献时。 
- 
在服务层实施安全性 
- 
从风格上偏好基于注解的配置而非基于 HttpSecurity的配置
并且由于方法安全性是使用 Spring AOP 构建的,你可以利用其所有的表达能力来根据需要覆盖 Spring Security 的默认设置。
正如前面提到的,你需要在 @Configuration 类中添加 @EnableMethodSecurity,或在 Spring XML 配置文件中添加 <sec:method-security/>。
此注解和 XML 元素分别取代了 @EnableGlobalMethodSecurity 和 <sec:global-method-security/>。它们提供了以下改进:
- 
使用简化的 AuthorizationManagerAPI,而不是元数据源、配置属性、决策管理器和投票器。这简化了重用和自定义。
- 
倾向于直接基于 bean 的配置,而不是要求扩展 GlobalMethodSecurityConfiguration来自定义 bean。
- 
使用原生的 Spring AOP 构建,移除了抽象层,并允许你使用 Spring AOP 构建块来自定义。 
- 
检查冲突的注解,以确保安全配置不模糊。 
- 
遵循 JSR-250。 
- 
默认启用 @PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter。
如果你正在使用 @EnableGlobalMethodSecurity 或 <global-method-security/>,这些现在已被弃用,建议你进行迁移。
方法授权是前置方法授权和后置方法授权的结合。考虑一个服务 Bean,其注解方式如下:
- Java
- Kotlin
@Service
public class MyCustomerService {
    @PreAuthorize("hasAuthority('permission:read')")
    @PostAuthorize("returnObject.owner == authentication.name")
    public Customer readCustomer(String id) { ... }
}
@Service
open class MyCustomerService {
    @PreAuthorize("hasAuthority('permission:read')")
    @PostAuthorize("returnObject.owner == authentication.name")
    fun readCustomer(val id: String): Customer { ... }
}
对 MyCustomerService#readCustomer 的调用在方法安全被激活时可能看起来像这样:

- 
Spring AOP 为 readCustomer调用其代理方法。在代理的其他 advisor 中,它调用了一个与 the @PreAuthorize pointcut 匹配的 AuthorizationManagerBeforeMethodInterceptor。
- 
授权管理器使用 MethodSecurityExpressionHandler解析注解的 SpEL 表达式,并从包含 a Supplier<Authentication>和MethodInvocation的MethodSecurityExpressionRoot构建相应的EvaluationContext。
- 
拦截器使用此上下文来评估表达式;具体来说,它从 Supplier中读取 the Authentication,并检查其 authorities 集合中是否包含permission:read。
- 
如果评估通过,则 Spring AOP 继续调用该方法。 
- 
如果不通过,拦截器发布一个 AuthorizationDeniedEvent并抛出一个 AccessDeniedException,the ExceptionTranslationFilter 捕获该异常并向响应返回 403 状态码。
- 
在方法返回后,Spring AOP 调用一个与 the @PostAuthorize pointcut 匹配的 AuthorizationManagerAfterMethodInterceptor,其操作方式与上述相同,但使用的是 PostAuthorizeAuthorizationManager。 
- 
如果评估通过(在这种情况下,返回值属于已登录用户),则处理继续正常进行。 
- 
如果不通过,拦截器发布一个 AuthorizationDeniedEvent并抛出一个 AccessDeniedException,the ExceptionTranslationFilter 捕获该异常并向响应返回 403 状态码。
如果该方法不是在 HTTP 请求的上下文中被调用,你可能需要自己处理 AccessDeniedException
多个注解按顺序计算
如上所示,如果一个方法调用涉及多个方法安全注解,则这些注解会逐一进行处理。这意味着可以将它们整体视为是“与”在一起的。换句话说,为了使调用被授权,所有注解检查都需要通过授权。
不支持重复注解
也就是说,不支持在同一个方法上重复相同的注解。例如,你不能在同一个方法上放置两次 @PreAuthorize。
相反,使用 SpEL 的布尔支持或其委托给单独 bean 的支持。
每个注解都有其自己的切入点
每个注解都有自己的方法拦截器
每个注解都有其自己的专用方法拦截器。这样做的原因是使事情更具可组合性。例如,如果需要,你可以禁用 Spring Security 的默认设置,并仅发布 @PostAuthorize 方法拦截器。
方法拦截器如下:
- 
对于 @PreAuthorize,Spring Security 使用 AuthorizationManagerBeforeMethodInterceptor#preAuthorize,它又使用了 PreAuthorizeAuthorizationManager 
- 
对于 @PostAuthorize,Spring Security 使用 AuthorizationManagerAfterMethodInterceptor#postAuthorize,它又使用了 PostAuthorizeAuthorizationManager 
- 
对于 @PreFilter,Spring Security 使用 PreFilterAuthorizationMethodInterceptor 
- 
对于 @PostFilter,Spring Security 使用 PostFilterAuthorizationMethodInterceptor 
- 
对于 @Secured,Spring Security 使用 AuthorizationManagerBeforeMethodInterceptor#secured,它又使用了 SecuredAuthorizationManager 
- 
对于 JSR-250 注解,Spring Security 使用 AuthorizationManagerBeforeMethodInterceptor#jsr250,它又使用了 Jsr250AuthorizationManager 
一般来说,当你添加 @EnableMethodSecurity 时,可以将以下列表视为 Spring Security 发布的拦截器的代表:
- Java
@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表达式的诱惑:
- Java
@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')")
@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')")
但是,你可以改为向具有 ROLE_ADMIN 的用户授予 permission:read。一种方法是使用如下所示的 RoleHierarchy:
- Java
- Kotlin
- Xml
@Bean
static RoleHierarchy roleHierarchy() {
    return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > permission:read");
}
companion object {
    @Bean
    fun roleHierarchy(): RoleHierarchy {
        return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > permission:read")
    }
}
<bean id="roleHierarchy"
        class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl" factory-method="fromHierarchy">
    <constructor-arg value="ROLE_ADMIN > permission:read"/>
</bean>
然后将其设置在 MethodSecurityExpressionHandler 实例 中。这样你就可以使用更简单的 @PreAuthorize 表达式,如下所示:
- Java
- Kotlin
@PreAuthorize("hasAuthority('permission:read')")
@PreAuthorize("hasAuthority('permission:read')")
或者,在可能的情况下,将特定于应用程序的授权逻辑在登录时适配为授予的权限。
比较请求级与方法级授权
你应该在什么情况下优先选择方法级授权而不是请求级授权?这在一定程度上取决于个人偏好;然而,考虑以下每种方式的优点列表,以帮助你做出决定。
| request-level | 方法级 | |
|---|---|---|
| 授权类型 | 粗粒度 | 细粒度 | 
| 配置位置 | 在配置类中声明 | 在方法声明本地 | 
| 配置风格 | DSL | 注解 | 
| 授权定义 | 编程方式 | SpEL | 
主要的权衡点似乎在于你希望你的授权规则存放在哪里。
重要的是要记住,当你使用基于注解的方法安全时,未注解的方法是不安全的。为了防止这种情况,在你的 HttpSecurity 实例中声明一个通用授权规则。
使用注解授权
Spring Security 主要通过可以添加到方法、类和接口上的注解来实现方法级别的授权支持。
使用 @PreAuthorize 授权方法调用
当方法安全被激活时,你可以使用@PreAuthorize注解来标注一个方法,如下所示:
- Java
- Kotlin
@Component
public class BankService {
    @PreAuthorize("hasRole('ADMIN')")
    public Account readAccount(Long id) {
        // ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority
    }
}
@Component
open class BankService {
    @PreAuthorize("hasRole('ADMIN')")
    fun readAccount(val id: Long): Account {
        // ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority
    }
}
这表示该方法只有在提供的表达式 hasRole('ADMIN') 通过时才能被调用。
然后,你可以测试该类以确认它是否像这样强制执行授权规则:
- Java
- Kotlin
@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"));
}
@WithMockUser(roles="ADMIN")
@Test
fun readAccountWithAdminRoleThenInvokes() {
    val account: Account = this.bankService.readAccount("12345678")
    // ... assertions
}
@WithMockUser(roles="WRONG")
@Test
fun readAccountWithWrongRoleThenAccessDenied() {
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
        this.bankService.readAccount("12345678")
    }
}
@PreAuthorize 也可以是一个 元注解,可以在 类或接口级别 定义,并且可以使用 SpEL 授权表达式。
虽然 @PreAuthorize 在声明所需的权限时非常有用,但它也可以用于评估涉及方法参数的更复杂的表达式。
使用 @PostAuthorize 的授权方法结果
当方法安全处于激活状态时,你可以使用 @PostAuthorize 注解标注一个方法,如下所示:
- Java
- Kotlin
@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
    }
}
@Component
open class BankService {
    @PostAuthorize("returnObject.owner == authentication.name")
    fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
    }
}
这表示该方法只有在提供的表达式 returnObject.owner == authentication.name 通过时才能返回值。returnObject 代表要返回的 Account 对象。
然后,您可以测试该类以确认其正在执行授权规则:
- Java
- Kotlin
@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"));
}
@WithMockUser(username="owner")
@Test
fun readAccountWhenOwnedThenReturns() {
    val account: Account = this.bankService.readAccount("12345678")
    // ... assertions
}
@WithMockUser(username="wrong")
@Test
fun readAccountWhenNotOwnedThenAccessDenied() {
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
        this.bankService.readAccount("12345678")
    }
}
@PostAuthorize 也可以是一个 元注解,可以在 类或接口级别 定义,并且可以使用 SpEL 授权表达式。
@PostAuthorize 在防御不安全的直接对象引用时特别有用。实际上,它可以被定义为一个元注解,如下所示:
- Java
- Kotlin
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PostAuthorize("returnObject.owner == authentication.name")
public @interface RequireOwnership {}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PostAuthorize("returnObject.owner == authentication.name")
annotation class RequireOwnership
允许你以如下方式注解服务:
- Java
- Kotlin
@Component
public class BankService {
    @RequireOwnership
    public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
    }
}
@Component
open class BankService {
    @RequireOwnership
    fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
    }
}
结果是,上述方法只有在 Account 的 owner 属性与登录用户的 name 匹配时才会返回该 Account。如果不匹配,Spring Security 将抛出一个 AccessDeniedException 并返回 403 状态码。
使用 @PreFilter 过滤方法参数
当方法安全处于激活状态时,你可以使用 @PreFilter 注解来标注一个方法,如下所示:
- Java
- Kotlin
@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;
    }
}
@Component
open class BankService {
    @PreFilter("filterObject.owner == authentication.name")
    fun updateAccounts(vararg accounts: Account): Collection<Account> {
        // ... `accounts` will only contain the accounts owned by the logged-in user
        return updated
    }
}
这旨在过滤掉 accounts 中所有不符合 filterObject.owner == authentication.name 表达式的值。filterObject 代表 accounts 中的每个 account,并用于测试每个 account。
然后,您可以以下列方式测试该类,以确认它正在执行授权规则:
- Java
- Kotlin
@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);
}
@Autowired
lateinit var bankService: BankService
@WithMockUser(username="owner")
@Test
fun updateAccountsWhenOwnedThenReturns() {
    val ownedBy: Account = ...
    val notOwnedBy: Account = ...
    val updated: Collection<Account> = bankService.updateAccounts(ownedBy, notOwnedBy)
    assertThat(updated).containsOnly(ownedBy)
}
@PreFilter 也可以是一个 元注解,可以在 类或接口级别 定义,并且可以使用 SpEL 授权表达式。
@PreFilter 支持数组、集合、映射和流(只要流仍然处于打开状态)。
例如,上述 updateAccounts 声明将与以下四种方式的功能相同:
- Java
- Kotlin
@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)
@PreFilter("filterObject.owner == authentication.name")
fun updateAccounts(accounts: Array<Account>): Collection<Account>
@PreFilter("filterObject.owner == authentication.name")
fun updateAccounts(accounts: Collection<Account>): Collection<Account>
@PreFilter("filterObject.value.owner == authentication.name")
fun updateAccounts(accounts: Map<String, Account>): Collection<Account>
@PreFilter("filterObject.owner == authentication.name")
fun updateAccounts(accounts: Stream<Account>): Collection<Account>
结果是,上述方法将只包含 owner 属性与登录用户的 name 匹配的 Account 实例。
使用 @PostFilter 过滤方法结果
当方法安全处于激活状态时,你可以使用 @PostFilter 注解来标注一个方法,如下所示:
- Java
- Kotlin
@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;
    }
}
@Component
open class BankService {
    @PreFilter("filterObject.owner == authentication.name")
    fun readAccounts(vararg ids: String): Collection<Account> {
        // ... 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。
然后你可以像下面这样测试该类,以确认它是否强制执行了授权规则:
- Java
- Kotlin
@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");
}
@Autowired
lateinit var bankService: BankService
@WithMockUser(username="owner")
@Test
fun readAccountsWhenOwnedThenReturns() {
    val accounts: Collection<Account> = bankService.updateAccounts("owner", "not-owner")
    assertThat(accounts).hasSize(1)
    assertThat(accounts[0].owner).isEqualTo("owner")
}
@PostFilter 也可以是一个 元注解,可以在 类或接口级别 定义,并且可以使用 SpEL 授权表达式。
@PostFilter 支持数组、集合、映射和流(只要流仍然打开)。
例如,上面的 readAccounts 声明将与以下其他三种声明具有相同的功能:
- Java
- Kotlin
@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)
@PostFilter("filterObject.owner == authentication.name")
fun readAccounts(vararg ids: String): Collection<Account>
@PostFilter("filterObject.owner == authentication.name")
fun readAccounts(vararg ids: String): Array<Account>
@PostFilter("filterObject.owner == authentication.name")
fun readAccounts(vararg ids: String): Map<String, Account>
@PostFilter("filterObject.owner == authentication.name")
fun readAccounts(vararg ids: String): Stream<Account>
结果是,上述方法将返回 owner 属性与登录用户的 name 匹配的 Account 实例。
在内存中进行过滤显然可能会很耗费资源,因此请考虑是否最好在数据层中对数据进行过滤。
使用 @Secured 授权方法调用
@Secured 是一种用于授权调用的遗留选项。推荐使用 @PreAuthorize 来替代它。
要使用 @Secured 注解,你应该首先更改你的方法安全声明以启用它,如下所示:
- Java
- Kotlin
- Xml
@EnableMethodSecurity(securedEnabled = true)
@EnableMethodSecurity(securedEnabled = true)
<sec:method-security secured-enabled="true"/>
这将导致 Spring Security 发布相应的方法拦截器,该拦截器授权用 @Secured 注解的方法、类和接口。
使用 JSR-250 注解授权方法调用
如果您想使用 JSR-250 注解,Spring Security 也支持这一点。@PreAuthorize 具有更强的表达能力,因此推荐使用。
要使用 JSR-250 注解,你应该首先像这样更改你的方法安全声明:
- Java
- Kotlin
- Xml
@EnableMethodSecurity(jsr250Enabled = true)
@EnableMethodSecurity(jsr250Enabled = true)
<sec:method-security jsr250-enabled="true"/>
这将导致 Spring Security 发布相应的方法拦截器,该拦截器授权用 @RolesAllowed、@PermitAll 和 @DenyAll 注解的方法、类和接口。
在类或接口级别声明注解
在类和接口级别使用方法安全注解也是支持的。
如果它在类级别,如下所示:
- Java
- Kotlin
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
    @GetMapping("/endpoint")
    public String endpoint() { ... }
}
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
open class MyController {
    @GetMapping("/endpoint")
    fun endpoint(): String { ... }
}
那么所有方法都会继承类级别的行为。
或者,如果在类和方法级别都像下面这样声明:
- Java
- Kotlin
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
    @GetMapping("/endpoint")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public String endpoint() { ... }
}
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
open class MyController {
    @GetMapping("/endpoint")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    fun endpoint(): String { ... }
}
则声明该注解的方法会覆盖类级别的注解。
对于接口来说也是如此,不同之处在于,如果一个类从两个不同的接口继承了注解,那么启动将会失败。这是因为 Spring Security 无法判断你想要使用哪一个。
在这种情况下,你可以通过将注解添加到具体方法来解决歧义。
使用元注解
方法安全性支持元注解。这意味着你可以采用任何注解,并根据你的特定应用程序用例来提高可读性。
例如,你可以将 @PreAuthorize("hasRole('ADMIN')") 简化为 @IsAdmin,如下所示:
- Java
- Kotlin
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
annotation class IsAdmin
结果是,现在您可以在受保护的方法中执行以下操作:
- Java
- Kotlin
@Component
public class BankService {
    @IsAdmin
    public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
    }
}
@Component
open class BankService {
    @IsAdmin
    fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
    }
}
这使得方法定义更具可读性。
模板元注解表达式
您还可以选择使用元注解模板,这允许定义更强大的注解。
首先,发布以下bean:
- Java
- Kotlin
@Bean
static AnnotationTemplateExpressionDefaults templateExpressionDefaults() {
    return new AnnotationTemplateExpressionDefaults();
}
companion object {
    @Bean
    fun templateExpressionDefaults(): AnnotationTemplateExpressionDefaults {
        return AnnotationTemplateExpressionDefaults()
    }
}
现在,你可以创建一个更强大的 @HasRole,而不是使用 @IsAdmin,如下所示:
- Java
- Kotlin
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('{value}')")
public @interface HasRole {
    String value();
}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('{value}')")
annotation class HasRole(val value: String)
结果是,现在你可以在受保护的方法中执行以下操作:
- Java
- Kotlin
@Component
public class BankService {
    @HasRole("ADMIN")
    public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
    }
}
@Component
open class BankService {
    @HasRole("ADMIN")
    fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
    }
}
请注意,这也可以用于方法变量和所有注解类型,但您需要注意正确处理引号,以使生成的 SpEL 表达式正确。
例如,考虑以下 @HasAnyRole 注解:
- Java
- Kotlin
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole({roles})")
public @interface HasAnyRole {
    String[] roles();
}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole({roles})")
annotation class HasAnyRole(val roles: Array<String>)
在这种情况下,你会注意到不应该在表达式中使用引号,而应该在参数值中使用,如下所示:
- Java
- Kotlin
@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
    }
}
@Component
open class BankService {
    @HasAnyRole(roles = arrayOf("'USER'", "'ADMIN'"))
    fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
    }
}
这样,替换之后,表达式就变成了 @PreAuthorize("hasAnyRole('USER', 'ADMIN')")。
启用某些注释
你可以关闭 @EnableMethodSecurity 的预配置,并用你自己的配置来替换。如果你想要自定义授权管理器或 Pointcut,或者你只想启用特定的注解(如 @PostAuthorize),则可以选择这样做。
你可以通过以下方式实现:
- Java
- Kotlin
- Xml
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    Advisor postAuthorize() {
        return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
    }
}
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    fun postAuthorize() : Advisor {
        return AuthorizationManagerAfterMethodInterceptor.postAuthorize()
    }
}
<sec:method-security pre-post-enabled="false"/>
<aop:config/>
<bean id="postAuthorize"
    class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
    factory-method="postAuthorize"/>
上述代码片段首先禁用方法安全的预配置,然后发布the @PostAuthorize 拦截器本身。
使用 <intercept-methods> 授权
虽然使用 Spring Security 的基于注解的支持是方法安全的首选,但你也可以使用 XML 来声明 bean 授权规则。
如果你需要在XML配置中声明它,你可以像这样使用<intercept-methods>:
- Xml
<bean class="org.mycompany.MyController">
    <intercept-methods>
        <protect method="get*" access="hasAuthority('read')"/>
        <protect method="*" access="hasAuthority('write')"/>
    </intercept-methods>
</bean>
这仅支持通过前缀或名称进行匹配的方法。如果你的需求比这更复杂,请改用注解支持。
通过编程方式授权方法
正如你已经看到的,你可以使用方法安全 SpEL 表达式来指定复杂的授权规则。
有多种方法可以让你的逻辑基于Java而不是基于SpEL。这使得我们可以使用整个Java语言,从而提高可测试性和流程控制。
在 SpEL 中使用自定义 Bean
程序化授权方法的第一种方式是一个两步过程。
首先,声明一个具有如下方法的 bean,该方法接收一个 MethodSecurityExpressionOperations 实例:
- Java
- Kotlin
@Component("authz")
public class AuthorizationLogic {
    public boolean decide(MethodSecurityExpressionOperations operations) {
        // ... authorization logic
    }
}
@Component("authz")
open class AuthorizationLogic {
    fun decide(val operations: MethodSecurityExpressionOperations): boolean {
        // ... authorization logic
    }
}
然后,在注解中以如下方式引用该 bean:
- Java
- Kotlin
@Controller
public class MyController {
    @PreAuthorize("@authz.decide(#root)")
    @GetMapping("/endpoint")
    public String endpoint() {
        // ...
    }
}
@Controller
open class MyController {
    @PreAuthorize("@authz.decide(#root)")
    @GetMapping("/endpoint")
    fun String endpoint() {
        // ...
    }
}
Spring Security 将在每次方法调用时调用该 bean 上的给定方法。
这样做的好处是,所有的授权逻辑都在一个独立的类中,可以独立进行单元测试并验证其正确性。它还可以访问完整的 Java 语言。
除了返回一个 Boolean,您还可以返回 null 来表示代码不做出决定。
如果你想要包含更多关于决策性质的信息,你可以返回一个自定义的 AuthorizationDecision,像这样:
- Java
- Kotlin
@Component("authz")
public class AuthorizationLogic {
    public AuthorizationDecision decide(MethodSecurityExpressionOperations operations) {
        // ... authorization logic
        return new MyAuthorizationDecision(false, details);
    }
}
@Component("authz")
open class AuthorizationLogic {
    fun decide(val operations: MethodSecurityExpressionOperations): AuthorizationDecision {
        // ... authorization logic
        return MyAuthorizationDecision(false, details)
    }
}
或者抛出一个自定义的 AuthorizationDeniedException 实例。不过,请注意,返回一个对象是更可取的做法,因为这样不会产生生成堆栈跟踪的开销。
然后,您可以在自定义如何处理授权结果时访问自定义详细信息。
使用自定义授权管理器
第二种以编程方式授权方法的方式是创建自定义的 AuthorizationManager。
首先,声明一个授权管理器实例,可能像这样:
- Java
- Kotlin
@Component
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
        // ... authorization logic
    }
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
        // ... authorization logic
    }
}
@Component
class MyAuthorizationManager : AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
    override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationDecision {
        // ... authorization logic
    }
    override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationDecision {
        // ... authorization logic
    }
}
然后,使用与你希望 AuthorizationManager 运行时对应的通知发布方法拦截器。例如,你可以像这样替换 @PreAuthorize 和 @PostAuthorize 的工作方式:
- Java
- Kotlin
- Xml
@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);
    }
}
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
       @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    fun preAuthorize(val manager: MyAuthorizationManager) : Advisor {
        return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager)
    }
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    fun postAuthorize(val manager: MyAuthorizationManager) : Advisor {
        return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager)
    }
}
<sec:method-security pre-post-enabled="false"/>
<aop:config/>
<bean id="preAuthorize"
    class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
    factory-method="preAuthorize">
    <constructor-arg ref="myAuthorizationManager"/>
</bean>
<bean id="postAuthorize"
    class="org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor"
    factory-method="postAuthorize">
    <constructor-arg ref="myAuthorizationManager"/>
</bean>
你可以使用 AuthorizationInterceptorsOrder 中指定的顺序常量,将拦截器放置在 Spring Security 方法拦截器之间。
自定义表达式处理
或者,第三种方法,你可以自定义每个 SpEL 表达式的处理方式。要做到这一点,你可以暴露一个自定义的 MethodSecurityExpressionHandler,如下所示:
- Java
- Kotlin
- Xml
@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
    DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
    handler.setRoleHierarchy(roleHierarchy);
    return handler;
}
companion object {
    @Bean
    fun methodSecurityExpressionHandler(val roleHierarchy: RoleHierarchy) : MethodSecurityExpressionHandler {
        val handler = DefaultMethodSecurityExpressionHandler()
        handler.setRoleHierarchy(roleHierarchy)
        return handler
    }
}
<sec:method-security>
    <sec:expression-handler ref="myExpressionHandler"/>
</sec:method-security>
<bean id="myExpressionHandler"
        class="org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler">
    <property name="roleHierarchy" ref="roleHierarchy"/>
</bean>
我们通过一个 static 方法来暴露 MethodSecurityExpressionHandler,以确保 Spring 在初始化 Spring Security 的方法安全 @Configuration 类之前发布它。
你也可以子类化 DefaultMessageSecurityExpressionHandler 以添加自定义授权表达式,而不仅仅是默认的。
使用 AOT
Spring Security 会扫描应用程序上下文中的所有 bean,查找使用 @PreAuthorize 或 @PostAuthorize 的方法。当找到这样的方法时,它会解析安全表达式中使用的任何 bean,并为该 bean 注册适当的运行时提示。如果找到使用 @AuthorizeReturnObject 的方法,它会递归地在方法的返回类型中搜索 @PreAuthorize 和 @PostAuthorize 注解,并相应地进行注册。
例如,考虑以下 Spring Boot 应用程序:
- Java
- Kotlin
@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;
    }
}
@Service
class AccountService { 1
    @PreAuthorize("@authz.decide()") 2
    @AuthorizeReturnObject 3
    fun getAccountById(accountId: String): Account {
        // ...
    }
}
class Account(private val accountNumber: String) {
    @PreAuthorize("@accountAuthz.canViewAccountNumber()") 4
    fun getAccountNumber(): String {
        return this.accountNumber
    }
    @AuthorizeReturnObject 5
    fun getUser(): User {
        return User("John Doe")
    }
}
class User(private val fullName: String) {
    @PostAuthorize("@myOtherAuthz.decide()") 6
    fun getFullName(): String {
        return this.fullName
    }
}
- Spring Security 找到 - AccountServicebean
- 找到使用 - @PreAuthorize的方法时,它会解析表达式中使用的任何 bean 名称,在这种情况下是- authz,并为 bean 类注册运行时提示
- 找到使用 - @AuthorizeReturnObject的方法时,它会检查方法的返回类型中的任何- @PreAuthorize或- @PostAuthorize
- 然后,它找到另一个带有 bean 名称的 - @PreAuthorize:- accountAuthz;也会为 bean 类注册运行时提示
- 找到另一个 - @AuthorizeReturnObject时,它会再次检查方法的返回类型
- 现在,找到一个带有另一个 bean 名称的 - @PostAuthorize:- myOtherAuthz;也会为 bean 类注册运行时提示
有许多情况下,Spring Security 无法提前确定方法的实际返回类型,因为它可能隐藏在被擦除的泛型类型中。
考虑以下服务:
- Java
- Kotlin
@Service
public class AccountService {
    @AuthorizeReturnObject
    public List<Account> getAllAccounts() {
        // ...
    }
}
@Service
class AccountService {
    @AuthorizeReturnObject
    fun getAllAccounts(): List<Account> {
        // ...
    }
}
在这种情况下,泛型类型被擦除,因此 Spring Security 无法提前知道需要访问 Account 来检查 @PreAuthorize 和 @PostAuthorize。
为了解决这个问题,你可以发布一个 PrePostAuthorizeExpressionBeanHintsRegistrar,如下所示:
- Java
- Kotlin
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static SecurityHintsRegistrar registerTheseToo() {
    return new PrePostAuthorizeExpressionBeanHintsRegistrar(Account.class);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun registerTheseToo(): SecurityHintsRegistrar {
    return PrePostAuthorizeExpressionBeanHintsRegistrar(Account::class.java)
}
使用 AspectJ 进行授权
通过自定义切点匹配方法
基于 Spring AOP,你可以声明与注解无关的模式,类似于请求级授权。这样做的潜在优势在于可以集中管理方法级的授权规则。
例如,你可以使用发布自己的 Advisor 或使用 <protect-pointcut> 将 AOP 表达式与服务层的授权规则匹配,如下所示:
- Java
- Kotlin
- Xml
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"))
}
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole
companion object {
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    fun protectServicePointcut(): Advisor {
        val pattern = AspectJExpressionPointcut()
        pattern.setExpression("execution(* com.mycompany.*Service.*(..))")
        return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER"))
    }
}
<sec:method-security>
    <protect-pointcut expression="execution(* com.mycompany.*Service.*(..))" access="hasRole('USER')"/>
</sec:method-security>
集成 AspectJ 字节织入
有时可以通过使用 AspectJ 将 Spring Security 增强代码织入到 bean 的字节码中来提高性能。
在设置好 AspectJ 之后,您可以在 @EnableMethodSecurity 注解或 <method-security> 元素中简单地声明您正在使用 AspectJ:
- Java
- Kotlin
- Xml
@EnableMethodSecurity(mode=AdviceMode.ASPECTJ)
@EnableMethodSecurity(mode=AdviceMode.ASPECTJ)
<sec:method-security mode="aspectj"/>
结果是 Spring Security 会将其 advisor 发布为 AspectJ 建议,以便它们可以相应地被织入。
指定顺序
正如已经提到的,每个注解都有一个 Spring AOP 方法拦截器,并且每个拦截器在 Spring AOP 顾问链中都有一个位置。
即,@PreFilter 方法拦截器的顺序是 100,@PreAuthorize 的顺序是 200,依此类推。
需要注意这一点的原因是,还有其他基于AOP的注解,如 @EnableTransactionManagement,它们的顺序为 Integer.MAX_VALUE。换句话说,默认情况下,它们位于advisor链的末尾。
有时,在 Spring Security 之前执行其他建议可能会很有价值。例如,如果你有一个用 @Transactional 和 @PostAuthorize 注解的方法,你可能希望在 @PostAuthorize 运行时事务仍然是打开的,这样 AccessDeniedException 将会导致回滚。
要让 @EnableTransactionManagement 在方法授权通知运行之前开启事务,你可以这样设置 @EnableTransactionManagement 的顺序:
- Java
- Kotlin
- Xml
@EnableTransactionManagement(order = 0)
@EnableTransactionManagement(order = 0)
<tx:annotation-driven ref="txManager" order="0"/>
由于最早的拦截器方法(@PreFilter)被设置为 100 的顺序,因此设置为零意味着事务通知将在所有 Spring Security 通知之前运行。
用SpEL表达授权
你已经看过几个使用 SpEL 的示例,现在让我们更深入地介绍 API。
Spring Security 将其所有的授权字段和方法封装在一组根对象中。最通用的根对象被称为 SecurityExpressionRoot,它是 MethodSecurityExpressionRoot 的基础。当准备评估一个授权表达式时,Spring Security 会将这个根对象提供给 MethodSecurityEvaluationContext。
使用授权表达式字段和方法
这提供了增强的授权字段和方法到您的SpEL表达式中。以下是对最常用方法的快速概述:
- 
permitAll- 调用该方法不需要任何授权;请注意,在这种情况下,认证信息永远不会从会话中检索
- 
denyAll- 该方法在任何情况下都不允许调用;请注意,在这种情况下,Authentication永远不会从会话中检索
- 
hasAuthority- 该方法要求Authentication具有与给定值匹配的 GrantedAuthority
- 
hasRole-hasAuthority的快捷方式,它会在前面加上ROLE_或者配置的默认前缀
- 
hasAnyAuthority- 该方法要求Authentication具有与给定值中的任何一个匹配的GrantedAuthority
- 
hasAnyRole-hasAnyAuthority的快捷方式,它会在前面加上ROLE_或者配置的默认前缀
- 
hasPermission- 连接到您的PermissionEvaluator实例以进行对象级授权
以下是最常见的字段简介:
- 
authentication- 与此方法调用关联的Authentication实例
- 
principal- 与此方法调用关联的Authentication#getPrincipal
现在你已经学习了模式、规则以及它们如何组合在一起,你应该能够理解这个更复杂的例子中的内容:
- Java
- Kotlin
- Xml
@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(...);
}
@Component
open class MyService {
    @PreAuthorize("denyAll") 1
    fun myDeprecatedMethod(...): MyResource
    @PreAuthorize("hasRole('ADMIN')") 2
    fun writeResource(...): MyResource
    @PreAuthorize("hasAuthority('db') and hasRole('ADMIN')") 3
    fun deleteResource(...): MyResource
    @PreAuthorize("principal.claims['aud'] == 'my-audience'") 4
    fun readResource(...): MyResource
    @PreAuthorize("@authz.check(#root)")
    fun shareResource(...): MyResource
}
<sec:method-security>
    <protect-pointcut expression="execution(* com.mycompany.*Service.myDeprecatedMethod(..))" access="denyAll"/> // <1>
    <protect-pointcut expression="execution(* com.mycompany.*Service.writeResource(..))" access="hasRole('ADMIN')"/> // <2>
    <protect-pointcut expression="execution(* com.mycompany.*Service.deleteResource(..))" access="hasAuthority('db') and hasRole('ADMIN')"/> // <3>
    <protect-pointcut expression="execution(* com.mycompany.*Service.readResource(..))" access="principal.claims['aud'] == 'my-audience'"/> // <4>
    <protect-pointcut expression="execution(* com.mycompany.*Service.shareResource(..))" access="@authz.check(#root)"/> // <5>
</sec:method-security>
- 任何人均不得以任何理由调用此方法 
- 此方法仅可由被授予 - ROLE_ADMIN权限的- Authentication调用
- 此方法仅可由被授予 - db和- ROLE_ADMIN权限的- Authentication调用
- 此方法仅可由 - aud声明等于 "my-audience" 的- Princpal调用
- 仅当 bean - authz的- check方法返回- true时,才可调用此方法
你可以像上面那样使用一个名为 authz 的 bean 来添加编程授权。
使用方法参数
此外,Spring Security 提供了一种机制来发现方法参数,这样它们也可以在 SpEL 表达式中被访问。
作为一个完整的参考,Spring Security 使用 DefaultSecurityParameterNameDiscoverer 来发现参数名称。默认情况下,对于一个方法,会尝试以下选项。
- 
如果方法的单个参数上存在 Spring Security 的 @P注解,则使用该值。以下示例使用了@P注解:- Java
- Kotlin
 import org.springframework.security.access.method.P
 ...
 @PreAuthorize("hasPermission(#c, 'write')")
 public void updateContact(@P("c") Contact contact);import org.springframework.security.access.method.P
 ...
 @PreAuthorize("hasPermission(#c, 'write')")
 fun doSomething(@P("c") contact: Contact?)该表达式的意图是要求当前的 Authentication对此Contact实例具有write权限。在幕后,这是通过使用 AnnotationParameterNameDiscoverer实现的,你可以自定义它以支持任何指定注解的值属性。
- 
如果方法的至少一个参数上存在 Spring Data 的 @Param注解,则使用该值。以下示例使用了@Param注解:- Java
- Kotlin
 import org.springframework.data.repository.query.Param;
 ...
 @PreAuthorize("#n == authentication.name")
 Contact findContactByName(@Param("n") String name);import org.springframework.data.repository.query.Param
 ...
 @PreAuthorize("#n == authentication.name")
 fun findContactByName(@Param("n") name: String?): Contact?该表达式的意图是要求 name等于Authentication#getName以便授权调用。在幕后,这是通过使用 AnnotationParameterNameDiscoverer实现的,你可以自定义它以支持任何指定注解的值属性。
- 
如果你使用 -parameters参数编译代码,则使用标准的 JDK 反射 API 来发现参数名称。这适用于类和接口。
- 
最后,如果你使用调试符号编译代码,则使用调试符号来发现参数名称。这对接口不起作用,因为它们没有关于参数名称的调试信息。对于接口,必须使用注解或 -parameters方法。
授权任意对象
Spring Security 还支持包装任何使用其方法安全注解的对象。
实现这一目标的最简单方法是使用 @AuthorizeReturnObject 注解标记任何返回你希望授权的对象的方法。
例如,考虑下面的 User 类:
- Java
- Kotlin
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;
    }
}
class User (val name:String, @get:PreAuthorize("hasAuthority('user:read')") val email:String)
给定一个这样的接口:
- Java
- Kotlin
public class UserRepository {
    @AuthorizeReturnObject
    Optional<User> findByName(String name) {
        // ...
    }
}
class UserRepository {
    @AuthorizeReturnObject
    fun findByName(name:String?): Optional<User?>? {
        // ...
    }
}
那么从 findById 返回的任何 User 都将像其他受 Spring Security 保护的组件一样受到保护:
- Java
- Kotlin
@Autowired
UserRepository users;
@Test
void getEmailWhenProxiedThenAuthorizes() {
    Optional<User> securedUser = users.findByName("name");
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> securedUser.get().getEmail());
}
import jdk.incubator.vector.VectorOperators.Test
import java.nio.file.AccessDeniedException
import java.util.*
@Autowired
var users:UserRepository? = null
@Test
fun getEmailWhenProxiedThenAuthorizes() {
    val securedUser: Optional<User> = users.findByName("name")
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy{securedUser.get().getEmail()}
}
在类级别使用 @AuthorizeReturnObject
@AuthorizeReturnObject 可以放在类级别。不过,请注意,这意味着 Spring Security 会尝试代理任何返回对象,包括 String、Integer 和其他类型。这通常不是你想要做的。
如果你想要在一个类或接口的方法返回值类型(如 int、String、Double 或这些类型的集合)上使用 @AuthorizeReturnObject,那么你也应该发布相应的 AuthorizationAdvisorProxyFactory.TargetVisitor,如下所示:
- Java
- Kotlin
@Bean
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
    return (factory) -> factory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
}
@Bean
open fun skipValueTypes() = Customizer<AuthorizationAdvisorProxyFactory> {
    it.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes())
}
你可以设置你自己的 AuthorizationAdvisorProxyFactory.TargetVisitor 来为任何一组类型自定义代理
通过编程方式代理
您还可以以编程方式代理给定的对象。
要实现这一点,你可以自动装配提供的 AuthorizationProxyFactory 实例,该实例基于你配置的方法安全拦截器。如果你使用的是 @EnableMethodSecurity,那么这意味着它默认会有 @PreAuthorize、@PostAuthorize、@PreFilter 和 @PostFilter 的拦截器。
您可以按如下方式代理 user 的实例:
- Java
- Kotlin
@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);
}
@Autowired
var proxyFactory:AuthorizationProxyFactory? = null
@Test
fun getEmailWhenProxiedThenAuthorizes() {
    val user: User = User("name", "email")
    assertThat(user.getEmail()).isNotNull()
    val securedUser: User = proxyFactory.proxy(user)
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
}
手动构建
您也可以根据需要定义自己的实例,如果Spring Security默认设置不能满足需求的话。
例如,如果你像这样定义一个 AuthorizationProxyFactory 实例:
- Java
- Kotlin
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());
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize
// ...
val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize())
// and if needing to skip value types
proxyFactory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes())
然后你可以将任何 User 实例包装如下:
- Java
- Kotlin
@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);
}
@Test
fun getEmailWhenProxiedThenAuthorizes() {
    val proxyFactory: AuthorizationProxyFactory = AuthorizationAdvisorProxyFactory.withDefaults()
    val user: User = User("name", "email")
    assertThat(user.getEmail()).isNotNull()
    val securedUser: User = proxyFactory.proxy(user)
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
}
代理集合
AuthorizationProxyFactory 通过代理元素类型支持 Java 集合、流、数组、选项和迭代器,并通过代理值类型支持映射。
这意味着在代理一个 List 对象时,以下操作也是可行的:
- Java
@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 通知器,因此通常你无需执行任何操作即可激活这些功能。
使用 returnObject 或 filterObject 的 SpEL 表达式位于代理后面,因此可以完全访问对象。
自定义建议
如果你有想要应用的安全建议,你可以像这样发布你自己的 AuthorizationAdvisor:
- Java
- Kotlin
@EnableMethodSecurity
class SecurityConfig {
    @Bean
    static AuthorizationAdvisor myAuthorizationAdvisor() {
        return new AuthorizationAdvisor();
    }
}
@EnableMethodSecurity
internal class SecurityConfig {
    @Bean
    fun myAuthorizationAdvisor(): AuthorizationAdvisor {
        return AuthorizationAdvisor()
    }
]
并且 Spring Security 会将该 advisor 添加到 AuthorizationProxyFactory 在代理对象时添加的 advice 集合中。
使用 Jackson
这一功能的一个强大用法是从控制器中返回一个受保护的值,如下所示:
- Java
- Kotlin
@RestController
public class UserController {
    @Autowired
    AuthorizationProxyFactory proxyFactory;
    @GetMapping
    User currentUser(@AuthenticationPrincipal User user) {
        return this.proxyFactory.proxy(user);
    }
}
@RestController
class UserController  {
    @Autowired
    var proxyFactory: AuthorizationProxyFactory? = null
    @GetMapping
    fun currentUser(@AuthenticationPrincipal user:User?): User {
        return proxyFactory.proxy(user)
    }
}
- Java
- Kotlin
@Component
public class Null implements MethodAuthorizationDeniedHandler {
    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        return null;
    }
}
// ...
@HandleAuthorizationDenied(handlerClass = Null.class)
public class User {
    ...
}
@Component
class Null : MethodAuthorizationDeniedHandler {
    override fun handleDeniedInvocation(methodInvocation: MethodInvocation?, authorizationResult: AuthorizationResult?): Any? {
        return null
    }
}
// ...
@HandleAuthorizationDenied(handlerClass = Null.class)
open 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 应用程序:
- Java
- Kotlin
@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);
    }
}
@SpringBootApplication
open class MyApplication {
    @RestController
    open class MyController { 1
        @GetMapping
        @AuthorizeReturnObject
        fun getMessage():Message { 2
            return Message(someUser, "hello!")
        }
    }
    open class Message { 3
        val to: User
        val test: String
        // ...
        @AuthorizeReturnObject
        fun getTo(): User { 4
            return this.to
        }
        // ...
    }
    open class User { 5
        // ...
    }
    fun main(args: Array<String>) {
        SpringApplication.run(MyApplication.class)
    }
}
- - 首先,Spring Security 找到 - MyControllerbean
- - 找到使用 - @AuthorizeReturnObject的方法后,它代理返回值- Message,并将该代理类注册到- RuntimeHints
- - 然后,它遍历 - Message以查看是否使用了- @AuthorizeReturnObject
- - 找到使用 - @AuthorizeReturnObject的方法后,它代理返回值- User,并将该代理类注册到- RuntimeHints
- - 最后,它遍历 - User以查看是否使用了- @AuthorizeReturnObject;没有找到任何内容后,算法完成
有很多情况下,Spring Security 无法提前确定代理类,因为它可能隐藏在被擦除的泛型类型中。
考虑对 MyController 进行如下更改:
- Java
- Kotlin
@RestController
public static class MyController {
    @GetMapping
    @AuthorizeReturnObject
    List<Message> getMessages() {
        return List.of(new Message(someUser, "hello!"));
    }
}
@RestController
static class MyController {
    @AuthorizeReturnObject
    @GetMapping
    fun getMessages(): Array<Message> = arrayOf(Message(someUser, "hello!"))
}
在这种情况下,泛型类型被擦除,因此 Spring Security 无法提前得知 Message 需要在运行时被代理。
为了解决这个问题,你可以像这样发布 AuthorizeProxyFactoryHintsRegistrar:
- Java
- Kotlin
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static SecurityHintsRegsitrar registerTheseToo(AuthorizationProxyFactory proxyFactory) {
    return new AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message.class);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun registerTheseToo(proxyFactory: AuthorizationProxyFactory?): SecurityHintsRegistrar {
    return AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message::class.java)
}
Spring Security 会注册该类,然后像以前一样遍历其类型。
当授权被拒绝时提供备用值
在某些情况下,你可能不希望在没有所需权限的情况下调用方法时抛出 AuthorizationDeniedException。相反,你可能希望返回一个后处理的结果,比如一个被屏蔽的结果,或者在授权被拒绝而未调用方法的情况下返回一个默认值。
Spring Security 通过使用 @HandleAuthorizationDenied 提供了处理方法调用时授权被拒绝的支持。该处理器适用于在 @PreAuthorize 和 @PostAuthorize 注解 中发生的拒绝授权,以及从方法调用本身抛出的 AuthorizationDeniedException。
让我们考虑上一节中的示例,但不是创建 AccessDeniedExceptionInterceptor 将 AccessDeniedException 转换为 null 返回值,而是使用 @HandleAuthorizationDenied 的 handlerClass 属性:
- Java
- Kotlin
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;
    }
}
class NullMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler { 1
    override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
        return null
    }
}
@Configuration
@EnableMethodSecurity
class SecurityConfig {
    @Bean 2
    fun nullMethodAuthorizationDeniedHandler(): NullMethodAuthorizationDeniedHandler {
        return MaskMethodAuthorizationDeniedHandler()
    }
}
class User (val name:String, @PreAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler::class) val email:String) 3
- 创建 - MethodAuthorizationDeniedHandler的实现,该实现返回一个- null值
- 将 - NullMethodAuthorizationDeniedHandler注册为一个 bean
- 使用 - @HandleAuthorizationDenied注解方法,并将- NullMethodAuthorizationDeniedHandler传递给- handlerClass属性
然后你可以验证返回的是 null 值而不是 AccessDeniedException:
您也可以使用 @Component 注解您的类,而不是创建一个 @Bean 方法
- Java
- Kotlin
@Autowired
UserRepository users;
@Test
void getEmailWhenProxiedThenNullEmail() {
    Optional<User> securedUser = users.findByName("name");
    assertThat(securedUser.get().getEmail()).isNull();
}
@Autowired
var users:UserRepository? = null
@Test
fun getEmailWhenProxiedThenNullEmail() {
    val securedUser: Optional<User> = users.findByName("name")
    assertThat(securedUser.get().getEmail()).isNull()
}
使用方法调用中的拒绝结果
在某些情况下,你可能希望从被拒绝的结果中返回一个安全的结果。例如,如果用户没有权限查看电子邮件地址,你可能希望对原始电子邮件地址进行一些掩码处理,即 useremail@example.com 会变成 use******@example.com。
对于这些场景,你可以重写 MethodAuthorizationDeniedHandler 中的 handleDeniedInvocationResult 方法,该方法将 MethodInvocationResult 作为参数。让我们继续前面的例子,但这次我们不返回 null,而是返回一个经过掩码处理的电子邮件值:
- Java
- Kotlin
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;
    }
}
class EmailMaskingMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler {
    override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
        return "***"
    }
    override fun handleDeniedInvocationResult(methodInvocationResult: MethodInvocationResult, authorizationResult: AuthorizationResult): Any {
        val email = methodInvocationResult.result as String
        return email.replace("(^[^@]{3}|(?!^)\\G)[^@]".toRegex(), "$1*")
    }
}
@Configuration
@EnableMethodSecurity
class SecurityConfig {
    @Bean
    fun emailMaskingMethodAuthorizationDeniedHandler(): EmailMaskingMethodAuthorizationDeniedHandler {
        return EmailMaskingMethodAuthorizationDeniedHandler()
    }
}
class User (val name:String, @PostAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler::class) val email:String) 3
- 创建 - MethodAuthorizationDeniedHandler的实现,该实现返回未经授权的结果值的掩码值
- 将 - EmailMaskingMethodAuthorizationDeniedHandler注册为一个 bean
- 使用 - @HandleAuthorizationDenied注解方法,并将- EmailMaskingMethodAuthorizationDeniedHandler传递给- handlerClass属性
然后你可以验证返回的是一个被屏蔽的电子邮件,而不是 AccessDeniedException:
由于您可以访问原始被拒绝的值,请确保正确处理它,不要将其返回给调用者。
- Java
- Kotlin
@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");
}
@Autowired
var users:UserRepository? = null
@Test
fun getEmailWhenProxiedThenMaskedEmail() {
    val securedUser: Optional<User> = users.findByName("name")
    // email is useremail@example.com
    assertThat(securedUser.get().getEmail()).isEqualTo("use******@example.com")
}
在实现 MethodAuthorizationDeniedHandler 时,你可以选择返回几种不同类型的对象:
- 
一个 null值。
- 
一个非空值,遵循方法的返回类型。 
- 
抛出一个异常,通常是 AuthorizationDeniedException的实例。这是默认行为。
- 
反应式应用程序中的 Mono类型。
请注意,由于处理器必须在你的应用程序上下文中注册为bean,因此如果需要更复杂的逻辑,你可以向其中注入依赖项。除此之外,你还可以使用 MethodInvocation 或 MethodInvocationResult 以及 AuthorizationResult 来获取与授权决策相关的更多详细信息。
根据可用参数决定返回内容
考虑这样一种场景,可能存在针对不同方法的多个掩码值,如果我们必须为每个方法创建一个处理程序,那么效率会比较低,尽管这样做是完全可以的。在这种情况下,我们可以使用通过参数传递的信息来决定要做什么。例如,我们可以创建一个自定义的 @Mask 注解和一个处理程序,该处理程序检测该注解以决定返回哪个掩码值:
- Java
- Kotlin
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";
    }
}
import org.springframework.core.annotation.AnnotationUtils
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Mask(val value: String)
class MaskAnnotationDeniedHandler : MethodAuthorizationDeniedHandler {
    override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
        val mask = AnnotationUtils.getAnnotation(methodInvocation.method, Mask::class.java)
        return mask.value
    }
}
@Configuration
@EnableMethodSecurity
class SecurityConfig {
    @Bean
    fun maskAnnotationDeniedHandler(): MaskAnnotationDeniedHandler {
        return MaskAnnotationDeniedHandler()
    }
}
@Component
class MyService {
    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
    @Mask("***")
    fun foo(): String {
        return "foo"
    }
    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
    @Mask("???")
    fun bar(): String {
        return "bar"
    }
}
现在,当访问被拒绝时的返回值将根据 @Mask 注解来决定:
- Java
- Kotlin
@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("???");
}
@Autowired
var myService: MyService
@Test
fun fooWhenDeniedThenReturnStars() {
    val value: String = myService.foo()
    assertThat(value).isEqualTo("***")
}
@Test
fun barWhenDeniedThenReturnQuestionMarks() {
    val value: String = myService.foo()
    assertThat(value).isEqualTo("???")
}
结合元注解支持
您还可以将 @HandleAuthorizationDenied 与其他注解结合使用,以减少和简化方法中的注解。让我们考虑上一节的示例,并将 @HandleAuthorizationDenied 与 @Mask 合并:
- Java
- Kotlin
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
public @interface Mask {
    String value();
}
@Mask("***")
public String myMethod() {
    // ...
}
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
annotation class Mask(val value: String)
@Mask("***")
fun myMethod(): String {
    // ...
}
现在,当你需要在方法中使用掩码行为时,不必记住添加这两个注释。请确保阅读元注解支持部分,以获取更多使用细节。
从 @EnableGlobalMethodSecurity 迁移
如果你正在使用 @EnableGlobalMethodSecurity,你应该迁移到 @EnableMethodSecurity。
将 替换为
@EnableGlobalMethodSecurity 和 <global-method-security> 已被弃用,推荐使用 @EnableMethodSecurity 和 <method-security>。新的注解和 XML 元素默认激活 Spring 的 pre-post 注解,并在内部使用 AuthorizationManager。
这意味着以下两个代码示例在功能上是等价的:
- Java
- Kotlin
- Xml
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
<global-method-security pre-post-enabled="true"/>
和:
- Java
- Kotlin
- Xml
@EnableMethodSecurity
@EnableMethodSecurity
<method-security/>
对于不使用预处理-后处理注解的应用程序,请确保将其关闭以避免激活不需要的行为。
例如,像这样的列表:
- Java
- Kotlin
- Xml
@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(securedEnabled = true)
<global-method-security secured-enabled="true"/>
应该更改为:
- Java
- Kotlin
- Xml
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
<method-security secured-enabled="true" pre-post-enabled="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,如下所示:
- Java
- Kotlin
class MyAuthorizer {
    boolean isAdmin(MethodSecurityExpressionOperations root) {
        boolean decision = root.hasAuthority("ADMIN");
        // custom work ...
        return decision;
    }
}
class MyAuthorizer {
    fun isAdmin(val root: MethodSecurityExpressionOperations): boolean {
        val decision = root.hasAuthority("ADMIN");
        // custom work ...
        return decision;
    }
}
然后在注释中这样引用它:
- Java
- Kotlin
@PreAuthorize("@authz.isAdmin(#root)")
@PreAuthorize("@authz.isAdmin(#root)")
我仍然倾向于子类化 DefaultMethodSecurityExpressionHandler
如果你必须继续继承 DefaultMethodSecurityExpressionHandler,你仍然可以这样做。相反,请像下面这样重写 createEvaluationContext(Supplier<Authentication>, MethodInvocation) 方法:
- Java
- Kotlin
@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;
    }
}
@Component
class MyExpressionHandler: DefaultMethodSecurityExpressionHandler {
    override fun createEvaluationContext(val authentication: Supplier<Authentication>,
        val mi: MethodInvocation): EvaluationContext {
        val context = super.createEvaluationContext(authentication, mi) as StandardEvaluationContext
        val delegate = context.getRootObject().getValue() as MethodSecurityExpressionOperations
        val root = MySecurityExpressionRoot(delegate)
        context.setRootObject(root)
        return context
    }
}