阅读spring security基于方法的权限认证

This commit is contained in:
asahi
2024-03-26 19:22:38 +08:00
parent df5101160b
commit 34991d580a

View File

@@ -67,6 +67,18 @@
- [使用path变量](#使用path变量) - [使用path变量](#使用path变量)
- [使用数据库来进行权限认证](#使用数据库来进行权限认证) - [使用数据库来进行权限认证](#使用数据库来进行权限认证)
- [SecurityMatcher](#securitymatcher) - [SecurityMatcher](#securitymatcher)
- [基于方法的权限认证](#基于方法的权限认证)
- [Method Security的工作原理](#method-security的工作原理)
- [多个注解组合](#多个注解组合)
- [不支持重复注解](#不支持重复注解)
- [支持通过向角色授权来替换复杂的spel表达式](#支持通过向角色授权来替换复杂的spel表达式)
- [通过注解来执行权限校验](#通过注解来执行权限校验)
- [@PreAuthorize](#preauthorize)
- [@PostAuthorize](#postauthorize)
- [@PreFilter](#prefilter)
- [@PostFilter](#postfilter)
- [在接口或类级别声明注解](#在接口或类级别声明注解)
- [在方法spel中指定自定义的bean调用](#在方法spel中指定自定义的bean调用)
# Spring Security # Spring Security
@@ -1151,3 +1163,201 @@ public class SecurityConfig {
} }
``` ```
### 基于方法的权限认证
除了基于http请求的权限认证外spring security还支持基于方法的权限认证。可以通过在@Configuration类上指定`@EnableMethodSecurity`来启用基于方法的权限认证。
在通过注解开启基于方法的权限认证之后可以在spring管理的类或方法上添加`@PreAuthorize, @PostAuthorize, @PreFilter, 和@PostFilter`等注解来对方法调用进行权限认证,包括对其参数和方法返回值。
> 默认情况下spring security并不会开启基于方法的权限认证。
#### Method Security的工作原理
spring security基于方法的权限认证对于如下场景非常方便
- 细粒度的授权逻辑,例如方法调用的参数或返回值对权限认证逻辑有影响
- 在service层执行权限认证逻辑
Spring Security基于方法的权限认证是通过spring aop实现的。
基于方法的授权,其可分为两部分:
- before目标方法执行前的权限认证
- after 目标方法执行后的权限认证
如果想要对service bean中的方法进行权限认证可以参照如下示例
```java
@Service
public class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
public Customer readCustomer(String id) { ... }
}
```
当method security被启用时执行`Customer#readCustomer`将会进行如下逻辑:
1. spring aop将会调用`readCustomer`的proxy method`AuthorizationManagerBeforeMethodInterceptor`继承了`PointcutAdvisor`在调用proxy method时会调用所有满足`@PreAuthorize pointcut`的advisor
2. 该interceptor将会调用`PreAuthorizeAuthorizationManager#check`方法
3. authorization manager将会使用`MethodSecurityExpressionHandler`来转化注解中的spel表达式并且从`MethodSecurityExpressionRoot`中构建相应的`EvaluationContext`上下文
4. interceptor将使用上下文来计算spel表达式其会从Authentication中获取权限并且校验是否拥有`permission:read`的权限
5. 如果校验通过spring aop会继续调用被代理的方法
6. 如果校验不通过,那么会抛出`AccessDeniedException`异常,并发布`AuthorizationDeniedEvent`事件,`ExceptionTranslationFilter`会将异常捕获并返回403响应
7. 在方法执行返回后spring aop将会调用满足`@PostAuthorize pointcut``AuthorizationManagerAfterMethodInterceptor`,并执行和方法执行前一样的校验,
8. 如果表达式校验通过,那么继续正常执行
9. 如果表达式校验失败,那么会抛出`AccessDeniedException`异常,并发布`AuthorizationDeniedEvent`事件,`ExceptionTranslationFilter`会将异常捕获并返回403响应
#### 多个注解组合
如上述示例所示,如果向同一方法中指定多个`method security`注解,那么注解会一次执行,同一时刻只会针对一个注解的逻辑进行执行。
故而,在执行多个注解时,多个注解的校验逻辑为`and`操作。
#### 不支持重复注解
虽然method security支持在同一方法中指定多个`method security`注解,但是,不支持在同一方法中指定多个相同的`method security`注解。无法在同一方法上指定两个`@PreAuthorize`注解。
并且每个注解都有其自己的pointcut每个注解也有其自己的method interceptor。
#### 支持通过向角色授权来替换复杂的spel表达式
有时候可能会引入复杂的spel表达式
```java
@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')")
```
这种场景下,可以将`permission:read`权限授予给`ROLE_ADMIN`,可以通过`RoleHierarchy`来实现:
```java
@Bean
static RoleHierarchy roleHierarchy() {
return new RoleHierarchyImpl("ROLE_ADMIN > permission:read");
}
```
故而上述spel表达式可以被简化为如下形式
```java
@PreAuthorize("hasAuthority('permission:read')")
```
#### 通过注解来执行权限校验
在开启method security之后支持向类、方法、接口上添加注解来执行权限校验操作。
##### @PreAuthorize
@PreAuthorize的使用示例如下所示
```java
@Component
public class BankService {
@PreAuthorize("hasRole('ADMIN')")
public Account readAccount(Long id) {
// ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority
}
}
```
只有当spel表达式`hasRole('ADMIN')`校验通过时,才会实际调用`readAccount`方法。
##### @PostAuthorize
@PostAuthorize使用示例如下所示
```java
@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
}
}
```
通过@PostAuthorize,可以针对方法调用的返回结果进行校验。
##### @PreFilter
通过@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中所有不满足spel表达式的对象。
`@PreFilter`注解支持array、collection、map、stream如下示例中的四种方式其过滤逻辑都一样
```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;
}
}
```
##### @PostFilter
@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;
}
}
```
上述注解会过滤accounts中所有spel表达式校验不通过的account。
@PostFilter同样支持array、collection、map、stream形式其使用和@PreFilter类似
```java
@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)
```
##### 在接口或类级别声明注解
对于method security同样支持在方法级别或类级别使用注解。
在类级别使用注解的示例如下所示:
```java
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
@GetMapping("/endpoint")
public String endpoint() { ... }
}
```
在类级别指定注解后,类中所有的方法都支持该注解的行为:
```java
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
@GetMapping("/endpoint")
public String endpoint() { ... }
}
```
也可以同时在方法级别和类级别指定注解,此时方法级别的注解会覆盖类级别注解的行为:
```java
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
@GetMapping("/endpoint")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String endpoint() { ... }
}
```
#### 在方法spel中指定自定义的bean调用
如下为自定义的bean service
```java
@Component("authz")
public class AuthorizationLogic {
public boolean decide(MethodSecurityExpressionOperations operations) {
// ... authorization logic
}
}
```
可以通过如下方式在表达式中调用自定义的bean service
```java
@Controller
public class MyController {
@PreAuthorize("@authz.decide(#root)")
@GetMapping("/endpoint")
public String endpoint() {
// ...
}
}
```