From 34991d580a5056fc9de57541019e6d8a5698ed8a Mon Sep 17 00:00:00 2001 From: asahi Date: Tue, 26 Mar 2024 19:22:38 +0800 Subject: [PATCH] =?UTF-8?q?=E9=98=85=E8=AF=BBspring=20security=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=E6=96=B9=E6=B3=95=E7=9A=84=E6=9D=83=E9=99=90=E8=AE=A4?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spring/Spring Security/Spring Security.md | 210 ++++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/spring/Spring Security/Spring Security.md b/spring/Spring Security/Spring Security.md index 492dce0..dd38040 100644 --- a/spring/Spring Security/Spring Security.md +++ b/spring/Spring Security/Spring Security.md @@ -67,6 +67,18 @@ - [使用path变量](#使用path变量) - [使用数据库来进行权限认证](#使用数据库来进行权限认证) - [SecurityMatcher](#securitymatcher) + - [基于方法的权限认证](#基于方法的权限认证) + - [Method Security的工作原理](#method-security的工作原理) + - [多个注解组合](#多个注解组合) + - [不支持重复注解](#不支持重复注解) + - [支持通过向角色授权来替换复杂的spel表达式](#支持通过向角色授权来替换复杂的spel表达式) + - [通过注解来执行权限校验](#通过注解来执行权限校验) + - [@PreAuthorize](#preauthorize) + - [@PostAuthorize](#postauthorize) + - [@PreFilter](#prefilter) + - [@PostFilter](#postfilter) + - [在接口或类级别声明注解](#在接口或类级别声明注解) + - [在方法spel中指定自定义的bean调用](#在方法spel中指定自定义的bean调用) # 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 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 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 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 readAccounts(String... ids) + +@PostFilter("filterObject.owner == authentication.name") +public Stream 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() { + // ... + } +} +``` + + +