【Spring Security】 自定义权限注解
【Spring Security】 自定义权限注解
Metadata
title: 【Spring Security】 自定义权限注解
date: 2023-02-02 22:35
tags:
- 行动阶段/完成
- 主题场景/组件
- 笔记空间/KnowladgeSpace/ProgramSpace/ModuleSpace
- 细化主题/Module/SpringSecurity
categories:
- SpringSecurity
keywords:
- SpringSecurity
description: 【Spring Security】 自定义权限注解
【Spring Security】 自定义权限注解
在之前的案例中,我们可以使用 Security 提供的几大注解,轻松实现访问控制。
@PreAuthorize("hasRole('ROLE_ROOT')")
@PreAuthorize("hasAuthority('add:user')")
但是只提供了固定的几个表达式,内置表达式如下图所示:
表达式 | 说明 |
---|---|
hasRole(String role) | 返回 true 当前主体是否具有指定的角色。例如, hasRole(‘admin’)默认情况下,如果提供的角色不以 “ROLE_” 开头,则会添加它。这可以通过修改 defaultRolePrefixon 来定制 DefaultWebSecurityExpressionHandler。 |
hasAnyRole(String… roles) | 返回 true 当前主体是否具有任何提供的角色(以逗号分隔的字符串列表形式给出)。例如, hasAnyRole(‘admin’, ‘user’) |
hasAuthority(String authority) | 返回 true 当前主体是否具有指定的权限。例如, hasAuthority(‘read’) |
hasAnyAuthority(String… authorities) | 返回 true 当前主体是否具有任何提供的权限(以逗号分隔的字符串列表形式给出)例如, hasAnyAuthority(‘read’, ‘write’) |
principal | 允许直接访问代表当前用户的主体对象 |
authentication | 允许直接访问 Authentication 从 SecurityContext |
permitAll | 总是评估为 tru |
denyAll | 总是评估为 false |
isAnonymous() | 如果当前主体是匿名用户,则返回 true |
isRememberMe() | true 如果当前主体是记得,我的用户, 则返回 true |
isAuthenticated() | 如果用户不是匿名的则返回 true |
iisFullyAuthenticated() | 如果用户不是匿名或记得,我的用户, 则返回 true |
hasPermission(Object target, Object permission) | 返回 true 用户是否有权访问给定权限的提供目标。例如,hasPermission(domainObject, ‘read’) |
hasPermission(Object targetId, String targetType, Object permission) | 返回 true 用户是否有权访问给定权限的提供目标。例如,hasPermission(1, ‘com.example.domain.Message’, ‘read’) |
以上表达式能满足大部分应用场景,但是某些特殊条件下,如何进行自定义扩展呢?
分析
AccessDecisionVoter
AccessDecisionVoter 是一个投票器,负责对授权决策进行表决。然后,最终由唱票者 AccessDecisionManager 统计所有的投票器表决后,来做最终的授权决策。
AccessDecisionVoter 有众多的实现类,对不同的权限配置进行投票,比如 RoleVoter、WebExpressionVoter 分别负责对角色、配置类中配置的授权表达式进行处理。
其中 PreInvocationAuthorizationAdviceVoter 用于对 @PreFilter 和 @PreAuthorize 进行投票处理。
SecurityExpressionRoot
SecurityExpressionRoot 是用 Security 表达式评估的根对象,也就是进行表达式计算的时候,调用的是这个对象中的方法。
决策流程
- 首先对配置类配置的访问策略进行投票,比如默认配置所有的访问都必须经过认证,会调用 WebExpressionVoter 对当前请求进行决策,如果已认证则放行。
http.authorizeRequests((requests) -> {
((AuthorizedUrl)requests.anyRequest()).authenticated();
});
- 接着再次进入过滤器,对注解形式的授权表达式进行处理,这里有三个选民。
- 首先 PreInvocationAuthorizationAdviceVoter 进行投票,最终进入的是 ExpressionBasedPreInvocationAdvice 的 before 方法进行处理。
public boolean before(Authentication authentication, MethodInvocation mi, PreInvocationAttribute attr) {
// 获取注解配置项,转为PreInvocationExpressionAttribute对象,[authorize: 'hasAuthority('add:user')', filter: 'null', filterTarget: 'null']
PreInvocationExpressionAttribute preAttr = (PreInvocationExpressionAttribute) attr;
// 使用表达式处理器,获取评估上下文
EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, mi);
// 获取@PreFilter注解上的表达式,此处为null
Expression preFilter = preAttr.getFilterExpression();
// 获取@PreAuthorize注解上的表达式,hasAuthority('add:user')
Expression preAuthorize = preAttr.getAuthorizeExpression();
if (preFilter != null) {
Object filterTarget = findFilterTarget(preAttr.getFilterTarget(), ctx, mi);
this.expressionHandler.filter(filterTarget, preFilter, ctx);
}
// 使用表达式,进行计算,获取返回投票结果
return (preAuthorize != null) ? ExpressionUtils.evaluateAsBoolean(preAuthorize, ctx) : true;
}
- PreInvocationAuthorizationAdviceVoter 投票为 1,直接认为投票通过,放行请求。
分析 ExpressionBasedPreInvocationAdvice
由上可知,security 对注解进行授权判断的主要选民为 PreInvocationAuthorizationAdviceVoter,当它不通过后,才会调用其他的选民进行投票。而它进行投票的逻辑在 ExpressionBasedPreInvocationAdvice 类中。
ExpressionBasedPreInvocationAdvice 就是基于方法表达式的授权处理的核心类了,它包含了一个 MethodSecurityExpressionHandler 处理器,用来创建 SpEL 评估上下文。(题外话:可以看到 Security 基本上每个类都会有接口,想到了设计模式的依赖倒置,面相接口编程。。。)
接下来我们看下这个处理器是怎么创建的,可以看到有一个默认的处理器,但是也提供了 set 方法,那是否可以通过 set 方法,这是自定义的处理器,来创建自定义的评估上下文,在上下文中,添加我们自己的表达式方法,这样就可以自定义表达式咯?
public class ExpressionBasedPreInvocationAdvice implements PreInvocationAuthorizationAdvice {
// 默认处理器
private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
// 设置处理器
public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
this.expressionHandler = expressionHandler;
}
}
案例演示
演示需求: 创建一个表达式 hasUser,表示只有哪些用户可以访问资源。
1. 创建根对象
在源码中,发现有一个 MethodSecurityExpressionRoot 类,从注释上看可知,这是一个扩展根对象,并添加自定义表达式的类。但是这个类,没有修饰符,只能当前包访问。那只能仿照这个类,添加我们自定义的根对象即可。
自定义扩展根对象:
@Slf4j
public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
public CustomMethodSecurityExpressionRoot(Authentication authentication) {
super(authentication);
}
private Object filterObject;
private Object returnObject;
private Object target;
/**
* 自定义表达式
*
* @param username 具有权限的用户账号
* @return 投票结果
*/
public boolean hasUser(String... username) {
log.info("进入自定义表达式");
String name = this.getAuthentication().getName();
log.info("当前登陆用户:" + name);
String[] names = username;
// 循环表达式配置的值
for (String nameStr : names) {
if (name.equals(nameStr)) {
return true;
}
}
log.info("当前登陆用户:" + name + ",没有权限");
return false;
}
@Override
public void setFilterObject(Object filterObject) {
this.filterObject = filterObject;
}
@Override
public Object getFilterObject() {
return this.filterObject;
}
@Override
public void setReturnObject(Object returnObject) {
this.returnObject = returnObject;
}
@Override
public Object getReturnObject() {
return this.returnObject;
}
void setThis(Object target) {
this.target = target;
}
@Override
public Object getThis() {
return this.target;
}
}
2. 创建处理器
创建自定义处理器,主要是重写创建根对象的方法。
public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {
@Override
protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, MethodInvocation invocation) {
CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot(authentication);
root.setThis(invocation.getThis());
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(getTrustResolver());
root.setRoleHierarchy(getRoleHierarchy());
root.setDefaultRolePrefix(getDefaultRolePrefix());
return root;
}
}
3. 配置 GlobalMethodSecurityConfiguration
之前我们使用 @EnableGlobalMethodSecurity 开启全局方法安全,而这些全局方法级别的安全配置就在 GlobalMethodSecurityConfiguration 配置类中。
可以扩展这个类来自定义默认值,但必须确保在类上指定 @EnableGlobalMethodSecurity 注解,否则会 bean 冲突报错。
@Configuration
// 将EnableGlobalMethodSecurity注解移到这里
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
/**
* 重写创建处理器方法
*
* @return ExpressionHandler
*/
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new CustomMethodSecurityExpressionHandler();
}
}
4. 测试
创建一个访问接口,@PreAuthorize 注解添加自定义表达式 hasUser,表示只有 user,bbb 用户能访问这个接口。
@GetMapping("/403")
@PreAuthorize("hasUser('user','bbb')")
public String code403() {
return "templates/403";
}
使用 test 登录访问,发现没有权限。
使用 user 登录访问,权限校验通过。