对于最新的稳定版本,请使用 Spring Security 6.5.0! |
方法安全性
除了在请求级别对授权进行建模外, Spring Security 还支持在方法级别进行建模。
您可以在应用程序中通过注释任何@Configuration
class 替换为@EnableMethodSecurity
或添加<method-security>
添加到任何 XML 配置文件中,如下所示:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity
@EnableMethodSecurity
<sec:method-security/>
然后,您可以立即使用@PreAuthorize
,@PostAuthorize
,@PreFilter
和@PostFilter
授权方法调用,包括输入参数和返回值。
默认情况下,Spring Boot Starter Security 不激活方法级授权。 |
Method Security 还支持许多其他用例,包括 AspectJ 支持、自定义注释和几个配置点。 考虑了解以下使用案例:
-
了解方法安全性的工作原理以及使用它的原因
-
使用 授权方法
@PreAuthorize
和@PostAuthorize
-
使用 JSR-250 注解授权方法
-
使用 AspectJ 表达式授权方法
-
与 AspectJ 字节码编织集成
-
自定义 SpEL 表达式处理
方法安全性的工作原理
Spring Security 的方法授权支持对于以下方面非常方便:
-
提取细粒度的授权逻辑;例如,当 method parameters 和 return values 对授权决策有贡献时。
-
在服务层实施安全性
-
在风格上倾向于基于注释而不是
HttpSecurity
基于 的配置
由于 Method Security 是使用 Spring AOP 构建的,因此您可以访问其所有表达能力,以根据需要覆盖 Spring Security 的默认值。
如前所述,您首先要添加@EnableMethodSecurity
更改为@Configuration
class 或<sec:method-security/>
在 Spring XML 配置文件中。
此 annotation 和 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 你自己 |
多个注释是按顺序计算的
如上所述,如果方法调用涉及多个 Method Security 注释,则一次处理一个 Method Security 注释。 这意味着它们可以统称为“和”在一起。 换句话说,要使调用获得授权,所有 Comments 检查都需要通过授权。
每个注释都有自己的切入点
每个 Annotation 都有自己的 pointcut 实例,该实例从方法及其封闭类开始,在整个对象层次结构中查找该 Annotation 或其元 Annotation 对应项。
每个 Annotation 都有自己的 Method Interceptor
每个 Comments 都有其自己的专用方法拦截器。
这样做的原因是为了让事情更具可组合性。
例如,如果需要,您可以禁用 Spring Security 默认值和仅发布@PostAuthorize
method intercept器.
方法拦截器如下:
-
为
@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')")
或者,在可能的情况下,在登录时将特定于应用程序的授权逻辑调整为授予的权限。
比较请求级和方法级授权
何时应优先使用方法级授权而不是请求级授权? 其中一些归结为品味;但是,请考虑以下每个优势列表来帮助您做出决定。
请求级别 |
方法级别 |
|
授权类型 |
粗粒度 |
细粒度 |
配置位置 |
在 Config 类中声明 |
local to 方法声明 |
配置样式 |
DSL (英语) |
附注 |
授权定义 |
编程 |
斯佩尔 |
主要的权衡似乎是您希望授权规则所在的位置。
请务必记住,当您使用基于注释的方法安全性时,未注释的方法将不安全。
为了防止这种情况,请在您的HttpSecurity 实例。 |
使用注释进行授权
Spring Security 启用方法级授权支持的主要方式是通过可以添加到方法、类和接口的 Comments。
授权方法调用@PreAuthorize
当 Method Security 处于活动状态时,您可以使用@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
当 Method Security 处于活动状态时,您可以使用@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
在防御不安全的 Direct Object Reference 时特别有用。
事实上,它可以被定义为元注释,如下所示:
-
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 代码段 |
当 Method Security 处于活动状态时,您可以使用@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
declaration 的功能与其他四个相同:
-
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 代码段 |
当 Method Security 处于活动状态时,您可以使用@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
注解,您应该首先更改 Method Security 声明以启用它,如下所示:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity(securedEnabled = true)
@EnableMethodSecurity(securedEnabled = true)
<sec:method-security secured-enabled="true"/>
这将导致 Spring Security 发布相应的方法拦截器,该拦截器授权带有 Comments 的方法、类和接口@Secured
.
使用 JSR-250 注释授权方法调用
如果您想使用 JSR-250 注释,Spring Security 也支持它。@PreAuthorize
具有更强的表现力,因此推荐使用。
要使用 JSR-250 注释,您应该首先更改 Method Security 声明以启用它们,如下所示:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity(jsr250Enabled = true)
@EnableMethodSecurity(jsr250Enabled = true)
<sec:method-security jsr250-enabled="true"/>
这将导致 Spring Security 发布相应的方法拦截器,该拦截器授权带有 Comments 的方法、类和接口@RolesAllowed
,@PermitAll
和@DenyAll
.
在类或接口级别声明注释
还支持在类和接口级别具有 Method Security 注释。
如果它是在类级别,如下所示:
-
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 { ... }
}
然后,声明 Comments 的方法将覆盖类级 Comments。
接口也是如此,不同之处在于,如果一个类从两个不同的接口继承 Comments,则启动将失败。 这是因为 Spring Security 无法判断你想使用哪一个。
在这种情况下,您可以通过将 Annotation 添加到具体方法来解决歧义。
使用 Meta 注释
Method Security 支持元注释。 这意味着您可以采用任何注释,并根据特定于应用程序的用例提高可读性。
例如,您可以简化@PreAuthorize("hasRole('ADMIN')")
自@IsAdmin
这样:
-
Java
-
Kotlin
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
annotation class IsAdmin
结果是,您现在可以在安全方法上执行以下作:
-
Java
-
Kotlin
@Component
public class BankService {
@IsAdmin
public Account readAccount(Long id) {
// ... is only returned if the `Account` belongs to the logged in user
}
}
@Component
open class BankService {
@IsAdmin
fun readAccount(val id: Long): Account {
// ... is only returned if the `Account` belongs to the logged in user
}
}
这将产生更具可读性的方法定义。
启用某些注释
您可以关闭@EnableMethodSecurity
的预配置,并将其替换为您自己的配置。
如果您愿意,您可以选择这样做自定义AuthorizationManager
或Pointcut
.
或者您可能只想启用特定的注释,例如@PostAuthorize
.
您可以通过以下方式执行此作:
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize() {
return AuthorizationManagerBeforeMethodInterceptor.postAuthorize();
}
}
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun postAuthorize() : Advisor {
return AuthorizationManagerBeforeMethodInterceptor.postAuthorize()
}
}
<sec:method-security pre-post-enabled="false"/>
<aop:config/>
<bean id="postAuthorize"
class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
factory-method="postAuthorize"/>
上面的代码段通过首先禁用 Method Security 的预配置,然后发布这@PostAuthorize
拦截 器本身。
授权方式<intercept-methods>
虽然使用 Spring Security 的基于 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>
这仅支持按前缀或按名称匹配方法。 如果您的需求比这更复杂,请改用 annotation support。 |
以编程方式授权方法
如您所见,有几种方法可以使用方法安全性 SPEL 表达式指定重要的授权规则。
有多种方法可以让你的逻辑基于 Java 而不是基于 SPEL。 这为整个 Java 语言提供了 use 访问权限,以提高可测试性和流控制。
在 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
}
}
然后,按以下方式在 Comments 中引用该 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
.
首先,声明一个授权 Management 器实例,可能像这样:
-
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
}
}
然后,使用与所需时间相对应的切入点发布方法 interceptorAuthorizationManager
运行。
例如,您可以将@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>
你可以使用 Order 常量中指定的 order 常量将拦截器放置在 Spring Security 方法拦截器之间 |
自定义表达式处理
或者,第三种,您可以自定义每个 SPEL 表达式的处理方式。
为此,您可以公开自定义MethodSecurityExpressionHandler
这样:
-
Java
-
Kotlin
-
Xml
@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
return handler;
}
companion object {
@Bean
fun methodSecurityExpressionHandler(val roleHierarchy: RoleHierarchy) : MethodSecurityExpressionHandler {
val handler = DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
return handler;
}
}
<sec:method-security>
<sec:expression-handler ref="myExpressionHandler"/>
</sec:method-security>
<bean id="myExpressionHandler"
class="org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler">
<property name="roleHierarchy" ref="roleHierarchy"/>
</bean>
我们揭露 |
您还可以亚纲DefaultMessageSecurityExpressionHandler
以添加您自己的自定义授权表达式,而不是默认值。
使用 AspectJ 进行授权
使用自定义切入点匹配方法
由于 Spring AOP 构建,您可以声明与 Comments 无关的模式,类似于请求级授权。 这具有集中方法级授权规则的潜在优势。
例如,您可以使用 publish your ownAdvisor
或使用<protect-pointcut>
将 AOP 表达式与服务层的授权规则匹配,如下所示:
-
Java
-
Kotlin
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor protectServicePointcut() {
JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
pattern.setPattern("execution(* com.mycompany.*Service.*(..))");
return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER"));
}
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
companion object {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun protectServicePointcut(): Advisor {
var pattern = JdkRegexpMethodPointcut();
pattern.setPattern("execution(* com.mycompany.*Service.*(..))");
return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER"));
}
}
<sec:method-security>
<protect-pointcut expression="execution(* com.mycompany.*Service.*(..))" access="hasRole('USER')"/>
</sec:method-security>
与 AspectJ Byte-waving 集成
有时可以通过使用 AspectJ 将 Spring Security 建议编织到 bean 的字节码中来提高性能。
在设置了 AspectJ 之后,你可以非常简单地在@EnableMethodSecurity
annotation 或<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 advisor 链中都有一个位置。
即,@PreFilter
method interceptor 的 order 为 100,@PreAuthorize
的值为 200,依此类推。
需要注意的原因是还有其他基于 AOP 的注解,例如@EnableTransactionManagement
的订单为Integer.MAX_VALUE
.
换句话说,默认情况下,它们位于 advisor 链的末尾。
有时,在 Spring Security 之前执行其他建议可能很有价值。
例如,如果您有一个带有@Transactional
和@PostAuthorize
,您可能希望在以下情况下事务仍处于打开状态@PostAuthorize
运行,以便AccessDeniedException
将导致回滚。
要获取@EnableTransactionManagement
要在方法授权通知运行之前打开事务,您可以设置@EnableTransactionManagement
的顺序如下:
-
Java
-
Kotlin
-
Xml
@EnableTransactionManagement(order = 0)
@EnableTransactionManagement(order = 0)
<tx:annotation-driven ref="txManager" order="0"/>
由于最早的方法拦截器 (@PreFilter
)设置为 100 的顺序,则设置为 0 表示事务通知将在所有 Spring Security 通知之前运行。
使用 SPEL 表示授权
您已经看到了几个使用 SpEL 的示例,现在让我们更深入地介绍 API。
Spring Security 将其所有授权字段和方法封装在一组根对象中。
最通用的根对象称为SecurityExpressionRoot
它构成了MethodSecurityExpressionRoot
.
Spring Security 将此根对象提供给MethodSecurityEvaluationContext
准备评估授权表达式时。
使用授权表达式字段和方法
它提供的第一件事是 SPEL 表达式的一组增强的授权字段和方法。 以下是最常见方法的快速概述:
-
permitAll
- 该方法不需要授权即可调用;请注意,在本例中,这Authentication
从不从会话中检索 -
denyAll
- 该方法在任何情况下都是不允许的;请注意,在本例中,Authentication
从不从会话中检索 -
hasAuthority
- 该方法要求Authentication
有一个GrantedAuthority
匹配给定值 -
hasRole
- 快捷方式hasAuthority
that 前缀ROLE_
或配置为默认前缀的任何内容 -
hasAnyAuthority
- 该方法要求Authentication
有一个GrantedAuthority
匹配任何给定值 -
hasAnyRole
- 快捷方式hasAnyAuthority
that 前缀ROLE_
或配置为默认前缀的任何内容 -
hasPermission
- 将 hook 插入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 s 授予ROLE_ADMIN 柄 |
3 | 此方法只能由Authentication s 授予db 和ROLE_ADMIN 当局 |
4 | 此方法只能由Princpal s 替换为aud 声明等于 “my-audience” |
5 | 仅当 beanauthz 的check method 返回true |
使用方法参数
此外,Spring Security 提供了一种发现方法参数的机制,因此也可以在 SPEL 表达式中访问它们。
作为完整的参考,Spring Security 使用DefaultSecurityParameterNameDiscoverer
以发现参数名称。
默认情况下,对方法尝试以下选项。
-
如果 Spring Security 的
@P
annotation 存在于方法的单个参数上,则使用该值。 以下示例使用@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
annotation 的 Comments 存在于该方法的至少一个参数上,则使用该值。 以下示例使用@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>
分别。
新的 annotation 和 XML 元素默认激活 Spring 的 pre-post 注释,并使用AuthorizationManager
内部。
这意味着以下两个清单在功能上是等效的:
-
Java
-
Kotlin
-
Xml
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
<global-method-security pre-post-enabled="true"/>
和:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity
@EnableMethodSecurity
<method-security/>
对于不使用 pre-post 注释的应用程序,请确保将其关闭以避免激活不需要的行为。
例如,像这样的列表:
-
Java
-
Kotlin
-
Xml
@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(securedEnabled = true)
<global-method-security secured-enabled="true"/>
应更改为:
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
<method-security secured-enabled="true" pre-post-enabled="false"/>
使用自定义@Bean
而不是子类化DefaultMethodSecurityExpressionHandler
作为性能优化,引入了一种新方法MethodSecurityExpressionHandler
这需要Supplier<Authentication>
而不是Authentication
.
这允许 Spring Security 延迟对Authentication
,并在您使用@EnableMethodSecurity
而不是@EnableGlobalMethodSecurity
.
但是,假设您的代码扩展了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;
}
}
然后在 Comments 中引用它,如下所示:
-
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;
}
}
延伸阅读
现在您已经保护了应用程序的请求,如果您尚未保护其请求,请保护它。 您还可以进一步阅读有关测试应用程序或将 Spring Security 与应用程序的其他方面(如数据层或跟踪和指标)集成的信息。