Method Security
除了 modeling authorization at the request level 之外,Spring Security 还支持在方法级别上进行建模。
In addition to modeling authorization at the request level, Spring Security also supports modeling at the method level.
您可以通过使用 @EnableMethodSecurity
为任意 @Configuration
类添加注释或将 <method-security>
添加到任何 XML 配置文件来在应用程序中激活它,如下所示:
You can activate it in your application by annotating any @Configuration
class with @EnableMethodSecurity
or adding <method-security>
to any XML configuration file, like so:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity
@EnableMethodSecurity
<sec:method-security/>
然后,您可以立即使用 <<`@PreAuthorize`,use-preauthorize>>、<<`@PostAuthorize`,use-postauthorize>>、<<`@PreFilter`,use-prefilter>> 和 <<`@PostFilter`,use-postfilter>> 为任何 Spring 管理的类或方法添加注释,以授权方法调用,包括输入参数和返回值。
Then, you are immediately able to annotate any Spring-managed class or method with <<`@PreAuthorize`,use-preauthorize>>, <<`@PostAuthorize`,use-postauthorize>>, <<`@PreFilter`,use-prefilter>>, and <<`@PostFilter`,use-postfilter>> to authorize method invocations, including the input parameters and return values.
Spring Boot Starter Security 默认情况下不会启动方法级授权。 |
Spring Boot Starter Security does not activate method-level authorization by default. |
方法安全还支持许多其他用例,包括 AspectJ support、custom annotations 和多个配置点。考虑了解以下用例:
Method Security supports many other use cases as well including use-aspectj, use-programmatic-authorization, and several configuration points. Consider learning about the following use cases:
-
Understanding method-security-architecture and reasons to use it
-
Comparing request-vs-method
-
Authorizing methods with <<`@PreAuthorize`,use-preauthorize>> and <<`@PostAuthorize`,use-postauthorize>>
-
Filtering methods with <<`@PreFilter`,use-prefilter>> and <<`@PostFilter`,use-postfilter>>
-
Authorizing methods with use-jsr250
-
Authorizing methods with use-aspectj
-
Integrating with weave-aspectj
-
Coordinating with <<@Transactional and other AOP-based annotations,changing-the-order>>
-
Customizing customizing-expression-handling
-
Integrating with custom-authorization-managers
How Method Security Works
Spring Security 的方法授权支持适用于:
Spring Security’s method authorization support is handy for:
-
Extracting fine-grained authorization logic; for example, when the method parameters and return values contribute to the authorization decision.
-
Enforcing security at the service layer
-
Stylistically favoring annotation-based over
HttpSecurity
-based configuration
而且,由于方法安全是使用 {spring-framework-reference-url}core.html#aop-api[Spring AOP] 构建的,因此你可以访问所有 Spring Security 默认设置所需的所有表现力强的功能以进行替代。
And since Method Security is built using {spring-framework-reference-url}core.html#aop-api[Spring AOP], you have access to all its expressive power to override Spring Security’s defaults as needed.
如前所述,您首先将 @EnableMethodSecurity
添加到 @Configuration
类或 Spring XML 配置文件中的 <sec:method-security/>
。
As already mentioned, you begin by adding @EnableMethodSecurity
to a @Configuration
class or <sec:method-security/>
in a Spring XML configuration file.
此注释和 XML 元素分别取代了 This annotation and XML element supercede
如果您正在使用 If you are using |
方法授权是方法之前授权和方法之后授权的组合。考虑以下方式添加注释的服务 Bean:
Method authorization is a combination of before- and after-method authorization. Consider a service bean that is annotated in the following way:
-
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 { ... }
}
当方法安全 is activated 时,对 MyCustomerService#readCustomer
的给定调用可能如下所示:
A given invocation to MyCustomerService#readCustomer
may look something like this when Method Security activate-method-security:
-
Spring AOP invokes its proxy method for
readCustomer
. Among the proxy’s other advisors, it invokes an {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor/html[AuthorizationManagerBeforeMethodInterceptor
] that matches annotation-method-pointcuts -
The interceptor invokes {security-api-url}org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.html[
PreAuthorizeAuthorizationManager#check
] -
The authorization manager uses a
MethodSecurityExpressionHandler
to parse the annotation’s authorization-expressions and constructs a correspondingEvaluationContext
from aMethodSecurityExpressionRoot
containing aSupplier<Authentication>
andMethodInvocation
. -
The interceptor uses this context to evaluate the expression; specifically, it reads the
Authentication
from theSupplier
and checks whether it haspermission:read
in its collection of authorities -
If the evaluation passes, then Spring AOP proceeds to invoke the method.
-
If not, the interceptor publishes an
AuthorizationDeniedEvent
and throws an {security-api-url}org/springframework/security/access/AccessDeniedException.html[AccessDeniedException
] which theExceptionTranslationFilter
catches and returns a 403 status code to the response -
After the method returns, Spring AOP invokes an {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.html[
AuthorizationManagerAfterMethodInterceptor
] that matches annotation-method-pointcuts, operating the same as above, but with {security-api-url}org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.html[PostAuthorizeAuthorizationManager
] -
If the evaluation passes (in this case, the return value belongs to the logged-in user), processing continues normally
-
If not, the interceptor publishes an
AuthorizationDeniedEvent
and throws an {security-api-url}org/springframework/security/access/AccessDeniedException.html[AccessDeniedException
], which theExceptionTranslationFilter
catches and returns a 403 status code to the response
如果方法未在 HTTP 请求的上下文中被调用,您可能需要自己处理 |
If the method is not being called in the context of an HTTP request, you will likely need to handle the |
Multiple Annotations Are Computed In Series
如上所示,如果方法调用涉及多个 Method Security annotations,则其中每个方法将被一次处理。这意味着它们可以被共同视为“与”在一起。换句话说,对于要授权的调用,所有注释检查都需要通过授权。
As demonstrated above, if a method invocation involves multiple authorizing-with-annotations, each of those is processed one at a time. This means that they can collectively be thought of as being "anded" together. In other words, for an invocation to be authorized, all annotation inspections need to pass authorization.
Repeated Annotations Are Not Supported
也就是说,不支持在同一方法上重复使用相同的注释。例如,您无法对同一方法两次放置 @PreAuthorize
。
That said, it is not supported to repeat the same annotation on the same method.
For example, you cannot place @PreAuthorize
twice on the same method.
相反,使用 SpEL 的布尔支持或使用其支持委派到单独的 Bean。
Instead, use SpEL’s boolean support or its support for delegating to a separate bean.
Each Annotation Has Its Own Pointcut
每个注释都有自己的切入点实例,用于在整个对象层次结构中查找该注释或其 meta-annotation 对应项,从 the method and its enclosing class 开始。
Each annotation has its own pointcut instance that looks for that annotation or its meta-annotations counterparts across the entire object hierarchy, starting at class-or-interface-annotations.
你可以在 {security-api-url}org/springframework/security/authorization/method/AuthorizationMethodPointcuts.html[AuthorizationMethodPointcuts
] 中看到此问题的具体内容。
You can see the specifics of this in {security-api-url}org/springframework/security/authorization/method/AuthorizationMethodPointcuts.html[AuthorizationMethodPointcuts
].
Each Annotation Has Its Own Method Interceptor
每个注释都有自己专用的方法拦截器。这样做的原因是让事情变得更易于组合。例如,如果需要,您可以禁用 Spring Security 默认设置和 publish only the @PostAuthorize
method interceptor。
Each annotation has its own dedicated method interceptor. The reason for this is to make things more composable. For example, if needed, you can disable the Spring Security defaults and _enabling_certain_annotations.
方法拦截器如下:
The method interceptors are as follows:
-
For <<`@PreAuthorize`,use-preauthorize>>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[
AuthorizationManagerBeforeMethodInterceptor#preAuthorize
], which in turn uses {security-api-url}org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.html[PreAuthorizeAuthorizationManager
] -
For <<`@PostAuthorize`,use-postauthorize>>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.html[
AuthorizationManagerBeforeMethodInterceptor#postAuthorize
], which in turn uses {security-api-url}org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.html[PostAuthorizeAuthorizationManager
] -
For <<`@PreFilter`,use-prefilter>>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.html[
PreFilterAuthorizationMethodInterceptor
] -
For <<`@PostFilter`,use-postfilter>>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.html[
PostFilterAuthorizationMethodInterceptor
] -
For <<`@Secured`,use-secured>>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[
AuthorizationManagerBeforeMethodInterceptor#secured
], which in turn uses {security-api-url}org/springframework/security/authorization/method/SecuredAuthorizationManager.html[SecuredAuthorizationManager
] -
For JSR-250 annotations, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[
AuthorizationManagerBeforeMethodInterceptor#jsr250
], which in turn uses {security-api-url}org/springframework/security/authorization/method/Jsr250AuthorizationManager.html[Jsr250AuthorizationManager
]
通常,您可以将下列内容视为 Spring Security 在添加 @EnableMethodSecurity
时发布的拦截器的代表:
Generally speaking, you can consider the following listing as representative of what interceptors Spring Security publishes when you add @EnableMethodSecurity
:
-
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();
}
Favor Granting Authorities Over Complicated SpEL Expressions
通常经常会忍不住引入像以下内容这样的复杂 SpEL 表达式:
Quite often it can be tempting to introduce a complicated SpEL expression like the following:
-
Java
@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')")
@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')")
但是,您可以给 permission:read
授予 ROLE_ADMIN
。完成此操作的方法之一就是使用以下类似 RoleHierarchy
:
However, you could instead grant permission:read
to those with ROLE_ADMIN
.
One way to do this is with a RoleHierarchy
like so:
-
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>
然后 set that in a MethodSecurityExpressionHandler
instance。这样,您可以拥有更简单的 <<`@PreAuthorize`,use-preauthorize>> 表达式,如下所示:
and then customizing-expression-handling. This then allows you to have a simpler <<`@PreAuthorize`,use-preauthorize>> expression like this one:
-
Java
-
Kotlin
@PreAuthorize("hasAuthority('permission:read')")
@PreAuthorize("hasAuthority('permission:read')")
或者,在可能的情况下,在登录时将特定应用的授权逻辑改编成权限。
Or, where possible, adapt application-specific authorization logic into granted authorities at login time.
Comparing Request-level vs Method-level Authorization
你应该何时优先考虑方法级授权而不是 request-level authorization?这在一定程度上取决于品味;但是,请考虑通过以下优势列表来帮助你做出决定。
When should you favor method-level authorization over request-level authorization? Some of it comes down to taste; however, consider the following strengths list of each to help you decide.
request-level |
method-level |
|
authorization type |
coarse-grained |
fine-grained |
configuration location |
declared in a config class |
local to method declaration |
configuration style |
DSL |
Annotations |
authorization definitions |
programmatic |
SpEL |
主要取舍在于您希望将授权规则置于何处。
The main tradeoff seems to be where you want your authorization rules to live.
值得记住的是,当你使用基于注释的方法安全性时,未注释的方法是不安全的。为保护此类方法,在你的 |
It’s important to remember that when you use annotation-based Method Security, then unannotated methods are not secured.
To protect against this, declare a catch-all authorization rule in your |
Authorizing with Annotations
Spring Security 支持方法级授权的主要方式是,通过您可以添加到方法、类和接口的注解来实现。
The primary way Spring Security enables method-level authorization support is through annotations that you can add to methods, classes, and interfaces.
Authorizing Method Invocation with @PreAuthorize
当 Method Security is active 时,你可以使用 {security-api-url}org/springframework/security/access/prepost/PreAuthorize.html[@PreAuthorize
] 注解对方法进行注解,如下所示:
When activate-method-security, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PreAuthorize.html[@PreAuthorize
] annotation like so:
-
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')
通过时,才能调用方法。
This is meant to indicate that the method can only be invoked if the provided expression hasRole('ADMIN')
passes.
然后,你可以 test the class 以确认其正在强制执行授权规则,如下所示:
You can then test the class to confirm it is enforcing the authorization rule like so:
-
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
对于声明所需的权限很有帮助,但也可以用于评估更复杂的 expressions that involve the method parameters。
While @PreAuthorize
is quite helpful for declaring needed authorities, it can also be used to evaluate more complex using_method_parameters.
Authorization Method Results with @PostAuthorize
方法安全处于活动状态时,你可以使用 {security-api-url}org/springframework/security/access/prepost/PostAuthorize.html[@PostAuthorize
] 注解对方法进行注解,如下所示:
When Method Security is active, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PostAuthorize.html[@PostAuthorize
] annotation like so:
-
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
对象。
This is meant to indicate that the method can only return the value if the provided expression returnObject.owner == authentication.name
passes.
returnObject
represents the Account
object to be returned.
然后,你可以 test the class 以确认其正在强制执行授权规则:
You can then test the class to confirm it is enforcing the authorization rule:
-
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
在防御 Insecure Direct Object Reference 时特别有用。事实上,它可以定义为 meta-annotation,如下所示:
@PostAuthorize
is particularly helpful when defending against Insecure Direct Object Reference.
In fact, it can be defined as a meta-annotations like so:
-
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
这样,您就可以用以下方式对服务进行注解:
Allowing you to instead annotate the service in the following way:
-
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 状态代码。
The result is that the above method will only return the Account
if its owner
attribute matches the logged-in user’s name
.
If not, Spring Security will throw an AccessDeniedException
and return a 403 status code.
Filtering Method Parameters with @PreFilter
不支持特定于 Kotlin 的数据类型,因此只显示 Java 代码段 |
|
方法安全处于活动状态时,你可以使用 {security-api-url}org/springframework/security/access/prepost/PreFilter.html[@PreFilter
] 注解对方法进行注解,如下所示:
When Method Security is active, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PreFilter.html[@PreFilter
] annotation like so:
-
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;
}
}
这是为了筛选来自 accounts
的所有值,其中表达式 filterObject.owner == authentication.name
失败。filterObject
表示 accounts
中的每个 account
,用于测试每个 account
。
This is meant to filter out any values from accounts
where the expression filterObject.owner == authentication.name
fails.
filterObject
represents each account
in accounts
and is used to test each account
.
然后,你可以以下列方式测试该类以确认是否强制执行授权规则:
You can then test the class in the following way to confirm it is enforcing the authorization rule:
-
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
支持数组、集合、映射和流(只要流仍然处于打开状态)。
@PreFilter
supports arrays, collections, maps, and streams (so long as the stream is still open).
例如,上述 updateAccounts
声明将与以下其他四个以相同的方式执行:
For example, the above updateAccounts
declaration will function the same way as the following other four:
-
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
实例相匹配。
The result is that the above method will only have the Account
instances where their owner
attribute matches the logged-in user’s name
.
Filtering Method Results with @PostFilter
不支持特定于 Kotlin 的数据类型,因此只显示 Java 代码段 |
|
方法安全处于活动状态时,你可以使用 {security-api-url}org/springframework/security/access/prepost/PostFilter.html[@PostFilter
] 注解对方法进行注解,如下所示:
When Method Security is active, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PostFilter.html[@PostFilter
] annotation like so:
-
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
。
This is meant to filter out any values from the return value where the expression filterObject.owner == authentication.name
fails.
filterObject
represents each account
in accounts
and is used to test each account
.
然后,你可以这样测试该类以确认是否强制执行授权规则:
You can then test the class like so to confirm it is enforcing the authorization rule:
-
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
支持数组、集合、映射和流(只要流仍然处于打开状态)。
@PostFilter
supports arrays, collections, maps, and streams (so long as the stream is still open).
例如,上述 readAccounts
声明将与以下其他三个以相同的方式执行:
For example, the above readAccounts
declaration will function the same way as the following other three:
@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
实例相匹配。
The result is that the above method will return the Account
instances where their owner
attribute matches the logged-in user’s name
.
内存中过滤显然会耗费资源,所以要仔细考虑是否最好转而进行 filter the data in the data layer。 |
In-memory filtering can obviously be expensive, and so be considerate of whether it is better to filter the data in the data layer instead. |
Authorizing Method Invocation with @Secured
{security-api-url}org/springframework/security/access/annotation/Secured.html[@Secured
] 是用于授权调用的传统选项。<<`@PreAuthorize`,use-preauthorize>> 取代了它,并建议改为使用它。
{security-api-url}org/springframework/security/access/annotation/Secured.html[@Secured
] is a legacy option for authorizing invocations.
<<`@PreAuthorize`,use-preauthorize>> supercedes it and is recommended instead.
要使用 @Secured
注释,你应该首先更改方法安全性声明以像这样启用它:
To use the @Secured
annotation, you should first change your Method Security declaration to enable it like so:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity(securedEnabled = true)
@EnableMethodSecurity(securedEnabled = true)
<sec:method-security secured-enabled="true"/>
这将导致 Spring Security 发布 the corresponding method interceptor 以授权带 @Secured
注释的方法、类和接口。
This will cause Spring Security to publish annotation-method-interceptors that authorizes methods, classes, and interfaces annotated with @Secured
.
Authorizing Method Invocation with JSR-250 Annotations
如果你想使用 JSR-250 注解,Spring Security 也支持这样做。<<`@PreAuthorize`,use-preauthorize>> 具有更强的表现力,因此建议使用它。
In case you would like to use JSR-250 annotations, Spring Security also supports that. <<`@PreAuthorize`,use-preauthorize>> has more expressive power and is thus recommended.
要使用 JSR-250 注释,你应该首先更改方法安全性声明以像这样启用它们:
To use the JSR-250 annotations, you should first change your Method Security declaration to enable them like so:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity(jsr250Enabled = true)
@EnableMethodSecurity(jsr250Enabled = true)
<sec:method-security jsr250-enabled="true"/>
这将导致 Spring Security 发布 the corresponding method interceptor 以授权带 @RolesAllowed
、@PermitAll
和 @DenyAll
注释的方法、类和接口。
This will cause Spring Security to publish annotation-method-interceptors that authorizes methods, classes, and interfaces annotated with @RolesAllowed
, @PermitAll
, and @DenyAll
.
Declaring Annotations at the Class or Interface Level
还支持在类和接口级别使用方法安全性注释。
It’s also supported to have Method Security annotations at the class and interface level.
如果它像这样处于类级别:
If it is at the class level like so:
-
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 { ... }
}
那么所有方法都将继承类级别的行为。
then all methods inherit the class-level behavior.
或者,如果将其在类和方法级别同时声明如下所示:
Or, if it’s declared like the following at both the class and method level:
-
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 { ... }
}
那么声明注解的方法会覆盖类级别的注解。
then methods declaring the annotation override the class-level annotation.
对于接口也是如此,但如果某个类从两个不同的接口继承该注解,则启动将会失败,这是因为 Spring Security 无法得知您想要使用哪一个。
The same is true for interfaces, with the exception that if a class inherits the annotation from two different interfaces, then startup will fail. This is because Spring Security has no way to tell which one you want to use.
在这样的情况下,您可以通过将注解添加到具体方法来解决歧义。
In cases like this, you can resolve the ambiguity by adding the annotation to the concrete method.
Using Meta Annotations
方法安全性支持元注解。这意味着您可以使用任何注解,并根据您的特定应用程序用例来提高易读性。
Method Security supports meta annotations. This means that you can take any annotation and improve readability based on your application-specific use cases.
例如,您可以将 @PreAuthorize("hasRole('ADMIN')")
简化为 @IsAdmin
,如下所示:
For example, you can simplify @PreAuthorize("hasRole('ADMIN')")
to @IsAdmin
like so:
-
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
结果是现在您可以对您的安全方法执行以下操作:
And the result is that on your secured methods you can now do the following instead:
-
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
}
}
这会得到更具可读性的方法定义。
This results in more readable method definitions.
Templating Meta-Annotation Expressions
您还可以选择使用元注解模板,这允许获得更强大的注解定义。
You can also opt into using meta-annotation templates, which allow for much more powerful annotation definitions.
首先,发布以下 bean:
First, publish the following bean:
-
Java
-
Kotlin
@Bean
static PrePostTemplateDefaults prePostTemplateDefaults() {
return new PrePostTemplateDefaults();
}
companion object {
@Bean
fun prePostTemplateDefaults(): PrePostTemplateDefaults {
return PrePostTemplateDefaults()
}
}
现在,您可以创建更强大的 @HasRole
,而不是 @IsAdmin
,如下所示:
Now instead of @IsAdmin
, you can create something more powerful like @HasRole
like so:
-
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 IsAdmin(val value: String)
结果是现在您可以对您的安全方法执行以下操作:
And the result is that on your secured methods you can now do the following instead:
-
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 表达式。
Note that this works with method variables and all annotation types, too, though you will want to be careful to correctly take care of quotation marks so the resulting SpEL expression is correct.
例如,考虑以下 @HasAnyRole
注解:
For example, consider the following @HasAnyRole
annotation:
-
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>)
在那种情况下,您会注意到您不应该在表达式中使用引号,而应该像使用参数值一样:
In that case, you’ll notice that you should not use the quotation marks in the expression, but instead in the parameter value like so:
-
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')")
。
so that, once replaced, the expression becomes @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
.
Enabling Certain Annotations
您可以关闭 @EnableMethodSecurity’s pre-configuration and replace it with you own.
You may choose to do this if you want to [customize the `AuthorizationManager
、custom-authorization-managers] 或 Pointcut
。或者您可能只是想要仅启用某个特定注解,例如 @PostAuthorize
。
You can turn off @EnableMethodSecurity’s pre-configuration and replace it with you own.
You may choose to do this if you want to custom-authorization-managers or
Pointcut
.
Or you may simply want to only enable a specific annotation, like @PostAuthorize
.
您可以按以下方式执行此操作:
You can do this in the following way:
-
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"/>
以上代码段通过首先禁用方法安全性的预配置,然后发布 the @PostAuthorize
interceptor 本身,来实现此目的。
The above snippet achieves this by first disabling Method Security’s pre-configurations and then publishing annotation-method-interceptors itself.
Authorizing with <intercept-methods>
虽然首选使用 Spring Security 的 annotation-based support 来进行方法安全性,但您也可以使用 XML 声明 bean 授权规则。
While using Spring Security’s authorizing-with-annotations is preferred for method security, you can also use XML to declare bean authorization rules.
如果您需要在您的 XML 配置中声明它,您可以像这样使用 “@21”:
If you need to declare it in your XML configuration instead, you can use <intercept-methods>
like so:
-
Xml
<bean class="org.mycompany.MyController">
<intercept-methods>
<protect method="get*" access="hasAuthority('read')"/>
<protect method="*" access="hasAuthority('write')"/>
</intercept-methods>
</bean>
这仅支持按前缀或名称匹配方法。如果您的需求比这更复杂,则 use annotation support。 |
This only supports matching method by prefix or by name. If your needs are more complex than that, authorizing-with-annotations instead. |
Authorizing Methods Programmatically
正如您已经看到的,使用Method Security SpEL expressions 来指定非平凡的授权规则有几种方法。
As you’ve already seen, there are several ways that you can specify non-trivial authorization rules using authorization-expressions.
您可以使用多种方法将您的逻辑改为基于 Java 而非基于 SpEL。这样可供您使用整个 Java 语言来提高可测试性和流程控制。
There are a number of ways that you can instead allow your logic to be Java-based instead of SpEL-based. This gives use access the entire Java language for increased testability and flow control.
Using a Custom Bean in SpEL
对方法进行编程授权的第一种方法是两步流程。
The first way to authorize a method programmatically is a two-step process.
首先,声明一个 bean,其中有一个方法采用 MethodSecurityExpressionOperations
实例,如下所示:
First, declare a bean that has a method that takes a MethodSecurityExpressionOperations
instance like the following:
-
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,如下所示:
Then, reference that bean in your annotations in the following way:
-
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 调用指定的方法。
Spring Security will invoke the given method on that bean for each method invocation.
这样做的好处是您的所有授权逻辑都位于一个独立的类中,可以独立进行单元测试并验证其正确性。它还可以访问完整的 Java 语言。
What’s nice about this is all your authorization logic is in a separate class that can be independently unit tested and verified for correctness. It also has access to the full Java language.
Using a Custom Authorization Manager
通过编程授权方法的第二种方式是创建自定义 “@23”。
The second way to authorize a method programmatically is to create a custom AuthorizationManager
.
首先,声明一个授权管理器实例,例如该实例:
First, declare an authorization manager instance, perhaps like this one:
-
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
的工作方式,如下所示:
Then, publish the method interceptor with a pointcut that corresponds to when you want that AuthorizationManager
to run.
For example, you could replace how @PreAuthorize
and @PostAuthorize
work like so:
-
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>
您可以使用 You can place your interceptor in between Spring Security method interceptors using the order constants specified in |
Customizing Expression Handling
或者第三,您可以自定义处理每个 SpEL 表达式的方式。为此,您可以公开一个自定义的 {security-api-url}org.springframework.security.access.expression.method.MethodSecurityExpressionHandler.html[MethodSecurityExpressionHandler
],如下所示:
Or, third, you can customize how each SpEL expression is handled.
To do that, you can expose a custom {security-api-url}org.springframework.security.access.expression.method.MethodSecurityExpressionHandler.html[MethodSecurityExpressionHandler
], like so:
-
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>
我们使用 We expose |
您还可以 subclass DefaultMessageSecurityExpressionHandler
来添加您自己的自定义授权表达式,而不仅仅是默认表达式。
You can also subclass-defaultmethodsecurityexpressionhandler to add your own custom authorization expressions beyond the defaults.
Authorizing with AspectJ
Matching Methods with Custom Pointcuts
由于构建于 Spring AOP 之上,您可以声明与注释无关的模式,类似于 request-level authorization.这样做具有集中化方法级授权规则的潜在优势。
Being built on Spring AOP, you can declare patterns that are not related to annotations, similar to request-level authorization. This has the potential advantage of centralizing method-level authorization rules.
例如,您可以发布您自己的 “@24” 或使用 “@26” 将 AOP 表达式与服务层的授权规则匹配,如下所示:
For example, you can use publish your own Advisor
or use <protect-pointcut>
to match AOP expressions to authorization rules for your service layer like so:
-
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>
Integrate with AspectJ Byte-weaving
有时,可以通过使用 AspectJ 将 Spring Security 建议编织到 bean 的字节码中来提高性能。
Performance can at times be enhanced by using AspectJ to weave Spring Security advice into the byte code of your beans.
设置 AspectJ 之后,您只需在 @EnableMethodSecurity
注释或 <method-security>
元素中简单地说明您正在使用 AspectJ:
After setting up AspectJ, you can quite simply state in the @EnableMethodSecurity
annotation or <method-security>
element that you are using AspectJ:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity(mode=AdviceMode.ASPECTJ)
@EnableMethodSecurity(mode=AdviceMode.ASPECTJ)
<sec:method-security mode="aspectj"/>
结果是 Spring Security 会将其顾问发布为 AspectJ 建议,以便可以相应地将其编织进去。
And the result will be that Spring Security will publish its advisors as AspectJ advice so that they can be woven in accordingly.
Specifying Order
如前所述,每个注释都有一个 Spring AOP 方法拦截器,每个拦截器在 Spring AOP 顾问链中都有一个位置。
As already noted, there is a Spring AOP method interceptor for each annotation, and each of these has a location in the Spring AOP advisor chain.
例如,@PreFilter
方法拦截器的顺序为 100,@PreAuthorize
的顺序为 200,依此类推。
Namely, the @PreFilter
method interceptor’s order is 100, `@PreAuthorize’s is 200, and so on.
需要注意这一点的原因在于,还有其他基于 AOP 的注解(如 @EnableTransactionManagement
),其顺序为 Integer.MAX_VALUE
。换句话说,它们默认位于顾问链的末尾。
The reason this is important to note is that there are other AOP-based annotations like @EnableTransactionManagement
that have an order of Integer.MAX_VALUE
.
In other words, they are located at the end of the advisor chain by default.
有时,在 Spring Security 之前执行其他建议会很有价值。例如,如果你有一个使用 @Transactional
和 @PostAuthorize
进行注解的方法,你可能希望在 @PostAuthorize
运行时事务仍然处于打开状态,以便 AccessDeniedException
会导致回滚。
At times, it can be valuable to have other advice execute before Spring Security.
For example, if you have a method annotated with @Transactional
and @PostAuthorize
, you might want the transaction to still be open when @PostAuthorize
runs so that an AccessDeniedException
will cause a rollback.
要让 @EnableTransactionManagement
在方法授权建议运行之前打开事务,可以像这样设置 @EnableTransactionManagement
的顺序:
To get @EnableTransactionManagement
to open a transaction before method authorization advice runs, you can set `@EnableTransactionManagement’s order like so:
-
Java
-
Kotlin
-
Xml
@EnableTransactionManagement(order = 0)
@EnableTransactionManagement(order = 0)
<tx:annotation-driven ref="txManager" order="0"/>
由于最早的方法拦截器(@PreFilter
)的顺序设置为 100,因此将事务建议的顺序设置为 0 意味着它将在所有 Spring Security 建议之前运行。
Since the earliest method interceptor (@PreFilter
) is set to an order of 100, a setting of zero means that the transaction advice will run before all Spring Security advice.
Expressing Authorization with SpEL
你已经看到了几个使用 SpEL 的示例,现在让我们稍微深入地介绍一下 API。
You’ve already seen several examples using SpEL, so now let’s cover the API a bit more in depth.
Spring Security 将其所有授权字段和方法封装到一组根对象中。最通用的根对象称为 SecurityExpressionRoot
,它是 MethodSecurityExpressionRoot
的基础。在准备评估授权表达式时,Spring Security 会向 MethodSecurityEvaluationContext
提供此根对象。
Spring Security encapsulates all of its authorization fields and methods in a set of root objects.
The most generic root object is called SecurityExpressionRoot
and it forms the basis for MethodSecurityExpressionRoot
.
Spring Security supplies this root object to MethodSecurityEvaluationContext
when preparing to evaluate an authorization expression.
Using Authorization Expression Fields and Methods
此提供的第一个内容是为 SpEL 表达式提供了一组增强型授权字段和方法。以下是常用方法的快速概览:
The first thing this provides is an enhanced set of authorization fields and methods to your SpEL expressions. What follows is a quick overview of the most common methods:
-
permitAll
- The method requires no authorization to be invoked; note that in this case, theAuthentication
is never retrieved from the session -
denyAll
- The method is not allowed under any circumstances; note that in this case, theAuthentication
is never retrieved from the session -
hasAuthority
- The method requires that theAuthentication
have aGrantedAuthority
that matches the given value -
hasRole
- A shortcut forhasAuthority
that prefixesROLE_
or whatever is configured as the default prefix -
hasAnyAuthority
- The method requires that theAuthentication
have aGrantedAuthority
that matches any of the given values -
hasAnyRole
- A shortcut forhasAnyAuthority
that prefixesROLE_
or whatever is configured as the default prefix -
hasPermission
- A hook into yourPermissionEvaluator
instance for doing object-level authorization
以下是常用字段的简要介绍:
And here is a brief look at the most common fields:
-
authentication
- TheAuthentication
instance associated with this method invocation -
principal
- TheAuthentication#getPrincipal
associated with this method invocation
现在已经学习了模式、规则以及它们如何组合在一起,你应该能够理解此更复杂的示例中发生的事情:
Having now learned the patterns, rules, and how they can be paired together, you should be able to understand what is going on in this more complex example:
-
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 | This method may not be invoked by anyone for any reason |
2 | This method may only be invoked by Authentication`s granted the `ROLE_ADMIN authority |
3 | This method may only be invoked by Authentication`s granted the `db and ROLE_ADMIN authorities |
4 | This method may only be invoked by Princpal`s with an `aud claim equal to "my-audience" |
5 | This method may only be invoked if the bean authz’s `check method returns true |
Using Method Parameters
此外,Spring Security 提供了发现方法参数的机制,以便也可以在 SpEL 表达式中访问它们。
Additionally, Spring Security provides a mechanism for discovering method parameters so they can also be accessed in the SpEL expression as well.
为了获得一个完整的引用,Spring Security 使用 DefaultSecurityParameterNameDiscoverer
来发现参数名称。默认情况下,一个方法会尝试以下选项。
For a complete reference, Spring Security uses DefaultSecurityParameterNameDiscoverer
to discover the parameter names.
By default, the following options are tried for a method.
-
If Spring Security’s
@P
annotation is present on a single argument to the method, the value is used. The following example uses the@P
annotation:
- Java
-
import org.springframework.security.access.method.P; ... @PreAuthorize("hasPermission(#c, 'write')") public void updateContact(@P("c") Contact contact);
- Kotlin
-
import org.springframework.security.access.method.P ... @PreAuthorize("hasPermission(#c, 'write')") fun doSomething(@P("c") contact: Contact?)
此表达式的目的是要求当前 Authentication
针对此 Contact
实例专门具有 write
权限。
The intention of this expression is to require that the current Authentication
have write
permission specifically for this Contact
instance.
在后台,这是通过使用 AnnotationParameterNameDiscoverer
来实现的,你可以对其进行自定义以支持任何指定注解的值属性。
Behind the scenes, this is implemented by using AnnotationParameterNameDiscoverer
, which you can customize to support the value attribute of any specified annotation.
-
If Spring Data’s
@Param
annotation is present on at least one parameter for the method, the value is used. The following example uses the@Param
annotation:
- Java
-
import org.springframework.data.repository.query.Param; ... @PreAuthorize("#n == authentication.name") Contact findContactByName(@Param("n") String name);
- Kotlin
-
import org.springframework.data.repository.query.Param ... @PreAuthorize("#n == authentication.name") fun findContactByName(@Param("n") name: String?): Contact?
此表达式的目的是要求 name
等于 Authentication#getName
,才能授权调用。
The intention of this expression is to require that name
be equal to Authentication#getName
for the invocation to be authorized.
在后台,这是通过使用 AnnotationParameterNameDiscoverer
来实现的,你可以对其进行自定义以支持任何指定注解的值属性。
Behind the scenes, this is implemented by using AnnotationParameterNameDiscoverer
, which you can customize to support the value attribute of any specified annotation.
* If you compile your code with the -parameters
argument, the standard JDK reflection API is used to discover the parameter names.
This works on both classes and interfaces.
* Finally, if you compile your code with debug symbols, the parameter names are discovered by using the debug symbols.
This does not work for interfaces, since they do not have debug information about the parameter names.
For interfaces, either annotations or the -parameters
approach must be used.
Authorizing Arbitrary Objects
Spring Security 还支持包装对其方法安全注解进行注解的任何对象。
Spring Security also supports wrapping any object that is annotated its method security annotations.
为实现此目的,你可以自动装配提供的 AuthorizationProxyFactory
实例,该实例取决于你已配置哪些方法安全拦截器。如果你使用的是 @EnableMethodSecurity
,则这意味着它将默认具有 @PreAuthorize
、@PostAuthorize
、@PreFilter
和 @PostFilter
的拦截器。
To achieve this, you can autowire the provided AuthorizationProxyFactory
instance, which is based on which method security interceptors you have configured.
If you are using @EnableMethodSecurity
, then this means that it will by default have the interceptors for @PreAuthorize
, @PostAuthorize
, @PreFilter
, and @PostFilter
.
例如,考虑以下 User
类:
For example, consider the following User
class:
-
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)
你可以通过以下方式代理用户的实例:
You can proxy an instance of user in the following way:
-
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)
}
Manual Construction
如果你需要与 Spring Security 默认值不同的内容,你还可以定义自己的实例。
You can also define your own instance if you need something different from the Spring Security default.
例如,如果你定义的 AuthorizationProxyFactory
实例如:
For example, if you define an AuthorizationProxyFactory
instance like so:
-
Java
-
Kotlin
import static org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize;
// ...
AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize
// ...
val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize())
然后你可以封装 User
的任何实例如下:
Then you can wrap any instance of User
as follows:
-
Java
-
Kotlin
@Test
void getEmailWhenProxiedThenAuthorizes() {
AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
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 = AuthorizationProxyFactory(preAuthorize())
val user: User = User("name", "email")
assertThat(user.getEmail()).isNotNull()
val securedUser: User = proxyFactory.proxy(user)
assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
}
该特性当前不支持 Spring AOT This feature does not yet support Spring AOT |
Proxying Collections
AuthorizationProxyFactory
通过代理元素类型来支持 Java 集合、流、数组、可选值和迭代器,通过代理值类型来支持映射。
AuthorizationProxyFactory
supports Java collections, streams, arrays, optionals, and iterators by proxying the element type and maps by proxying the value type.
这意味着在代理对象 List
数组时,以下操作也可以使用:
This means that when proxying a List
of objects, the following also works:
-
Java
@Test
void getEmailWhenProxiedThenAuthorizes() {
AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize());
List<User> users = List.of(ada, albert, marie);
List<User> securedUsers = proxyFactory.proxy(users);
securedUsers.forEach((securedUser) ->
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail));
}
Proxying Classes
在有限的情况下,代理 Class
本身可能有价值,AuthorizationProxyFactory
也支持这一点。这大约等同于 Spring Framework 中用来创建代理的支持中的 ProxyFactory#getProxyClass
。
In limited circumstances, it may be valuable to proxy a Class
itself, and AuthorizationProxyFactory
also supports this.
This is roughly the equivalent of calling ProxyFactory#getProxyClass
in Spring Framework’s support for creating proxies.
预先构造代理类的某些情况下很有用,比如使用 Spring AOT 的时候。
One place where this is handy is when you need to construct the proxy class ahead-of-time, like with Spring AOT.
Support for All Method Security Annotations
AuthorizationProxyFactory
支持应用程序中启用的任何方法安全性标注。它是根据作为 bean 发布的任何 AuthorizationAdvisor
类。
AuthorizationProxyFactory
supports whichever method security annotations are enabled in your application.
It is based off of whatever AuthorizationAdvisor
classes are published as a bean.
由于 @EnableMethodSecurity
默认情况下发布 @PreAuthorize
、@PostAuthorize
、@PreFilter
和 @PostFilter
顾问,你通常不需要执行任何操作来激活该功能。
Since @EnableMethodSecurity
publishes @PreAuthorize
, @PostAuthorize
, @PreFilter
, and @PostFilter
advisors by default, you will typically need to do nothing to activate the ability.
使用 SpEL expressions that use |
Custom Advice
如果你有同样希望应用的安全建议,你可以发布自己的 AuthorizationAdvisor
如下:
If you have security advice that you also want applied, you can publish your own AuthorizationAdvisor
like so:
-
Java
-
Kotlin
@EnableMethodSecurity
class SecurityConfig {
@Bean
static AuthorizationAdvisor myAuthorizationAdvisor() {
return new AuthorizationAdvisor();
}
}
@EnableMethodSecurity
internal class SecurityConfig {
@Bean
fun myAuthorizationAdvisor(): AuthorizationAdvisor {
return AuthorizationAdvisor()
}
]
在 Spring 安全性添加代理对象时,Spring 安全性将该顾问添加到 AuthorizationProxyFactory
添加的顾问中。
And Spring Security will add that advisor into the set of advice that AuthorizationProxyFactory
adds when proxying an object.
Working with Jackson
该特性的一大强大用途是从控制器返回安全值,如下所示:
One powerful use of this feature is to return a secured value from a controller like so:
-
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)
}
}
但是如果你使用 Jackson,这可能会导致类似以下的序列化错误:
If you are using Jackson, though, this may result in a serialization error like the following:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:导致循环的直接自引用
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle
这是因为 Jackson 如何处理 CGLIB 代理。为了解决这个问题,在 User
类的开头添加以下标注:
This is due to how Jackson works with CGLIB proxies.
To address this, add the following annotation to the top of the User
class:
-
Java
-
Kotlin
@JsonSerialize(as = User.class)
public class User {
}
@JsonSerialize(`as` = User::class)
class User
最后,你需要发布 custom interceptor 来负责捕获每个字段抛出的 AccessDeniedException
,可以如下所示:
Finally, you will need to publish a custom_advice to catch the AccessDeniedException
thrown for each field, which you can do like so:
-
Java
-
Kotlin
@Component
public class AccessDeniedExceptionInterceptor implements AuthorizationAdvisor {
private final AuthorizationAdvisor advisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
return invocation.proceed();
} catch (AccessDeniedException ex) {
return null;
}
}
@Override
public Pointcut getPointcut() {
return this.advisor.getPointcut();
}
@Override
public Advice getAdvice() {
return this;
}
@Override
public int getOrder() {
return this.advisor.getOrder() - 1;
}
}
@Component
class AccessDeniedExceptionInterceptor: AuthorizationAdvisor {
var advisor: AuthorizationAdvisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize()
@Throws(Throwable::class)
fun invoke(invocation: MethodInvocation): Any? {
return try {
invocation.proceed()
} catch (ex:AccessDeniedException) {
null
}
}
val pointcut: Pointcut
get() = advisor.getPointcut()
val advice: Advice
get() = this
val order: Int
get() = advisor.getOrder() - 1
}
然后,你会看到基于用户授权级别的不同 JSON 序列化。如果没有 user:read
权限,则会看到:
Then, you’ll see a different JSON serialization based on the authorization level of the user.
If they don’t have the user:read
authority, then they’ll see:
{
"name" : "name",
"email" : null
}
而如果具有该权限,则会看到:
And if they do have that authority, they’ll see:
{
"name" : "name",
"email" : "email"
}
如果你也不想向未授权用户透露 JSON 密钥,还可以添加 Spring Boot 属性 You can also add the Spring Boot property |
Migrating from @EnableGlobalMethodSecurity
如果您正在使用 @EnableGlobalMethodSecurity
,则应迁移到 @EnableMethodSecurity
。
If you are using @EnableGlobalMethodSecurity
, you should migrate to @EnableMethodSecurity
.
Replace global method security with method security
{security-api-url}org/springframework/security/config/annotation/method/configuration/EnableGlobalMethodSecurity.html[@27] 和 “@32” 已弃用,支持 {security-api-url}org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.html[@29] 和 “@33”。新注解和 XML 元素默认激活 Spring 的 “@34”,并在内部使用 “@31”。
{security-api-url}org/springframework/security/config/annotation/method/configuration/EnableGlobalMethodSecurity.html[@EnableGlobalMethodSecurity
] and <global-method-security>
are deprecated in favor of {security-api-url}org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.html[@EnableMethodSecurity
] and <method-security>
, respectively.
The new annotation and XML element activate Spring’s pre-post annotations by default and use AuthorizationManager
internally.
这意味着以下两个列表在功能上是等效的:
This means that the following two listings are functionally equivalent:
-
Java
-
Kotlin
-
Xml
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
<global-method-security pre-post-enabled="true"/>
及:
and:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity
@EnableMethodSecurity
<method-security/>
对于不使用前后注释的应用程序,请务必关闭它以避免激活不需要的行为。
For applications not using the pre-post annotations, make sure to turn it off to avoid activating unwanted behavior.
例如,像这样的列表:
For example, a listing like:
-
Java
-
Kotlin
-
Xml
@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(securedEnabled = true)
<global-method-security secured-enabled="true"/>
应改为:
should change to:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
<method-security secured-enabled="true" pre-post-enabled="false"/>
Use a Custom @Bean
instead of subclassing DefaultMethodSecurityExpressionHandler
作为一种性能优化,新方法被引入到 MethodSecurityExpressionHandler
中,它采用 Supplier<Authentication>
而不是 Authentication
。
As a performance optimization, a new method was introduced to MethodSecurityExpressionHandler
that takes a Supplier<Authentication>
instead of an Authentication
.
这允许 Spring Security 推迟对 Authentication
的查找,并在使用 @EnableMethodSecurity
代替 @EnableGlobalMethodSecurity
时自动利用它。
This allows Spring Security to defer the lookup of the Authentication
, and is taken advantage of automatically when you use @EnableMethodSecurity
instead of @EnableGlobalMethodSecurity
.
但是,假设您的代码扩展了 DefaultMethodSecurityExpressionHandler
并覆盖了 createSecurityExpressionRoot(Authentication, MethodInvocation)
以返回自定义 SecurityExpressionRoot
实例。这将不再起作用,因为 @EnableMethodSecurity
设置的排列改为调用 createEvaluationContext(Supplier<Authentication>, MethodInvocation)
。
However, let’s say that your code extends DefaultMethodSecurityExpressionHandler
and overrides createSecurityExpressionRoot(Authentication, MethodInvocation)
to return a custom SecurityExpressionRoot
instance.
This will no longer work because the arrangement that @EnableMethodSecurity
sets up calls createEvaluationContext(Supplier<Authentication>, MethodInvocation)
instead.
幸运的是,这种级别的自定义通常是不必要的。相反,您可以使用所需的授权方法创建一个自定义 bean。
Happily, such a level of customization is often unnecessary. Instead, you can create a custom bean with the authorization methods that you need.
例如,假设您想要对 @PostAuthorize("hasAuthority('ADMIN')")
进行自定义评估。您可以像这样创建一个自定义 @Bean
:
For example, let’s say you are wanting a custom evaluation of @PostAuthorize("hasAuthority('ADMIN')")
.
You can create a custom @Bean
like this one:
-
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;
}
}
然后在注释中这样引用它:
and then refer to it in the annotation like so:
-
Java
-
Kotlin
@PreAuthorize("@authz.isAdmin(#root)")
@PreAuthorize("@authz.isAdmin(#root)")
I’d still prefer to subclass DefaultMethodSecurityExpressionHandler
如果您必须继续对 DefaultMethodSecurityExpressionHandler
进行子类化,您仍然可以这样做。相反,像这样覆盖 createEvaluationContext(Supplier<Authentication>, MethodInvocation)
方法:
If you must continue subclassing DefaultMethodSecurityExpressionHandler
, you can still do so.
Instead, override the createEvaluationContext(Supplier<Authentication>, MethodInvocation)
method like so:
-
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
}
}
Further Reading
现在您已经保护了应用程序的请求,如果您尚未执行此操作,请 secure its requests.您还可以进一步阅读 testing your application或 Spring Security 与应用程序其他方面的集成,如 the data layer或 tracing and metrics。
Now that you have secured your application’s requests, please secure its requests if you haven’t already. You can also read further on testing your application or on integrating Spring Security with other aspects of you application like the data layer or tracing and metrics.