对于最新的稳定版本,请使用 Spring Security 6.4.1! |
方法安全性
除了在请求级别对授权进行建模外, Spring Security 还支持在方法级别进行建模。
您可以通过使用任何 XML 配置文件注释任何类或将其添加到任何 XML 配置文件来在应用程序中激活它,如下所示:@Configuration
@EnableMethodSecurity
<method-security>
-
Java
-
Kotlin
-
Xml
@EnableMethodSecurity
@EnableMethodSecurity
<sec:method-security/>
然后,您可以立即使用 @PreAuthorize
、@PostAuthorize
、@PreFilter
和 @PostFilter
注释任何 Spring 管理的类或方法,以授权方法调用,包括输入参数和返回值。
默认情况下,Spring Boot Starter Security 不激活方法级授权。 |
Method Security 还支持许多其他用例,包括 AspectJ 支持、自定义注释和几个配置点。 考虑了解以下使用案例:
-
了解方法安全性的工作原理以及使用它的原因
-
使用
@PreAuthorize
和@PostAuthorize
授权方法 -
使用
@PreFilter
和@PostFilter
的筛选方法 -
使用 JSR-250 注解授权方法
-
使用 AspectJ 表达式授权方法
-
与 AspectJ 字节码编织集成
-
自定义 SpEL 表达式处理
方法安全性的工作原理
Spring Security 的方法授权支持对于以下方面非常方便:
-
提取细粒度的授权逻辑;例如,当 method parameters 和 return values 对授权决策有贡献时。
-
在服务层实施安全性
-
在风格上更倾向于基于 Comments 的配置,而不是基于 的配置
HttpSecurity
由于 Method Security 是使用 Spring AOP 构建的,因此您可以访问其所有表达能力,以根据需要覆盖 Spring Security 的默认值。
如前所述,您首先要添加到类或 Spring XML 配置文件中。@EnableMethodSecurity
@Configuration
<sec:method-security/>
此注释和 XML 元素分别取代 和 。
它们提供了以下改进:
如果您使用的是 或 ,则这些选项现已弃用,建议您迁移。 |
方法授权是方法授权之前和方法之后授权的组合。 考虑以下列方式注释的服务 Bean:
-
Java
-
Kotlin
@Service
public class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
public Customer readCustomer(String id) { ... }
}
@Service
open class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
fun readCustomer(val id: String): Customer { ... }
}
激活 Method Security 时,给定的 invokeation to 可能如下所示:MyCustomerService#readCustomer
-
Spring AOP 调用其代理方法 。在代理的其他顾问中,它调用与
@PreAuthorize
切入点匹配的AuthorizationManagerBeforeMethodInterceptor
readCustomer
-
授权管理器使用 a 来解析注释的 SPEL 表达式,并从 a 构造一个包含
Supplier<Authentication>
和 的对应 a。MethodSecurityExpressionHandler
EvaluationContext
MethodSecurityExpressionRoot
MethodInvocation
-
侦听器使用此上下文来评估表达式;具体来说,它从 中读取
Authentication
,并检查它是否在其权限集合中具有Supplier
permission:read
-
如果评估通过,则 Spring AOP 将继续调用该方法。
-
否则,拦截器将发布一个并抛出一个
AccessDeniedException,ExceptionTranslationFilter
捕获该异常,并向响应返回一个 403 状态码
AuthorizationDeniedEvent
-
在方法返回后, Spring AOP 调用一个与
@PostAuthorize
切入点匹配的AuthorizationManagerAfterMethodInterceptor
,操作与上述相同,但使用PostAuthorizeAuthorizationManager
-
如果评估通过(在本例中,返回值属于已登录用户),则处理将继续正常进行
-
如果不是,则拦截器发布一个并抛出一个
AccessDeniedException,ExceptionTranslationFilter
捕获该异常并向响应返回 403 状态码
AuthorizationDeniedEvent
如果该方法未在 HTTP 请求的上下文中调用,则可能需要自行处理AccessDeniedException |
多个注释是按顺序计算的
如上所述,如果方法调用涉及多个 Method Security 注释,则一次处理一个 Method Security 注释。 这意味着它们可以统称为“和”在一起。 换句话说,要使调用获得授权,所有 Comments 检查都需要通过授权。
每个注释都有自己的切入点
每个 Annotation 都有自己的 pointcut 实例,该实例从方法及其封闭类开始,在整个对象层次结构中查找该 Annotation 或其元 Annotation 对应项。
您可以在 AuthorizationMethodPointcuts
中查看具体内容。
每个 Annotation 都有自己的 Method Interceptor
每个 Comments 都有其自己的专用方法拦截器。
这样做的原因是为了让事情更具可组合性。
例如,如果需要,您可以禁用 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')")
或者,在可能的情况下,在登录时将特定于应用程序的授权逻辑调整为授予的权限。
比较请求级和方法级授权
何时应优先使用方法级授权而不是请求级授权? 其中一些归结为品味;但是,请考虑以下每个优势列表来帮助您做出决定。
请求级别 |
方法级别 |
|
授权类型 |
粗粒度 |
细粒度 |
配置位置 |
在 Config 类中声明 |
local to 方法声明 |
配置样式 |
DSL (英语) |
附注 |
授权定义 |
编程 |
斯佩尔 |
主要的权衡似乎是您希望授权规则所在的位置。
请务必记住,当您使用基于注释的方法安全性时,未注释的方法将不安全。
为了防止这种情况,请在 HttpSecurity 实例中声明一个 catch-all 授权规则。 |
使用注释进行授权
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
}
}
结果是,上述方法仅在其属性与已登录用户的 .
如果没有,Spring Security 将抛出一个并返回一个 403 状态代码。Account
owner
name
AccessDeniedException
过滤方法参数@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;
}
}
这是为了从表达式失败的地方过滤掉任何值。 表示每个 IN,并用于测试每个 。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 代码段 |
当 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;
}
}
这是为了从表达式失败的返回值中筛选出任何值。 表示每个 IN,并用于测试每个 。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
取代它,而是推荐使用。
要使用 Comments,您应该首先更改 Method Security 声明以启用它,如下所示:@Secured
-
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 发布相应的方法拦截器,该拦截器授权用 、 、 和 和 注释的方法、类和接口。@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
}
}
这将产生更具可读性的方法定义。
启用某些注释
您可以关闭 的预配置,并将其替换为您自己的预配置。
如果要自定义 AuthorizationManager
或 .
或者,您可能只想启用特定批注,例如 .@EnableMethodSecurity
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
}
}
然后,使用与您希望它运行的时间相对应的切入点发布方法 interceptor。
例如,你可以将 how 和 work 替换为: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>
您可以使用中指定的 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>
我们使用方法公开,以确保 Spring 在初始化 Spring Security 的方法安全类之前发布它 |
你还可以子类DefaultMessageSecurityExpressionHandler
,以添加超出默认值的自定义授权表达式。
使用 AspectJ 进行授权
使用自定义切入点匹配方法
由于 Spring AOP 构建,您可以声明与 Comments 无关的模式,类似于请求级授权。 这具有集中方法级授权规则的潜在优势。
例如,您可以使用 publish your own 或使用 <protect-pointcut>
将 AOP 表达式与服务层的授权规则匹配,如下所示:Advisor
-
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 之后,你可以非常简单地在注解或元素中声明你正在使用 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 advisor 链中都有一个位置。
即,方法 interceptor's order 是 100,'s 是 200,依此类推。@PreFilter
@PreAuthorize
需要注意的原因是,还有其他基于 AOP 的注解,例如 ,它们的 order 为 .
换句话说,默认情况下,它们位于 advisor 链的末尾。@EnableTransactionManagement
Integer.MAX_VALUE
有时,在 Spring Security 之前执行其他建议可能很有价值。
例如,如果您有一个带有 and 注释的方法,您可能希望事务在运行时仍处于打开状态,以便 an 将导致回滚。@Transactional
@PostAuthorize
@PostAuthorize
AccessDeniedException
要在方法授权建议运行之前打开交易,您可以像这样设置 'order:@EnableTransactionManagement
@EnableTransactionManagement
-
Java
-
Kotlin
-
Xml
@EnableTransactionManagement(order = 0)
@EnableTransactionManagement(order = 0)
<tx:annotation-driven ref="txManager" order="0"/>
由于最早的方法拦截器()被设置为 100 的顺序,因此设置为零意味着事务通知将在所有 Spring Security 通知之前运行。@PreFilter
使用 SPEL 表示授权
您已经看到了几个使用 SpEL 的示例,现在让我们更深入地介绍 API。
Spring Security 将其所有授权字段和方法封装在一组根对象中。
最通用的根对象称为 ,它构成了 的基础。
Spring Security 在准备评估授权表达式时提供此根对象。SecurityExpressionRoot
MethodSecurityExpressionRoot
MethodSecurityEvaluationContext
使用授权表达式字段和方法
它提供的第一件事是 SPEL 表达式的一组增强的授权字段和方法。 以下是最常见方法的快速概述:
-
permitAll
- 该方法不需要授权即可调用;请注意,在这种情况下,永远不会从会话中检索Authentication
-
denyAll
- 该方法在任何情况下都是不允许的;请注意,在这种情况下,永远不会从会话中检索Authentication
-
hasAuthority
- 该方法要求 具有与给定值匹配的GrantedAuthority
Authentication
-
hasRole
- 该前缀的快捷方式或配置为默认前缀的任何内容hasAuthority
ROLE_
-
hasAnyAuthority
- 该方法要求 have a 匹配任何给定值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 | 此方法只能由授予权限的 s 调用Authentication ROLE_ADMIN |
3 | 此方法只能由授予 和 权限的 s 调用Authentication db ROLE_ADMIN |
4 | 此方法只能由声明等于“my-audience”的 s 调用Princpal aud |
5 | 仅当 bean 的方法返回authz check true |
使用方法参数
此外,Spring Security 提供了一种发现方法参数的机制,因此也可以在 SPEL 表达式中访问它们。
有关完整参考,Spring Security 用于发现参数名称。
默认情况下,对方法尝试以下选项。DefaultSecurityParameterNameDiscoverer
-
如果 Spring Security 的 Comments 存在于方法的单个参数上,则使用该值。 以下示例使用注释:
@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?)
此表达式的目的是要求 current 具有专门针对此实例的权限。
Authentication
write
Contact
在幕后,这是通过使用 实现的,您可以自定义 value 以支持任何指定注释的 value 属性。
AnnotationParameterNameDiscoverer
-
如果 Spring Data 的 Comments 存在于该方法的至少一个参数上,则使用该值。 以下示例使用注释:
@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?
此表达式的目的是要求 that 等于 ,以便对调用进行授权。
name
Authentication#getName
在幕后,这是通过使用 实现的,您可以自定义 value 以支持任何指定注释的 value 属性。
AnnotationParameterNameDiscoverer
-
-
如果使用参数编译代码,则使用标准 JDK 反射 API 来发现参数名称。 这适用于类和接口。
-parameters
-
最后,如果使用调试符号编译代码,则使用调试符号会发现参数名称。 这不适用于接口,因为它们没有有关参数名称的调试信息。 对于接口,必须使用 annotations 或 the method。
-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"/>
使用 Custom 而不是子类化@Bean
DefaultMethodSecurityExpressionHandler
作为性能优化,引入了一种新方法,该方法采用 a 而不是 .MethodSecurityExpressionHandler
Supplier<Authentication>
Authentication
这允许 Spring Security 延迟查找 ,并在您使用而不是 .Authentication
@EnableMethodSecurity
@EnableGlobalMethodSecurity
但是,假设您的代码扩展并覆盖以返回自定义实例。
这将不再有效,因为 setup up calls 的 arrangement 改为。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 与应用程序的其他方面(如数据层或跟踪和指标)集成的信息。