对于最新的稳定版本,请使用 Spring Security 6.5.3! |
方法安全性
除了在请求级别对授权进行建模外,Spring Security 还支持在方法级别进行建模。
您可以通过注释任何@Configuration
类与@EnableMethodSecurity
或添加<method-security>
添加到任何 XML 配置文件,如下所示:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity
@EnableMethodSecurity
<sec:method-security/>
然后,您可以立即使用@PreAuthorize
,@PostAuthorize
,@PreFilter
和@PostFilter
授权方法调用,包括输入参数和返回值。
默认情况下,Spring Boot Starter Security 不激活方法级授权。 |
方法安全性还支持许多其他用例,包括 AspectJ 支持、自定义注释和多个配置点。 考虑了解以下用例:
-
了解方法安全性的工作原理以及使用它的原因
-
使用 JSR-250 注释授权方法
-
使用 AspectJ 表达式授权方法
-
与 AspectJ 字节码编织集成
-
自定义 SpEL 表达式处理
-
与自定义授权系统集成
方法安全性的工作原理
Spring Security 的方法授权支持适用于:
-
提取细粒度授权逻辑;例如,当方法参数和返回值有助于授权决策时。
-
在服务层强制实施安全性
-
在风格上偏爱基于注释的
HttpSecurity
基于配置
由于 Method Security 是使用 Spring AOP 构建的,因此您可以根据需要访问其所有表达能力来覆盖 Spring Security 的默认值。
如前所述,您首先添加@EnableMethodSecurity
设置为@Configuration
class 或<sec:method-security/>
在 Spring XML 配置文件中。
此注释和 XML 元素取代
如果您正在使用 |
方法授权是方法授权之前和之后的组合。 考虑一个以以下方式注释的服务 Bean:
-
Java
-
Kotlin
@Service
public class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
public Customer readCustomer(String id) { ... }
}
@Service
open class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
fun readCustomer(val id: String): Customer { ... }
}
给定的调用MyCustomerService#readCustomer
激活方法安全性时,可能如下所示:

