方法安全(Method Security)
本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。 |
除了 在请求级别对授权进行建模 外,Spring Security 还支持在方法级别对授权进行建模。
你可以通过在任何 @Configuration
类中注解 @EnableMethodSecurity
或在任何 XML 配置文件中添加 <method-security>
来激活它:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity
@EnableMethodSecurity
<sec:method-security/>
然后,你就可以立即用 @PreAuthorize
、@PostAuthorize
、@PreFilter
和 @PostFilter
注解任何 Spring 管理的类或方法,以授权方法调用,包括参数和返回值。
Spring Boot Starter Security 默认不激活方法级授权。 |
Method Security 还支持许多其他用例,包括 AspectJ 支持、自定义注解 和多个配置点。考虑学习以下用例:
-
了解 方法安全 的工作原理和使用原因。
-
比较 请求级授权和方法级授权。
-
使用
@PreAuthorize
和@PostAuthorize
授权方法。 -
使用
@PreFilter
和@PostFilter
过滤方法。 -
使用 JSR-250 注解 授权方法。
-
使用 AspectJ 表达式 授权方法。
-
与 AspectJ 字节码织入 集成。
-
与 @Transactional 和其他基于AOP的注解 相协调。
-
自定义 SpEL表达式处理。
-
与 自定义授权系统 集成。
方法安全的工作原理
Spring Security 的方法授权支持在以下方面非常方便:
-
提取细粒度的授权逻辑;例如,当方法参数和返回值有助于授权决策时。
-
在服务层加强安全性。
-
在风格上,基于注解的配置优于基于
HttpSecurity
的配置。
由 于Method Security 是使用 Spring AOP 构建的,因此你可以使用它所有的表达能力,根据需要覆盖 Spring Security 的默认值。
如前所述,首先要在 @Configuration
类或 Spring XML 配置文件中的 <sec:method-security/>
中添加 @EnableMethodSecurity
。
该注解和 XML 元素分别取代了
如果你正在使用 |
方法授权是方法“前”授权和方法“后”授权的结合。考虑一个按以下方式注解的 service 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
调用了代理方法。在代理的其他 advice 中,它调用了一个AuthorizationManagerBeforeMethodInterceptor
,该方法与@PreAuthorize
pointcut 匹配。 -
授权管理器使用
MethodSecurityExpressionHandler
解析注解的 SpEL 表达式,并从包含Supplier<Authentication>
和MethodInvocation
的MethodSecurityExpressionRoot
构建相应的EvaluationContext
。 -
拦截器使用该上下文来评估表达式;具体地说,它从
Supplier
读取 Authentication,并检查其 权限 集合中是否有permission:read
。 -
如果评估通过,Spring AOP 将继续调用该方法。
-
如果没有,拦截器会发布一个
AuthorizationDeniedEvent
,并抛出一个AccessDeniedException
,ExceptionTranslationFilter
会捕获并向响应返回一个403状态码。 -
在方法返回后,Spring AOP 调用一个与
@PostAuthorize
pointcut 相匹配的AuthorizationManagerAfterMethodInterceptor
,操作与上面相同,但使用了PostAuthorizeAuthorizationManager
。 -
如果评估通过(在这种情况下,返回值属于登录的用户),处理继续正常进行。
-
\如果没有,拦截器会发布一个
AuthorizationDeniedEvent
,并抛出一个AccessDeniedException
,ExceptionTranslationFilter
会捕获并向响应返回一个 403 状态码。
如果该方法不是在 HTTP 请求的上下文中被调用,你可能需要自己处理 AccessDeniedException 。
|
多个注解串联计算
如上所示,如果方法调用涉及多个 方法安全注解,则每个注解一次处理一个。这意味着它们可以被视为 "&&" 在一起。换句话说,要对调用进行授权,所有注解检查都需要通过授权。
每个注解都有自己的 Pointcut
每个注解都有自己的 pointcut 实例,从方法及其外层类开始,在整个对象层次结构中查找该注解或其对应的 元注解。
你可以在 AuthorizationMethodPointcuts
中查看具体细节。
每个注解都有自己的方法拦截器
每个注解都有自己专用的方法拦截器。这样做的原因是为了提高可组合性。例如,如果需要,你可以禁用 Spring Security 默认值, 只发布 @PostAuthorize
方法拦截器。
方法拦截器如下
-
对于
@PreAuthorize
,Spring Security 使用AuthenticationManagerBeforeMethodInterceptor#preAuthorize
,而它又使用PreAuthorizeAuthorizationManager
。 -
对于
@PostAuthorize
,Spring Security 使用AuthenticationManagerAfterMethodInterceptor#postAuthorize
,而它又使用PostAuthorizeAuthorizationManager
。 -
对于
@PreFilter
,Spring Security 使用PreFilterAuthorizationMethodInterceptor
。 -
对于
@PostFilter
,Spring Security 使用PostFilterAuthorizationMethodInterceptor
。 -
对于
@Secured
,Spring Security 使用AuthenticationManagerBeforeMethodInterceptor#secured
,而它又使用SecuredAuthorizationManager
。 -
对于 JSR-250 注解,Spring Security 使用
AuthenticationManagerBeforeMethodInterceptor#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')")
然而,你可以将权限 permission:read
授予那些具有 ROLE_ADMIN
的用户。一种方法是使用角色层次结构(RoleHierarchy
)来实现:
-
Java
-
Kotlin
-
Xml
@Bean
static RoleHierarchy roleHierarchy() {
return new RoleHierarchyImpl("ROLE_ADMIN > permission:read");
}
companion object {
@Bean
fun roleHierarchy(): RoleHierarchy {
return RoleHierarchyImpl("ROLE_ADMIN > permission:read")
}
}
<bean id="roleHierarchy" class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
<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(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
对于声明所需的授权很有帮助,但它也可用于评估 涉及方法参数 的更复杂的表达式。
以上两个代码段通过将 username 参数与 Authentication#getName
进行比较,确保用户只能请求属于自己的订单。
其结果是,只有当请求路径中的 username
与登录用户 name
相匹配时,上述方法才会被调用。否则,Spring Security 将抛出 AccessDeniedException
并返回 403 状态码。
使用 @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
}
}
结果是,上述方法只有在账户的 owner
属性与登录用户的 name
匹配时才会返回 Account
。否则,Spring Security 将抛出 AccessDeniedException
并返回 403 状态码。
使用 @PreFilter
过滤方法参数
@PreFilter 还不支持特定于 Kotlin 的数据类型;因此,只显示了 Java 片段。
|
当方法安全激活时,你可以使用 @PreFilter
注解方法:
-
Java
@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;
}
}
这是为了过滤掉 filterObject.owner == authentication.name
表达式失败的 accounts
中的任何值。filterObject
代表 accounts
中的每个 account
,用于测试每个 account
。
然后,你可以通过以下方式测试该类,以确认它执行了授权规则:
-
Java
@Autowired
BankService bankService;
@WithMockUser(username="owner")
@Test
void updateAccountsWhenOwnedThenReturns() {
Account ownedBy = ...
Account notOwnedBy = ...
Collection<Account> updated = this.bankService.updateAccounts(ownedBy, notOwnedBy);
assertThat(updated).containsOnly(ownedBy);
}
@PreFilter 也可以是 元注解,定义在 类或接口级别,并使用 SpEL 授权表达式。
|
@PreFilter
支持数组、集合、map 和 stream(只要s tream 仍然打开)。
例如,上面的 updateAccounts
声明与下面的其他四个声明的方法相同:
-
Java
@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Account[] accounts)
@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Collection<Account> accounts)
@PreFilter("filterObject.value.owner == authentication.name")
public Collection<Account> updateAccounts(Map<String, Account> accounts)
@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Stream<Account> accounts)
其结果是,上述方法将只拥有其 owner
属性与登录用户 name
相匹配的 Account
实例。
使用 @PostFilter
过滤方法结果
@PostFilter 还不支持特定于 Kotlin 的数据类型;因此,只显示了 Java 片段。
|
当方法安全处于激活状态时,你可以使用 @PostFilter
注解方法:
-
Java
@Component
public class BankService {
@PostFilter("filterObject.owner == authentication.name")
public Collection<Account> readAccounts(String... ids) {
// ... the return value will be filtered to only contain the accounts owned by the logged-in user
return accounts;
}
}
这是为了从 filterObject.owner == authentication.name
表达式失败的返回值中过滤掉任何值。filterObject
代表 accounts
中的每个 account
,用于测试每个 account
。
然后你可以像这样测试该类,以确认它执行了授权规则:
-
Java
@Autowired
BankService bankService;
@WithMockUser(username="owner")
@Test
void readAccountsWhenOwnedThenReturns() {
Collection<Account> accounts = this.bankService.updateAccounts("owner", "not-owner");
assertThat(accounts).hasSize(1);
assertThat(accounts.get(0).getOwner()).isEqualTo("owner");
}
@PostFilter 也可以是 元注解,定义在 类或接口级别 ,并使用 SpEL 授权表达式。
|
@PostFilter
支持数组、集合、map 和 stream(只要 stream 仍然打开)。
例如,上面的 readAccounts
声明与下面其他三个声明的函数相同:
@PostFilter("filterObject.owner == authentication.name")
public Account[] readAccounts(String... ids)
@PostFilter("filterObject.value.owner == authentication.name")
public Map<String, Account> readAccounts(String... ids)
@PostFilter("filterObject.owner == authentication.name")
public Stream<Account> readAccounts(String... ids)
结果是,上述方法将返回其 owner
属性与登录用户 name
相匹配的 Account
实例。
内存过滤显然会很昂贵,因此要考虑是否最好 在数据层中过滤数据。 |
使用 @Secured
授权方法调用
@Secured
是授权调用的传统选项。我们推荐使用 @PreAuthorize
来取代它。
要使用 @Secured
注解,应首先更改方法的安全声明,使其能够像这样使用:
-
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 注解,应首先更改 Method Security 声明以启用注解:
-
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
}
}
这使得方法定义更加易读。
启用某些注解
你可以关闭 @EnableMethodSecurity
的预配置并用你自己的配置替换它。如果你想 自定义 AuthorizationManager
或 Pointcut
,你可以选择这样做。或者你可能只想启用特定的注解,如 @PostAuthorize
。
你可以通过以下方式实现:
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize() {
return AuthorizationManagerBeforeMethodInterceptor.postAuthorize();
}
}
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun postAuthorize() : Advisor {
return AuthorizationManagerBeforeMethodInterceptor.postAuthorize()
}
}
<sec:method-security pre-post-enabled="false"/>
<aop:config/>
<bean id="postAuthorize"
class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
factory-method="postAuthorize"/>
上面的代码段首先禁用了 Method Security 的预配置,然后发布了 @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>
这仅支持通过前缀或名称匹配方法。如果你的需求比这更复杂,请使用 注解支持。 |
以编程方式授权方法
正如你已经看到的,有几种方法可以让你使用 Method Security 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(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语言。
使用自定义授权管理器
以编程方式授权方法的第二种方法是创建一个自定义的 AuthorizationManager
。
首先,声明一个授权管理器实例,也许像这样:
-
Java
-
Kotlin
@Component
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation> {
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
// ... authorization logic
}
}
@Component("authz")
open class MyAuthorizationManager: AuthorizationManager<MethodInvocation> {
fun check(val authentication: Supplier<Authentication>, val invocation: MethodInvocation): AuthorizationDecision {
// ... authorization logic
}
}
然后,发布方法拦截器,并在其上添加一个与你希望授权管理器运行时间相对应的快捷方式。例如,你可以这样替换 @PreAuthorize
和 @PostAuthorize
的工作方式:
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize(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="postAuthorize"
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(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>
我们使用 |
You can also subclass DefaultMessageSecurityExpressionHandler
to add your own custom authorization expressions beyond the defaults.
使用 AspectJ 授权
使用自定义的 Pointcut 匹配方法
基于 Spring AOP 构建,你可以声明与注解无关的模式,类似于 请求级授权。这具有集中方法级授权规则的潜在优势。
例如,你可以使用发布自己的 Advisor
或使用 <protect-pointcut>
将 AOP 表达式与服务层的授权规则相匹配:
-
Java
-
Kotlin
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor protectServicePointcut() {
JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
pattern.setPattern("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 {
var pattern = JdkRegexpMethodPointcut();
pattern.setPattern("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 advice 织入到 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 advice 的形式发布其 advisor,以便相应地织入它们。
指定顺序
如前所述,每个注解都有一个 Spring AOP 方法拦截器,每个方法在 Spring AOP advisor 链中都有一个位置。
也就是说,@PreFilter
方法拦截器的顺序是 100,@PreAuthorize
的顺序是 200,以此类推。
之所以需要注意这一点,是因为还有其他基于 AOP 的注解,如 @EnableTransactionManagement
,其顺序为 Integer.MAX_VALUE
。换句话说,它们默认位于 advisor 链的末尾。
有时,在 Spring Security 之前执行其他 advice 可能很有价值。例如,如果你有一个注解了 @Transactional
和 @PostAuthorize
的方法,你可能希望当 @PostAuthorize
运行时事务仍然打开,这样 AccessDeniedException
就会导致回滚。
要使 @EnableTransactionManagement
在方法授权 advice 运行之前打开事务,可以这样设置 @EnableTransactionManagement
的顺序:
-
Java
-
Kotlin
-
Xml
@EnableTransactionManagement(order = 0)
@EnableTransactionManagement(order = 0)
<tx:annotation-driven ref="txManager" order="0"/>
由于最早的方法拦截器(@PreFilter
)被设置为100,设置为 0 意味着事务 advice 将在所有Spring Security advice 之前运行。
使用 SpEL 表达授权
你已经看过几个使用SpEL的例子,现在让我们更深入地介绍一下API。
Spring Security 将所有授权字段和方法封装在一组根(root)对象中。最通用的根对象称为 SecurityExpressionRoot
,它构成了 MethodSecurityExpressionRoot
的基础。当准备评估授权表达式时,Spring Security 会将该根对象提供给 MethodSecurityEvaluationContext
。
使用授权表达式字段和方法
这首先为你的 SpEL 表达式提供了一组增强的授权字段和方法。下面是对最常用方法的快速概述:
-
permitAll
- 该方法无需授权即可调用;请注意,在这种情况下,将不会从 session 中获取Authentication
信息。 -
denyAll
- 在任何情况下都不允许使用该方法;请注意,在这种情况下,将永远不会从 session 中检索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>
1 | 任何人不得以任何理由调用该方法。 |
2 | 该方法只能由被授予 ROLE_ADMIN 权限的 Authentication 调用。 |
3 | 该方法只能由被授予 db 和 ROLE_ADMIN 权限的 Authentication 调用。 |
4 | 本方法仅可由 aud claim 等于 "my-audience" 的 Princpal 调用。 |
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
来实现的,你可以自定义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
来实现的,你可以自定义AnnotationParameterNameDiscoverer
以支持任何指定注解的值属性。 -
-
如果你使用
-parameters
参数编译代码,标准的JDK反射API将被用来发现参数名。这对类和接口都有效。 -
最后,如果使用 debug 符号编译代码,参数名将通过 debug 符号被发现。这对接口无效,因为它们没有关于参数名的 debug 信息。对于接口,必须使用注解或
-parameters
方法。
-
从 @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/>
对于不使用 pre-post 注解的应用程序,请确保将其关闭,以避免激活不需要的行为。
例如,以下列表:
-
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
时,Spring Security 会自动利用这一点。
然而,假设你的代码继承了 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;
}
}