在 Spring Boot 应用开发中,利用自定义注解结合切面(AOP)来实现接口权限控制是一种常见做法。通常的实现思路包含以下几个步骤:
- 自定义一个权限校验的注解,包含参数 value。
- 在对应的接口方法上配置该注解。
- 定义一个切面类,并指定切点。
- 在切入的方法体内编写权限判断的逻辑。
这套流程看似清晰易懂,但在实际复杂的业务场景中,我们往往会遇到多样化的权限校验需求。例如:
- 只要用户配置了任何角色,就可以访问。
- 用户拥有某个特定操作权限才可以访问。
- 无条件放行所有请求。
- 只有超级管理员角色才可以访问。
- 用户登录后才可以访问。
- 在指定的时间段内允许访问。
- 用户拥有某个特定角色才可以访问。
- 用户必须同时具有多个指定角色才可以访问。
面对如此多的场景,如果按照传统方式为每种情况都编写独立的注解和判断逻辑,代码将变得臃肿且难以维护。此时,SpEL(Spring Expression Language)表达式就为我们提供了一种更为优雅和灵活的解决方案。
什么是 SpEL 表达式?
SpEL 的全称为 Spring Expression Language,即 Spring 表达式语言,自 Spring 3.0 开始提供。它最强大的功能在于,能够在运行时动态地执行表达式,并将结果装配到属性或构造函数中。简单来说,它允许我们将权限规则以字符串表达式的形式动态配置,并由框架在运行时解析执行,从而实现权限逻辑与业务代码的解耦。
实现步骤详解
1. 自定义注解
首先,我们需要定义一个用于权限控制的注解。与传统做法不同,这里的 value 属性将用于接收 SpEL 表达式字符串。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuth {
/**
* Spring EL 表达式,用于动态权限校验。
* 示例:
* hasPermission('MENU.QUERY') - 有 MENU.QUERY 操作权限的角色可以访问
* hasRole('管理员') - 具有管理员角色的人才能访问
* hasAllRole('管理员','总工程师') - 同时具有管理员、总工程师角色的人才能访问
* permitAll() - 放行所有请求
* denyAll() - 只有超级管理员角色才可访问
* hasAuth() - 只有登录后才可访问
* hasTimeAuth(1, 10) - 只有在1-10点间可以访问
*/
String value();
}
2. 定义切面与切入点
接下来,我们定义切面。这里的关键是切入点的选择:我们希望注解既可以作用于方法上,也可以作用于类上(此时对该类下所有方法生效)。因此,我们使用 @annotation 和 @within 来定义组合切点。
@Around("@annotation(com.yourpackage.PreAuth) || @within(com.yourpackage.PreAuth)")
public Object preAuth(ProceedingJoinPoint point) throws Throwable {
if (handleAuth(point)) {
return point.proceed();
}
throw new SecureException(ResultCode.REQ_REJECT);
}
private boolean handleAuth(ProceedingJoinPoint point) {
// TODO: 权限校验逻辑,返回 true 或 false
}
3. 集成 SpEL 进行权限校验
这是最核心的部分。我们将在 handleAuth 方法中,解析注解上的 SpEL 表达式并执行。
3.1 引入 SpEL 解析器
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
3.2 表达式解析与执行
private boolean handleAuth(ProceedingJoinPoint point) {
MethodSignature ms = point.getSignature() instanceof MethodSignature ? (MethodSignature) point.getSignature() : null;
Method method = ms.getMethod();
// 读取权限注解,优先从方法上读取,没有则读取类上的注解
PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class);
// 获取注解中的表达式
String condition = preAuth.value();
if (StringUtil.isNotBlank(condition)) {
// 解析表达式
Expression expression = EXPRESSION_PARSER.parseExpression(condition);
// 获取方法参数值
Object[] args = point.getArgs();
// 创建并设置表达式执行上下文
StandardEvaluationContext context = getEvaluationContext(method, args);
// 执行表达式并获取布尔值结果
return expression.getValue(context, Boolean.class);
}
return false;
}
/**
* 构建 SpEL 表达式执行上下文
* @param method 当前执行的方法
* @param args 方法参数
* @return StandardEvaluationContext
*/
private StandardEvaluationContext getEvaluationContext(Method method, Object[] args) {
// 初始化上下文,并注册自定义的权限校验函数类 AuthFun
StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun());
// 设置上下文支持解析 Spring Bean(如果表达式需要引用Bean)
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
// 将方法参数设置为SpEL上下文中的变量,便于在表达式中使用
for (int i = 0; i < args.length; i++) {
MethodParameter methodParam = ClassUtil.getMethodParameter(method, i);
context.setVariable(methodParam.getParameterName(), args[i]);
}
return context;
}
4. 实现权限校验逻辑类 AuthFun
请注意 getEvaluationContext 方法中的 new AuthFun(),这个 AuthFun 类是我们的“魔法”所在。该类中定义的每一个公共方法,都可以直接在 @PreAuth 注解的表达式里调用。这里是具体权限逻辑实现的地方。
public class AuthFun {
/**
* 判断角色是否具有接口权限 (示例逻辑,需按需实现)
*/
public boolean permissionAll() {
//TODO: 实现“有任意角色即可访问”的逻辑
return true;
}
/**
* 判断是否具有特定操作权限
* @param permission 权限编号
*/
public boolean hasPermission(String permission) {
//TODO: 实现根据权限编码校验的逻辑
return true;
}
/**
* 放行所有请求
*/
public boolean permitAll() {
return true;
}
/**
* 只有超管角色才可访问
*/
public boolean denyAll() {
return hasRole(RoleConstant.ADMIN);
}
/**
* 是否已登录授权
*/
public boolean hasAuth() {
if(Func.isEmpty(AuthUtil.getUser())){
// TODO 返回异常或 false
return false;
} else {
return true;
}
}
/**
* 是否有时间授权
* @param start 开始时间(小时)
* @param end 结束时间(小时)
*/
public boolean hasTimeAuth(Integer start, Integer end) {
Integer hour = DateUtil.hour();
return hour >= start && hour <= end;
}
/**
* 判断是否有该角色权限
* @param role 角色名
*/
public boolean hasRole(String role) {
return hasAnyRole(role);
}
/**
* 判断是否具有所有指定角色权限
* @param role 角色集合
*/
public boolean hasAllRole(String... role) {
for (String r : role) {
if (!hasRole(r)) {
return false;
}
}
return true;
}
/**
* 判断是否具有任意一个指定角色权限
* @param role 角色集合
*/
public boolean hasAnyRole(String... role) {
// 获取当前登录用户(示例,需替换为实际获取方式)
BladeUser user = AuthUtil.getUser();
if (user == null) {
return false;
}
String userRole = user.getRoleName();
if (StringUtil.isBlank(userRole)) {
return false;
}
String[] roles = Func.toStrArray(userRole);
for (String r : role) {
if (CollectionUtil.contains(roles, r)) {
return true;
}
}
return false;
}
}
实际应用
在 Controller 层使用时,我们只需在类或方法上添加 @PreAuth 注解,并在 value 中写入符合 SpEL 语法的表达式即可。表达式调用的正是 AuthFun 类中定义的方法。
例如,要求用户同时具有“管理员”和“总工程师”角色:
@PreAuth("hasAllRole('管理员','总工程师')")
@GetMapping("/some-api")
public ResponseEntity<?> someApi() {
// ...
}
要求用户拥有 LM_QUERY 或 LM_QUERY_ALL 权限之一:
@PreAuth("hasPermission('LM_QUERY') or hasPermission('LM_QUERY_ALL')")
public T queryMethod(...) {
// ...
}
实现原理简述
整个流程的核心在于 SpEL 的动态解析能力。Spring AOP 拦截到带有 @PreAuth 注解的方法调用后,切面会提取注解中的表达式字符串(如 "hasAllRole('管理员','总工程师')")。SpEL 解析器会将其解析为一个可执行的表达式对象。
随后,解析器在指定的上下文(StandardEvaluationContext)中执行该表达式。这个上下文里注册了我们自定义的 AuthFun 对象。因此,当解析到 hasAllRole 时,它会自动调用 AuthFun.hasAllRole(String... role) 方法,并将字符串参数 '管理员' 和 '总工程师' 传递进去,最终根据该方法的返回值决定是否放行请求。
总结与优势
通过引入 SpEL 表达式,我们将原本硬编码在切面或注解中的权限判断逻辑,抽象为可动态配置的字符串规则。这种设计模式带来的主要优势包括:
- 高度灵活:新的权限校验场景只需在
AuthFun 类中添加对应方法,并在注解中配置相应表达式即可,无需修改切面核心逻辑或创建新注解。
- 高度可读:权限规则以接近自然语言的表达式形式直观地写在注解中,一目了然。
- 便于维护:权限逻辑集中在
AuthFun 类中,结构清晰,易于管理和扩展。
- 强大表达能力:
SpEL 本身支持运算符、条件判断、方法调用等,可以组合出非常复杂的权限规则。
这种基于 Spring Boot 和 SpEL 的权限控制方案,优雅地解决了复杂多变的权限需求,是构建灵活、可扩展的后端权限系统的优秀实践。如果你想了解更多关于 Spring Boot 或 AOP 的进阶用法,可以到云栈社区的Java技术板块与其他开发者交流探讨。