-
Spring AOP 调用其代理方法
readCustomer
.在代理的其他顾问中,它调用AuthorizationManagerBeforeMethodInterceptor
匹配这@PreAuthorize
切入点 -
授权管理器使用
MethodSecurityExpressionHandler
解析注释的 SpEL 表达式并构造相应的EvaluationContext
从MethodSecurityExpressionRoot
含一个Supplier<Authentication>
和MethodInvocation
. -
拦截器使用此上下文来评估表达式;具体来说,它写道这
Authentication
从Supplier
并检查它是否具有permission:read
在其权威的集合中 -
如果评估通过,则 Spring AOP 将继续调用该方法。
-
如果没有,拦截器会发布一个
AuthorizationDeniedEvent
并抛出一个AccessDeniedException
哪这ExceptionTranslationFilter
捕获并返回响应的 403 状态代码 -
方法返回后,Spring AOP 调用
AuthorizationManagerAfterMethodInterceptor
匹配这@PostAuthorize
切入点,作方式与上述相同,但与PostAuthorizeAuthorizationManager
-
如果评估通过(在本例中,返回值属于登录用户),则处理将正常继续
-
如果没有,拦截器会发布一个
AuthorizationDeniedEvent
并抛出一个AccessDeniedException
哪这ExceptionTranslationFilter
捕获并返回响应的 403 状态代码
如果该方法未在 HTTP 请求的上下文中调用,则可能需要处理AccessDeniedException 你自己 |
多个注释是串联计算的
如上所述,如果方法调用涉及多个方法安全注释,则每次处理一个注释。 这意味着它们可以统称为“和”在一起。 换句话说,要授权调用,所有注释检查都需要通过授权。
每个注释都有自己的切入点
您可以在以下位置查看其具体内容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
一般来说,您可以将以下列表视为代表 Spring Security 在添加@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();
}
赞成授予权限而不是复杂的 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')")
或者,在可能的情况下,在登录时将特定于应用程序的授权逻辑调整到授予的权限中。
比较请求级授权与方法级授权
什么时候应该优先使用方法级授权而不是请求级授权? 其中一些归结为口味;但是,请考虑以下每种优势列表以帮助您做出决定。
请求级 |
方法级 |
|
授权类型 |
粗粒度 |
细粒度 |
配置位置 |
在配置类中声明 |
local to 方法声明 |
配置样式 |
DSL的 |
附注 |
授权定义 |
编程 |
斯佩尔 |
主要权衡似乎是您希望授权规则存在的位置。
请务必记住,当您使用基于注释的方法安全性时,未注释的方法不会受到保护。为了防止这种情况发生,请在HttpSecurity 实例。 |
使用注释进行授权
Spring Security 启用方法级授权支持的主要方式是通过可以添加到方法、类和接口的注释。
授权方法 调用@PreAuthorize
当方法安全性处于活动状态时,您可以使用@PreAuthorize
注释如下:
-
Java
-
Kotlin
@Component
public class BankService {
@PreAuthorize("hasRole('ADMIN')")
public Account readAccount(Long id) {
// ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority
}
}
@Component
open class BankService {
@PreAuthorize("hasRole('ADMIN')")
fun readAccount(val id: Long): Account {
// ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority
}
}
这表示只有在提供的表达式hasRole('ADMIN')
通过。
然后,您可以测试该类以确认它正在强制执行授权规则,如下所示:
-
Java
-
Kotlin
@Autowired
BankService bankService;
@WithMockUser(roles="ADMIN")
@Test
void readAccountWithAdminRoleThenInvokes() {
Account account = this.bankService.readAccount("12345678");
// ... assertions
}
@WithMockUser(roles="WRONG")
@Test
void readAccountWithWrongRoleThenAccessDenied() {
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(
() -> this.bankService.readAccount("12345678"));
}
@WithMockUser(roles="ADMIN")
@Test
fun readAccountWithAdminRoleThenInvokes() {
val account: Account = this.bankService.readAccount("12345678")
// ... assertions
}
@WithMockUser(roles="WRONG")
@Test
fun readAccountWithWrongRoleThenAccessDenied() {
assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
this.bankService.readAccount("12345678")
}
}
@PreAuthorize 也可以是元注释,在类或接口级别定义,并使用 SpEL 授权表达式。 |
而@PreAuthorize
对于声明所需的权限非常有帮助,它还可用于评估涉及方法参数的更复杂的表达式。
授权方法结果@PostAuthorize
当方法安全性处于活动状态时,您可以使用@PostAuthorize
注释如下:
-
Java
-
Kotlin
@Component
public class BankService {
@PostAuthorize("returnObject.owner == authentication.name")
public Account readAccount(Long id) {
// ... is only returned if the `Account` belongs to the logged in user
}
}
@Component
open class BankService {
@PostAuthorize("returnObject.owner == authentication.name")
fun readAccount(val id: Long): Account {
// ... is only returned if the `Account` belongs to the logged in user
}
}
这意味着该方法只能在提供的表达式returnObject.owner == authentication.name
通过。returnObject
表示Account
对象。
然后,您可以测试该类以确认它正在强制执行授权规则:
-
Java
-
Kotlin
@Autowired
BankService bankService;
@WithMockUser(username="owner")
@Test
void readAccountWhenOwnedThenReturns() {
Account account = this.bankService.readAccount("12345678");
// ... assertions
}
@WithMockUser(username="wrong")
@Test
void readAccountWhenNotOwnedThenAccessDenied() {
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(
() -> this.bankService.readAccount("12345678"));
}
@WithMockUser(username="owner")
@Test
fun readAccountWhenOwnedThenReturns() {
val account: Account = this.bankService.readAccount("12345678")
// ... assertions
}
@WithMockUser(username="wrong")
@Test
fun readAccountWhenNotOwnedThenAccessDenied() {
assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
this.bankService.readAccount("12345678")
}
}
@PostAuthorize 也可以是元注释,在类或接口级别定义,并使用 SpEL 授权表达式。 |
@PostAuthorize
在防御不安全的直接对象引用时特别有用。
事实上,它可以定义为如下所示的元注释:
-
Java
-
Kotlin
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PostAuthorize("returnObject.owner == authentication.name")
public @interface RequireOwnership {}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PostAuthorize("returnObject.owner == authentication.name")
annotation class RequireOwnership
允许您通过以下方式注释服务:
-
Java
-
Kotlin
@Component
public class BankService {
@RequireOwnership
public Account readAccount(Long id) {
// ... is only returned if the `Account` belongs to the logged in user
}
}
@Component
open class BankService {
@RequireOwnership
fun readAccount(val id: Long): Account {
// ... is only returned if the `Account` belongs to the logged in user
}
}
结果是上述方法只会返回Account
如果它的owner
属性与登录用户的name
.
如果没有,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;
}
}
这是为了过滤掉accounts
其中表达式filterObject.owner == authentication.name
失败。filterObject
代表每个account
在accounts
并用于测试每个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
支持数组、集合、映射和流(只要流仍然打开)。
例如,上面的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)
结果是,上述方法将只有Account
他们的实例owner
属性与登录用户的name
.
过滤方法结果@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
代表每个account
在accounts
并用于测试每个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
支持数组、集合、映射和流(只要流仍然打开)。
例如,上面的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)
结果是,上述方法将返回Account
他们的实例owner
属性与登录用户的name
.
内存中过滤显然可能很昂贵,因此请考虑是否最好在数据层中过滤数据。 |
授权方法 调用@Secured
@Secured
是授权调用的旧版选项。@PreAuthorize
取代它,建议改为。
要使用@Secured
注释,您应该首先更改方法安全声明以启用它,如下所示:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity(securedEnabled = true)
@EnableMethodSecurity(securedEnabled = true)
<sec:method-security secured-enabled="true"/>
这将导致 Spring Security 发布相应的方法拦截器,该拦截器授权方法、类和带有@Secured
.
使用 JSR-250 Comments 授权方法调用
如果您想使用 JSR-250 注释,Spring Security 也支持它。@PreAuthorize
具有更强的表现力,因此被推荐。
要使用 JSR-250 注解,您应该首先更改方法安全声明以启用它们,如下所示:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity(jsr250Enabled = true)
@EnableMethodSecurity(jsr250Enabled = true)
<sec:method-security jsr250-enabled="true"/>
这将导致 Spring Security 发布相应的方法拦截器,该拦截器授权方法、类和带有@RolesAllowed
,@PermitAll
和@DenyAll
.
在类或接口级别声明注释
还支持在类和接口级别使用方法安全注释。
如果它是在类级别,如下所示:
-
Java
-
Kotlin
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
@GetMapping("/endpoint")
public String endpoint() { ... }
}
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
open class MyController {
@GetMapping("/endpoint")
fun endpoint(): String { ... }
}
则所有方法都继承类级行为。
或者,如果它在类和方法级别都声明如下:
-
Java
-
Kotlin
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
@GetMapping("/endpoint")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String endpoint() { ... }
}
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
open class MyController {
@GetMapping("/endpoint")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
fun endpoint(): String { ... }
}
然后声明注释的方法会覆盖类级注释。
接口也是如此,但如果一个类从两个不同的接口继承了注释,则启动将失败。 这是因为 Spring Security 无法判断您要使用哪一个。
在这种情况下,您可以通过将注释添加到具体方法来解决歧义。
使用元注释
方法安全支持元注释。 这意味着您可以根据特定于应用程序的用例获取任何注释并提高可读性。
例如,您可以简化@PreAuthorize("hasRole('ADMIN')")
自@IsAdmin
这样:
-
Java
-
Kotlin
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
annotation class IsAdmin
结果是,在安全方法上,您现在可以执行以下作:
-
Java
-
Kotlin
@Component
public class BankService {
@IsAdmin
public Account readAccount(Long id) {
// ... is only returned if the `Account` belongs to the logged in user
}
}
@Component
open class BankService {
@IsAdmin
fun readAccount(val id: Long): Account {
// ... is only returned if the `Account` belongs to the logged in user
}
}
这导致方法定义更具可读性。
启用某些注释
您可以关闭@EnableMethodSecurity
的预配置,并将其替换为您自己的。
如果您愿意,可以选择这样做自定义AuthorizationManager
或Pointcut
.
或者您可能只想启用特定的注释,例如@PostAuthorize
.
您可以通过以下方式执行此作:
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize() {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
}
}
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun postAuthorize() : Advisor {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize()
}
}
<sec:method-security pre-post-enabled="false"/>
<aop:config/>
<bean id="postAuthorize"
class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
factory-method="postAuthorize"/>
上面的代码段通过首先禁用 Method Security 的预配置,然后发布这@PostAuthorize
拦截 器本身。
授权<intercept-methods>
虽然对于方法安全性来说,首选使用 Spring Security 基于 Comments 的支持,但您也可以使用 XML 来声明 bean 授权规则。
如果您需要在 XML 配置中声明它,您可以使用<intercept-methods>
这样:
-
Xml
<bean class="org.mycompany.MyController">
<intercept-methods>
<protect method="get*" access="hasAuthority('read')"/>
<protect method="*" access="hasAuthority('write')"/>
</intercept-methods>
</bean>
这仅支持按前缀或名称匹配方法。 如果您的需求比这更复杂,请改用注释支持。 |
以编程方式授权方法
如您所见,有几种方法可以使用方法安全 SpEL 表达式指定重要的授权规则。
有多种方法可以让你的逻辑基于 Java 而不是基于 SpEL。 这提供了对整个 Java 语言的访问,以提高可测试性和流控制。
在 SpEL 中使用自定义 Bean
以编程方式授权方法的第一种方法是两步过程。
首先,声明一个 bean,该 bean 具有一个方法,该方法采用MethodSecurityExpressionOperations
实例如下所示:
-
Java
-
Kotlin
@Component("authz")
public class AuthorizationLogic {
public boolean decide(MethodSecurityExpressionOperations operations) {
// ... authorization logic
}
}
@Component("authz")
open class AuthorizationLogic {
fun decide(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>, AuthorizationManager<MethodInvocationResult> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
// ... authorization logic
}
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
// ... authorization logic
}
}
@Component
class MyAuthorizationManager : AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationDecision {
// ... authorization logic
}
override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationDecision {
// ... authorization logic
}
}
然后,发布方法拦截器,并发布与所需时间相对应的切入点AuthorizationManager
运行。
例如,您可以将@PreAuthorize
和@PostAuthorize
工作方式如下:
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorize(MyAuthorizationManager manager) {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize(MyAuthorizationManager manager) {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
}
}
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun preAuthorize(val manager: MyAuthorizationManager) : Advisor {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager)
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun postAuthorize(val manager: MyAuthorizationManager) : Advisor {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager)
}
}
<sec:method-security pre-post-enabled="false"/>
<aop:config/>
<bean id="preAuthorize"
class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
factory-method="preAuthorize">
<constructor-arg ref="myAuthorizationManager"/>
</bean>
<bean id="postAuthorize"
class="org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor"
factory-method="postAuthorize">
<constructor-arg ref="myAuthorizationManager"/>
</bean>
您可以使用 |
自定义表达式处理
或者,第三,您可以自定义每个 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>
我们揭露 |
您还可以亚纲DefaultMessageSecurityExpressionHandler
以添加超出默认值的自定义授权表达式。
使用 AspectJ 进行授权
使用自定义切入进行匹配方法
基于 Spring AOP 构建,您可以声明与 Comments 无关的模式,类似于请求级授权。 这具有集中方法级授权规则的潜在优势。
例如,您可以使用发布自己的Advisor
或使用<protect-pointcut>
将 AOP 表达式与服务层的授权规则进行匹配,如下所示:
-
Java
-
Kotlin
-
Xml
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor protectServicePointcut() {
AspectJExpressionPointcut pattern = new AspectJExpressionPointcut()
pattern.setExpression("execution(* com.mycompany.*Service.*(..))")
return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER"))
}
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole
companion object {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun protectServicePointcut(): Advisor {
val pattern = AspectJExpressionPointcut()
pattern.setExpression("execution(* com.mycompany.*Service.*(..))")
return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER"))
}
}
<sec:method-security>
<protect-pointcut expression="execution(* com.mycompany.*Service.*(..))" access="hasRole('USER')"/>
</sec:method-security>
与 AspectJ 字节编织集成
有时可以通过使用 AspectJ 将 Spring Security 建议编织到 bean 的字节码中来提高性能。
设置 AspectJ 后,您可以非常简单地在@EnableMethodSecurity
注释或<method-security>
元素:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity(mode=AdviceMode.ASPECTJ)
@EnableMethodSecurity(mode=AdviceMode.ASPECTJ)
<sec:method-security mode="aspectj"/>
结果将是 Spring Security 将发布其顾问作为 AspectJ 建议,以便可以相应地融入其中。
指定顺序
如前所述,每个注解都有一个 Spring AOP 方法拦截器,并且每个注解在 Spring AOP 顾问链中都有一个位置。
即,@PreFilter
方法拦截器的阶数为 100,@PreAuthorize
的为 200,依此类推。
值得注意的是,还有其他基于 AOP 的注释,例如@EnableTransactionManagement
的顺序为Integer.MAX_VALUE
.
换句话说,默认情况下,它们位于顾问链的末端。
有时,在 Spring Security 之前执行其他建议可能很有价值。
例如,如果您有一个用@Transactional
和@PostAuthorize
时,您可能希望交易在以下情况下仍处于打开状态@PostAuthorize
运行,以便AccessDeniedException
将导致回滚。
获取@EnableTransactionManagement
要在方法授权通知运行之前打开事务,您可以将@EnableTransactionManagement
的顺序是这样的:
-
Java
-
Kotlin
-
Xml
@EnableTransactionManagement(order = 0)
@EnableTransactionManagement(order = 0)
<tx:annotation-driven ref="txManager" order="0"/>
由于最早的方法拦截器(@PreFilter
)设置为100的阶数,设置为零意味着事务通知将在所有Spring Security通知之前运行。
使用 SpEL 表达授权
您已经看到了几个使用 SpEL 的示例,所以现在让我们更深入地介绍一下 API。
Spring Security 将其所有授权字段和方法封装在一组根对象中。
最通用的根对象称为SecurityExpressionRoot
它构成了MethodSecurityExpressionRoot
.
Spring Security 将此根对象提供给MethodSecurityEvaluationContext
准备计算授权表达式时。
使用授权表达式字段和方法
首先,它为您的 SpEL 表达式提供了一组增强的授权字段和方法。 以下是最常见方法的快速概述:
-
permitAll
- 该方法不需要授权即可调用;请注意,在这种情况下,这Authentication
永远不会从会话中检索 -
denyAll
- 在任何情况下都不允许使用该方法;请注意,在这种情况下,Authentication
永远不会从会话中检索 -
hasAuthority
- 该方法要求Authentication
有一个GrantedAuthority
与给定值匹配的 -
hasRole
- 快捷方式hasAuthority
该前缀ROLE_
或配置为默认前缀的任何内容 -
hasAnyAuthority
- 该方法要求Authentication
有一个GrantedAuthority
与任何给定值匹配的 -
hasAnyRole
- 快捷方式hasAnyAuthority
该前缀ROLE_
或配置为默认前缀的任何内容 -
hasPermission
- 一个钩子进入你的PermissionEvaluator
用于执行对象级授权的实例
以下是最常见的字段的简要介绍:
-
authentication
-这Authentication
与此方法调用关联的实例 -
principal
-这Authentication#getPrincipal
与此方法调用相关联
现在已经了解了模式、规则以及如何将它们配对在一起,您应该能够理解这个更复杂的示例中发生了什么:
-
Java
-
Kotlin
-
Xml
@Component
public class MyService {
@PreAuthorize("denyAll") (1)
MyResource myDeprecatedMethod(...);
@PreAuthorize("hasRole('ADMIN')") (2)
MyResource writeResource(...)
@PreAuthorize("hasAuthority('db') and hasRole('ADMIN')") (3)
MyResource deleteResource(...)
@PreAuthorize("principal.claims['aud'] == 'my-audience'") (4)
MyResource readResource(...);
@PreAuthorize("@authz.check(authentication, #root)")
MyResource shareResource(...);
}
@Component
open class MyService {
@PreAuthorize("denyAll") (1)
fun myDeprecatedMethod(...): MyResource
@PreAuthorize("hasRole('ADMIN')") (2)
fun writeResource(...): MyResource
@PreAuthorize("hasAuthority('db') and hasRole('ADMIN')") (3)
fun deleteResource(...): MyResource
@PreAuthorize("principal.claims['aud'] == 'my-audience'") (4)
fun readResource(...): MyResource
@PreAuthorize("@authz.check(#root)")
fun shareResource(...): MyResource
}
<sec:method-security>
<protect-pointcut expression="execution(* com.mycompany.*Service.myDeprecatedMethod(..))" access="denyAll"/> (1)
<protect-pointcut expression="execution(* com.mycompany.*Service.writeResource(..))" access="hasRole('ADMIN')"/> (2)
<protect-pointcut expression="execution(* com.mycompany.*Service.deleteResource(..))" access="hasAuthority('db') and hasRole('ADMIN')"/> (3)
<protect-pointcut expression="execution(* com.mycompany.*Service.readResource(..))" access="principal.claims['aud'] == 'my-audience'"/> (4)
<protect-pointcut expression="execution(* com.mycompany.*Service.shareResource(..))" access="@authz.check(#root)"/> (5)
</sec:method-security>
1 | 任何人都不得出于任何原因调用此方法 |
2 | 此方法只能由Authentication 授予ROLE_ADMIN 柄 |
3 | 此方法只能由Authentication 授予db 和ROLE_ADMIN 当局 |
4 | 此方法只能由Princpal s 与aud 声明等于“my-audience” |
5 | 只有当 beanauthz 的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
有write
专门为此提供的许可Contact
实例。在幕后,这是通过使用
AnnotationParameterNameDiscoverer
,您可以自定义它以支持任何指定注释的 value 属性。-
如果 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
,您可以自定义它以支持任何指定注释的 value 属性。 -
-
如果使用
-parameters
参数,则使用标准 JDK 反射 API 来发现参数名称。 这适用于类和接口。 -
最后,如果使用调试符号编译代码,则使用调试符号发现参数名称。 这不适用于接口,因为它们没有有关参数名称的调试信息。 对于接口,注释或
-parameters
必须使用方法。
-
从@EnableGlobalMethodSecurity
如果您正在使用@EnableGlobalMethodSecurity
,您应该迁移到@EnableMethodSecurity
.
将全局方法安全性替换为方法安全性
@EnableGlobalMethodSecurity
和<global-method-security>
被弃用,取而代之的是@EnableMethodSecurity
和<method-security>
分别。
新的 Comments 和 XML 元素默认激活 Spring 的 pre-post Comments,并使用AuthorizationManager
内部。
这意味着以下两个列表在功能上是等效的:
-
Java
-
Kotlin
-
Xml
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
<global-method-security pre-post-enabled="true"/>
和:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity
@EnableMethodSecurity
<method-security/>
对于不使用前置注释的应用程序,请确保将其关闭以避免激活不需要的行为。
例如,列表如下:
-
Java
-
Kotlin
-
Xml
@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(securedEnabled = true)
<global-method-security secured-enabled="true"/>
应更改为:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
<method-security secured-enabled="true" pre-post-enabled="false"/>
使用自定义@Bean
而不是子类化DefaultMethodSecurityExpressionHandler
作为性能优化,引入了一种新方法MethodSecurityExpressionHandler
这需要一个Supplier<Authentication>
而不是Authentication
.
这允许 Spring Security 延迟查找Authentication
,并在您使用@EnableMethodSecurity
而不是@EnableGlobalMethodSecurity
.
但是,假设您的代码扩展了DefaultMethodSecurityExpressionHandler
和覆盖createSecurityExpressionRoot(Authentication, MethodInvocation)
返回自定义SecurityExpressionRoot
实例。
这将不再有效,因为安排@EnableMethodSecurity
设置通话createEvaluationContext(Supplier<Authentication>, MethodInvocation)
相反。
令人高兴的是,这种程度的定制通常是不必要的。 相反,您可以使用所需的授权方法创建自定义 Bean。
例如,假设您想要自定义评估@PostAuthorize("hasAuthority('ADMIN')")
.
您可以创建自定义@Bean
像这个:
-
Java
-
Kotlin
class MyAuthorizer {
boolean isAdmin(MethodSecurityExpressionOperations root) {
boolean decision = root.hasAuthority("ADMIN");
// custom work ...
return decision;
}
}
class MyAuthorizer {
fun isAdmin(val root: MethodSecurityExpressionOperations): boolean {
val decision = root.hasAuthority("ADMIN");
// custom work ...
return decision;
}
}
然后在注解中引用它,如下所示:
-
Java
-
Kotlin
@PreAuthorize("@authz.isAdmin(#root)")
@PreAuthorize("@authz.isAdmin(#root)")
我还是更愿意子类DefaultMethodSecurityExpressionHandler
如果必须继续子类化DefaultMethodSecurityExpressionHandler
,您仍然可以这样做。
相反,请覆盖createEvaluationContext(Supplier<Authentication>, MethodInvocation)
方法如下:
-
Java
-
Kotlin
@Component
class MyExpressionHandler extends DefaultMethodSecurityExpressionHandler {
@Override
public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication, MethodInvocation mi) {
StandardEvaluationContext context = (StandardEvaluationContext) super.createEvaluationContext(authentication, mi);
MethodSecurityExpressionOperations delegate = (MethodSecurityExpressionOperations) context.getRootObject().getValue();
MySecurityExpressionRoot root = new MySecurityExpressionRoot(delegate);
context.setRootObject(root);
return context;
}
}
@Component
class MyExpressionHandler: DefaultMethodSecurityExpressionHandler {
override fun createEvaluationContext(val authentication: Supplier<Authentication>,
val mi: MethodInvocation): EvaluationContext {
val context = super.createEvaluationContext(authentication, mi) as StandardEvaluationContext
val delegate = context.getRootObject().getValue() as MethodSecurityExpressionOperations
val root = MySecurityExpressionRoot(delegate)
context.setRootObject(root)
return context
}
}