方法安全
除了在请求级别建模授权之外,Spring Security 还支持在方法级别建模授权。
您可以通过在任何 @Configuration 类上添加 @EnableMethodSecurity 注解来激活它,或者将 <method-security> 添加到任何 XML 配置文件中,像这样
-
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 表达式授权方法
-
自定义SpEL 表达式处理
-
与自定义授权系统集成
方法安全的工作原理
Spring Security 的方法授权支持对于以下情况非常有用
-
提取细粒度授权逻辑;例如,当方法参数和返回值有助于授权决策时。
-
在服务层强制执行安全
-
在风格上偏爱基于注解的配置而非基于
HttpSecurity的配置
由于方法安全是使用 Spring AOP 构建的,您可以访问其所有表达能力,以便根据需要覆盖 Spring Security 的默认设置。
如前所述,您可以首先将 @EnableMethodSecurity 添加到 @Configuration 类中,或者在 Spring XML 配置文件中添加 <sec:method-security/>。
|
此注解和 XML 元素分别取代了
如果您正在使用 |
方法授权是方法调用前授权和方法调用后授权的组合。考虑一个以下列方式注解的服务 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(id: String): Customer { ... }
}
当方法安全被激活时,对 MyCustomerService#readCustomer 的给定调用可能看起来像这样
-
Spring AOP 调用其
readCustomer的代理方法。在代理的其他通知器中,它调用了一个与@PreAuthorize切点匹配的AuthorizationManagerBeforeMethodInterceptor -
授权管理器使用
MethodSecurityExpressionHandler解析注解的SpEL 表达式,并从包含Supplier<Authentication>和MethodInvocation的MethodSecurityExpressionRoot构造相应的EvaluationContext。 -
拦截器使用此上下文评估表达式;具体来说,它从
Supplier读取Authentication,并检查其权限集合中是否具有permission:read -
如果评估通过,Spring AOP 则继续调用该方法。
-
如果未通过,拦截器将发布
AuthorizationDeniedEvent并抛出AccessDeniedException,ExceptionTranslationFilter将捕获该异常并向响应返回 403 状态码 -
方法返回后,Spring AOP 调用与
@PostAuthorize切点匹配的AuthorizationManagerAfterMethodInterceptor,其操作与上述相同,但使用PostAuthorizeAuthorizationManager -
如果评估通过(在这种情况下,返回值属于登录用户),则处理正常继续
-
如果未通过,拦截器将发布
AuthorizationDeniedEvent并抛出AccessDeniedException,ExceptionTranslationFilter将捕获该异常并向响应返回 403 状态码
如果方法不是在 HTTP 请求的上下文中被调用,您可能需要自行处理 AccessDeniedException |
多个注解按顺序计算
如上所示,如果方法调用涉及多个方法安全注解,则每个注解都会逐一处理。这意味着它们可以被认为是通过“与”运算组合在一起的。换句话说,要使调用获得授权,所有注解检查都需要通过授权。
每个注解都有自己的方法拦截器
每个注解都有自己专用的方法拦截器。这样做的原因是为了使事物更具组合性。例如,如果需要,您可以禁用 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
-
Kotlin
@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')")
或者,在可能的情况下,将特定于应用程序的授权逻辑在登录时转换为授予的权限。
请求级与方法级授权的比较
何时应优先使用方法级授权而不是请求级授权?这其中一些取决于个人喜好;但是,请考虑以下各项的优点列表,以帮助您做出决定。
请求级别 |
方法级别 |
|
授权类型 |
粗粒度 |
细粒度 |
配置位置 |
在配置类中声明 |
方法声明的本地 |
配置风格 |
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(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(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(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 {
@PostFilter("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(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(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(id: Long): Account {
// ... is only returned if the `Account` belongs to the logged in user
}
}
这样,一旦替换,表达式就变成了 @PreAuthorize("hasAnyRole('USER', 'ADMIN')")。
启用某些注解
您可以关闭 @EnableMethodSecurity 的预配置并用您自己的配置替换。如果您想自定义 AuthorizationManager 或 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"/>
上面的代码片段通过首先禁用方法安全的预配置,然后发布@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,该 bean 的方法接受 MethodSecurityExpressionOperations 实例,如下所示
-
Java
-
Kotlin
@Component("authz")
public class AuthorizationLogic {
public boolean decide(MethodSecurityExpressionOperations operations) {
// ... authorization logic
}
}
@Component("authz")
open class AuthorizationLogic {
fun decide(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(operations: MethodSecurityExpressionOperations): AuthorizationDecision {
// ... authorization logic
return MyAuthorizationDecision(false, details)
}
}
或者抛出自定义 AuthorizationDeniedException 实例。但是请注意,返回对象是首选,因为这不会产生生成堆栈跟踪的开销。
然后,当您自定义如何处理授权结果时,您可以访问自定义详细信息。
|
此外,您可以返回 |
使用自定义授权管理器
以编程方式授权方法的第二种方法是创建自定义的AuthorizationManager。
首先,声明一个授权管理器实例,也许像这样
-
Java
-
Kotlin
@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
}
}
@Component
class MyAuthorizationManager : AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationResult {
// ... authorization logic
}
override fun authorize(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationResult {
// ... 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(manager: MyAuthorizationManager) : Advisor {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager)
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun postAuthorize(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>
|
您可以使用 |
自定义表达式处理
或者,第三,您可以自定义每个 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(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>
|
我们使用 |
您还可以子类化 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
}
}
| 1 | Spring Security 找到了 AccountService bean |
| 2 | 找到使用 @PreAuthorize 的方法后,它将解析表达式中使用的任何 bean 名称(在这种情况下为 authz),并为该 bean 类注册运行时提示 |
| 3 | 找到使用 @AuthorizeReturnObject 的方法后,它将查看该方法的返回类型中是否有 @PreAuthorize 或 @PostAuthorize |
| 4 | 然后,它找到另一个带有另一个 bean 名称:accountAuthz 的 @PreAuthorize;运行时提示也为该 bean 类注册 |
| 5 | 找到另一个 @AuthorizeReturnObject 后,它将再次查看方法的返回类型 |
| 6 | 现在,找到一个 @PostAuthorize,其中使用了另一个 bean 名称: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 会将其通知器作为 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
现在您已经了解了模式、规则以及它们如何组合在一起,您应该能够理解这个更复杂的示例中发生的事情
-
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>
| 1 | 任何人都不得以任何理由调用此方法 |
| 2 | 此方法只能由被授予 ROLE_ADMIN 权限的 Authentication 调用 |
| 3 | 此方法只能由被授予 db 和 ROLE_ADMIN 权限的 Authentication 调用 |
| 4 | 此方法只能由 Princpal 的 aud 声明等于 "my-audience" 的情况下调用 |
| 5 | 仅当 bean authz 的 check 方法返回 true 时才能调用此方法 |
|
您可以使用像上面 |
使用方法参数
此外,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方法。
自定义授权管理器
当您将 SpEL 表达式与@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter一起使用时,Spring Security 会为您创建适当的 AuthorizationManager 实例。在某些情况下,您可能希望自定义所创建的内容,以便完全控制框架级别的授权决策。
为了控制为前置和后置注解创建 AuthorizationManager 实例,您可以创建自定义的 AuthorizationManagerFactory。例如,假设您希望在需要任何其他角色时允许具有 ADMIN 角色的用户。为此,您可以为方法安全创建自定义实现,如下例所示
-
Java
-
Kotlin
@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")
);
}
}
@Component
class CustomMethodInvocationAuthorizationManagerFactory : AuthorizationManagerFactory<MethodInvocation> {
private val delegate = DefaultAuthorizationManagerFactory<MethodInvocation>()
override fun hasRole(role: String): AuthorizationManager<MethodInvocation> {
return AuthorizationManagers.anyOf(
delegate.hasRole(role),
delegate.hasRole("ADMIN")
)
}
override fun hasAnyRole(vararg roles: String): AuthorizationManager<MethodInvocation> {
return AuthorizationManagers.anyOf(
delegate.hasAnyRole(*roles),
delegate.hasRole("ADMIN")
)
}
}
现在,每当您使用 @PreAuthorize 注解与 hasRole 或 hasAnyRole 时,Spring Security 将自动调用您的自定义工厂来创建一个 AuthorizationManager 实例,该实例允许给定角色或 ADMIN 角色访问。
我们将其作为一个创建自定义 AuthorizationManagerFactory 的简单示例,尽管可以使用角色层次结构实现相同的结果。请根据您的情况选择最合适的方法。 |
授权任意对象
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 和其他类型。这通常不是您想要做的。
如果您想在类或接口上使用 @AuthorizeReturnObject,其方法返回值类型(如 int、String、Double 或这些类型的集合),那么您还应该发布适当的 AuthorizationAdvisorProxyFactory.TargetVisitor,如下所示
-
Java
-
Kotlin
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
// ...
@Bean
static TargetVisitor skipValueTypes() {
return TargetVisitor.defaultsSkipValueTypes();
}
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor
// ...
@Bean
open fun skipValueTypes() = TargetVisitor.defaultsSkipValueTypes()
|
您可以设置自己的 |
以编程方式代理
您也可以以编程方式代理给定对象。
为此,您可以自动注入提供的 AuthorizationProxyFactory 实例,该实例基于您配置的方法安全拦截器。如果您使用 @EnableMethodSecurity,则默认情况下它将包含 @PreAuthorize、@PostAuthorize、@PreFilter 和 @PostFilter 的拦截器。
您可以按以下方式代理用户实例
-
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 集合、流、数组、Optional 和迭代器,通过代理元素类型以及通过代理值类型来代理映射。
这意味着在代理对象 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 通知器,因此您通常无需执行任何操作即可激活此功能。
|
使用 |
自定义建议
如果您还有希望应用的自定义安全建议,可以发布自己的 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 会将该通知器添加到 AuthorizationProxyFactory 在代理对象时添加的建议集中。
使用 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 属性 |
与 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)
}
}
| 1 | - 首先,Spring Security 找到 MyController bean |
| 2 | - 找到使用 @AuthorizeReturnObject 的方法后,它会代理返回值 Message,并将该代理类注册到 RuntimeHints |
| 3 | - 然后,它遍历 Message 以查看它是否使用 @AuthorizeReturnObject |
| 4 | - 找到使用 @AuthorizeReturnObject 的方法后,它会代理返回值 User,并将该代理类注册到 RuntimeHints |
| 5 | - 最后,它遍历 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。相反,您可能希望返回一个后处理结果,例如 masked 结果,或者在方法调用之前发生授权拒绝时返回默认值。
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)
| 1 | 创建 MethodAuthorizationDeniedHandler 的实现,该实现返回 null 值 |
| 2 | 将 NullMethodAuthorizationDeniedHandler 注册为 bean |
| 3 | 使用 @HandleAuthorizationDenied 注解方法并将 NullMethodAuthorizationDeniedHandler 传递给 handlerClass 属性 |
然后,您可以验证返回的是 null 值而不是 AccessDeniedException
|
您还可以使用 |
-
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()
}
使用方法调用的拒绝结果
在某些情况下,您可能希望返回从拒绝结果派生的安全结果。例如,如果用户无权查看电子邮件地址,您可能希望对原始电子邮件地址应用一些掩码,即 [email protected] 将变为 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)
| 1 | 创建 MethodAuthorizationDeniedHandler 的实现,该实现返回未经授权结果值的掩码值 |
| 2 | 将 EmailMaskingMethodAuthorizationDeniedHandler 注册为 bean |
| 3 | 使用 @HandleAuthorizationDenied 注解方法并将 EmailMaskingMethodAuthorizationDeniedHandler 传递给 handlerClass 属性 |
然后您可以验证返回的是掩码电子邮件而不是 AccessDeniedException
|
由于您可以访问原始被拒绝值,请确保正确处理它并且不要将其返回给调用者。 |
-
Java
-
Kotlin
@Autowired
UserRepository users;
@Test
void getEmailWhenProxiedThenMaskedEmail() {
Optional<User> securedUser = users.findByName("name");
// email is [email protected]
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 [email protected]
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。
如果您目前无法迁移,请将 spring-security-access 模块作为依赖项包含在内,如下所示
-
Maven
-
Gradle
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-access</artifactId>
</dependency>
implementation('org.springframework.security:spring-security-access')
用方法安全替换全局方法安全
@EnableGlobalMethodSecurity 和 <global-method-security> 已弃用,取而代之的是 @EnableMethodSecurity 和 <method-security>。新的注解和 XML 元素默认激活 Spring 的前置-后置注解,并在内部使用 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(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(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
}
}