参考:方法安全(Method Security) :: Spring Security Reference (springdoc.cn)、 授权 HttpServletRequest :: Spring Security Reference (springdoc.cn)
前文为:Spring Security:授权框架
一、HttpServletRequest授权
Spring Security 允许在 request 层(通过URL和请求类型等)建立授权模型。例如,可以让 /admin 下的所有页面都需要授权,而所有其他页面只需要认证。
1.1调度(Dispatche)授权
AuthorizationFilter 不仅在每个 request 上运行,而且在每个 dispatch 上运行。这意味着 REQUEST dispatch 需要授权,FORWARD、ERROR 和 INCLUDE 也需要:
- 比如 Spring MVC 将一个请求 FORWARD 到一个渲染 Thymeleaf 模板的视图解析器,会发生两次授权:一次是授权 /endpoint,一次是转发到 Thymeleaf 以渲染 "endpoint" 模板。这就是 FORWARD dispatche。
- 比如 Spring Boot 捕捉到一个异常后,会将它 dispatch 给 ERROR dispatch,也会发生两次授权:一次是授权 /endpoint ,一次是 dispatch error。
代码如
@Controller
public class MyController {
@GetMapping("/endpoint")
public String endpoint() {//Spring MVC的FORWARD dispatche
return "endpoint";
}
@GetMapping("/endpoint2")
public String endpoint2() {//Spring Boot的ERROR dispatch
throw new UnsupportedOperationException("unsupported");
}
}
进行放行,就需要:
http
.authorizeHttpRequests((authorize) -> authorize
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.requestMatchers("/endpoint").permitAll()
.requestMatchers("/endpoint2").permitAll()
.anyRequest().denyAll()
)
1.2Endpoint授权
对端点进行授权的方式,如/endpoint 只能被具有 USER 权限的终端用户访问:
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/endpoint").hasAuthority('USER')
.anyRequest().authenticated()
)
// ...
return http.build();
}
观察代码可以发现,声明可以被分解为 pattern/rule 对,即requestMatchers("pattern").rule。
解释一下上述代码:
- requestMatchers("/endpoint").XXX:授权规则
- permitAll:该请求不需要授权。(不会从 session 中检索 Authentication)
- denyAll:任何情况下都不允许。(不会从 session 中检索 Authentication)
- hasAuthority: Authentication 有一个符合给定值的 GrantedAuthority。
- hasRole:hasAuthority 的一个快捷方式,自动添加默认的前缀。
- hasAnyAuthority:要求 Authentication 有一个符合任何给定值的 GrantedAuthority。
- hasAnyRole:hasAnyAuthority 的一个快捷方式,自动添加默认的前缀。
- access:该请求使用这个自定义的 AuthorizationManager 来确定访问权限。
- requestMatchers(XXX):匹配请求
- URI模式
- Ant语言:如 requestMatchers("/resource/**").hasAuthority("USER"),代表——如果请求是 /resource 或一些子目录,则要求 USER 权限。
- 正则表达式:如requestMatchers(RegexRequestMatcher.regexMatcher("/username/[A-Za-z0-9]+")).hasAuthority("USER"),代表包含 username 的路径和所有 username 必须是字母数字。
- Http方法
- 如:所有的 GET 都有 read 权限——requestMatchers(HttpMethod.GET).hasAuthority("read")。
- URI模式
也可以自定义RequestMatcher,然后提供给http:
RequestMatcher printview = (request) -> request.getParameter("print") != null;//自定义RequestMatcher
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(printview).hasAuthority("print")
.anyRequest().authenticated()
)
1.3案例
基础配置
如:配置 /auth/hello 的 get 请求需要 user 权限;配置 /auth/hello 的 post 请求需要 manager 权限;配置 /hello 的 get 请求需要 manager 权限:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.requestMatchers(HttpMethod.GET,"/auth/hello").hasAuthority("user")
.requestMatchers(HttpMethod.POST,"/auth/hello").hasAuthority("manager")
.requestMatchers("/hello").hasAuthority("manager")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
http.userDetailsService(myJdbcDaoImpl());
return http.build();
}
数据库中权限为:
自定义AuthorizationManager
复习一下AuthorizationManager是什么:用来判断授权是否通过的组件。
代码:
@Component
public final class OpenPolicyAgentAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
return new AuthorizationDecision(true);
}
@Override
public void verify(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
AuthorizationManager.super.verify(authentication, object);
}
}
@Bean
SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> authz) throws Exception {
http.authorizeHttpRequests((authorize) -> authorize
.anyRequest().access(authz)
);
return http.build();
}
check内填写授权的方式,我直接返回授权成功了...RequestAuthorizationContext context 中包含了请求信息,可从中获得URL等信息进行判定。
1.4SpEL表示授权
Spring Security 将其所有的授权字段和方法都封装在一组根(root)对象中。最通用的根对象被称为 SecurityExpressionRoot,它是 WebSecurityExpressionRoot 的基础。当准备评估一个授权表达式时,Spring Security 将这个根对象提供给 StandardEvaluationContext。
常见方法:
- permitAll - 该请求不需要授权即可调用;注意,在这种情况下,将不会从 session 中检索 Authentication。
- denyAll - 该请求在任何情况下都是不允许的;注意在这种情况下,永远不会从会话中检索 Authentication。
- hasAuthority - 请求要求 Authentication 的 GrantedAuthority 符合给定值。
- hasRole - hasAuthority 的快捷方式,前缀为 ROLE_ 或任何配置为默认前缀的内容。
- hasAnyAuthority - 请求要 Authentication 具有符合任何给定值的 GrantedAuthority。
- hasAnyRole - hasAnyAuthority 的一个快捷方式,其前缀为 ROLE_ 或任何被配置为默认的前缀。
- hasPermission - 用于对象级授权的 PermissionEvaluator 实例的 hook。
举例:
<http>
<intercept-url pattern="/static/**" access="permitAll"/>
<intercept-url pattern="/admin/**" access="hasRole('ADMIN')"/>
<intercept-url pattern="/db/**" access="hasAuthority('db') and hasRole('ADMIN')"/>
<intercept-url pattern="/**" access="denyAll"/>
</http>
- 我们指定了一个任何用户都可以访问的 URL patter。具体来说,如果URL以 "/static/" 开头,任何用户都可以访问一个请求。
- 任何以 "/admin/" 开头的 URL 将被限制给拥有 "ROLE_ADMIN" 角色的用户。你会注意到,由于我们调用的是 hasRole 方法,我们不需要指定 "ROLE_" 前缀。
- 任何以 "/db/" 开头的 URL 都需要用户被授予 "db" 权限,并且是 "ROLE_ADMIN"。你会注意到,由于我们使用的是 hasRole 表达式,我们不需要指定 "ROLE_" 前缀。
- 任何还没有被匹配的URL都会被拒绝访问。如果你不想意外地忘记更新你的授权规则,这是一个好的策略。
二、方法安全
2.1基础流程
除了对 HttpServletRequest 授权外,Spring Security 还支持在方法级别进行授权,方法安全具有细粒度、服务层的优点,并在风格上优于基于 HttpSecurity 的配置。
方法安全需要通过注解@EnableMethodSecurity启用,启用后即可使用注解 @PreAuthorize、@PostAuthorize、@PreFilter 和 @PostFilter。
代码举例:
@Service
public class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
public Customer readCustomer(String id) { ... }
}
方法安全是基于Spring AOP实现的。调用流程:
- Spring AOP 为 readCustomer 调用了代理方法,调用了一个与 @PreAuthorize pointcut 匹配的拦截器 AuthorizationManagerBeforeMethodInterceptor。
- 拦截器调用 PreAuthorizeAuthorizationManager#check。
- PreAuthorizeAuthorizationManager 使用 MethodSecurityExpressionHandler 解析注解的 SpEL 表达式,并从包含 Supplier 和 MethodInvocation 的 MethodSecurityExpressionRoot 构建相应的 EvaluationContext。
- 拦截器使用该上下文来评估表达式;具体地说,它从 Supplier 读取 Authentication,并检查其 权限 集合中是否有 permission:read。
- 如果评估通过,Spring AOP 将继续调用该方法。
- 如果没有,拦截器会发布一个 AuthorizationDeniedEvent,并抛出一个 AccessDeniedException, ExceptionTranslationFilter 会捕获并向响应返回一个403状态码。
- 在方法返回后,Spring AOP 调用一个与 @PostAuthorize pointcut 相匹配的拦截器 AuthorizationManagerAfterMethodInterceptor,操作与上面相同,但使用了 PostAuthorizeAuthorizationManager。
- 如果评估通过(在这种情况下,返回值属于登录的用户),处理继续正常进行。
- 如果没有,拦截器会发布一个 AuthorizationDeniedEvent,并抛出一个 AccessDeniedException, ExceptionTranslationFilter 会捕获并向响应返回一个 403 状态码。
2.2注解使用介绍与举例
注解使用方式:
- 多个注解串联计算:如果方法调用涉及多个 方法安全注解,则每个注解一次处理一个(等同于&&)。
- 同一个方法上不能重复相同的注解。
- 注解内的授权使用 SpEL 定义。
- 在类和接口级别也支持方法安全注解,所有方法都继承类级别的行为。
- 通过@Param或@P注解,可在方法安全中使用方法参数。
@PreAuthorize应用代码举例:
@RestController
@RequestMapping("/auth")
public class TestController {
@PreAuthorize("hasAuthority('user')")
@GetMapping("/hello")
public String sayHello(){
return "hello security";
}
@PreAuthorize("hasAuthority('manager')")
@PostMapping("/hello")
public String sayHello2(){
return "hello security";
}
}
要求调用处理/auth/hello的get请求的控制权方法时需要user权限,调用处理/auth/hello的post请求的控制权方法时需要manager权限。
@PostAuthorize、@PreFilter、@PostFilter 应用举例:
@Component
public class BankService {//返回的 Account 对象.owner == authentication.name 通过时才能返回值。
@PostAuthorize("returnObject.owner == authentication.name")
public Account readAccount(Long id) {
// ... is only returned if the `Account` belongs to the logged in user
}
}
@Component
public class BankService {//过滤掉实参 accounts 中的 filterObject.owner == authentication.name 表达式失败的值。
//filterObject 代表 accounts 中的每个 account,用于测试每个 account。
@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;
}
}
@Component
public class BankService {//过滤掉返回值 accounts 中的 filterObject.owner == authentication.name 表达式失败的值。
@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;
}
}
在方法安全中使用方法参数:
- @Param 注解:适用于方法有一个及以上参数使用注解。
@PreAuthorize("#n == authentication.name") Contact findContactByName(@Param("n") String name);//要求 name 等于 Authentication#getName 才能对调用进行授权。
- @P 注解:适用于方法仅有一个参数使用注解。
@PreAuthorize("hasPermission(#c, 'write')") public void updateContact(@P("c") Contact contact);//当前 Authentication 具有专门针对此 Contact 实例的 write 权限。
在方法安全中,注解的拦截器可以自定义,拦截器使用的AuthenticationManager也可以自定义;SpEL 中可以使用自定义 Bean 进行判断,SpEL的表达式的处理方式也可以自定义,这些内容请自行了解(方法安全(Method Security) :: Spring Security Reference (springdoc.cn))。
2.3SpEL表达式案例
@Component
public class MyService {
//任何人不得以任何理由调用该方法。
@PreAuthorize("denyAll")
MyResource myDeprecatedMethod(...);
//该方法只能由被授予 ROLE_ADMIN 权限的 Authentication 调用。
@PreAuthorize("hasRole('ADMIN')")
MyResource writeResource(...)
//该方法只能由被授予 db 和 ROLE_ADMIN 权限的 Authentication 调用。
@PreAuthorize("hasAuthority('db') and hasRole('ADMIN')")
MyResource deleteResource(...)
//本方法仅可由 aud claim 等于 "my-audience" 的 Princpal 调用。
@PreAuthorize("principal.claims['aud'] == 'my-audience'")
MyResource readResource(...);
//只有当 Bean authz 的 decide 方法返回 true 时,才能调用该方法。
@PreAuthorize("@authz.decide(#root)")
MyResource shareResource(...);
}
//Bean authz 的定义
@Component("authz")
public class AuthorizationLogic {
public boolean decide(MethodSecurityExpressionOperations operations) {
// ... authorization logic
}
}