feat(plugin): 新增 sa-token-spring-el 插件,用于支持 SpEL 表达式注解鉴权

This commit is contained in:
click33
2025-01-15 22:28:50 +08:00
parent 079376107a
commit b7b13fe4ed
16 changed files with 698 additions and 2 deletions

View File

@@ -0,0 +1,34 @@
<?xml version='1.0' encoding='utf-8'?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-plugin</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<packaging>jar</packaging>
<name>sa-token-spring-el</name>
<artifactId>sa-token-spring-el</artifactId>
<description>sa-token authentication by spring-el</description>
<dependencies>
<!-- sa-token-spring-boot-starter -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
</dependency>
<!-- spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 注解鉴权:根据 EL 表达式执行鉴权
*
* <p> 可标注在方法、类上(效果等同于标注在此类的所有方法上)
*
* @author click33
* @since 1.40.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface SaCheckEL {
/**
* 需要执行的 EL 表达式
*
* @return /
*/
String value() default "";
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.aop;
import cn.dev33.satoken.annotation.SaCheckEL;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.strategy.SaAnnotationStrategy;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MapAccessor;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.ObjectUtils;
import java.lang.reflect.Method;
/**
* Sa-Token 注解鉴权 EL 表达式 AOP 切入 (用于处理 @SaCheckEL 注解)
*
* @author click33
* @since 1.40.0
*/
@Aspect
public class SaCheckELAspect implements BeanFactoryAware {
/**
* 表达式解析器 (用于解析 EL 表达式)
*/
private final ExpressionParser parser = new SpelExpressionParser();
/**
* 参数名发现器 (用于获取方法参数名)
*/
private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
/**
* Spring Bean 工厂 (用于解析 Spring 容器中的 Bean 对象)
*/
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/**
* 前置通知 (所有被 SaCheckEL 注解修饰的方法或类)
*
* @param joinPoint /
*/
@Before("@within(cn.dev33.satoken.annotation.SaCheckEL) || @annotation(cn.dev33.satoken.annotation.SaCheckEL)")
public void atBefore(JoinPoint joinPoint) {
// 获取方法签名与参数列表
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Object[] args = joinPoint.getArgs();
// 如果标注了 @SaIgnore 注解,则跳过,代表不进行校验
if(SaAnnotationStrategy.instance.isAnnotationPresent.apply(method, SaIgnore.class)) {
return;
}
// 1、根数据对象构建
// 构建校验上下文根数据对象
SaCheckELRootMap rootMap = new SaCheckELRootMap(method, extractArgs(method, args), joinPoint.getTarget() );
// 添加 this 指针指向注解函数所在类,使之可以在表达式中通过 this.xx 访问类的属性和方法 (与Target一致此处只是为了更加语义化)
rootMap.put(SaCheckELRootMap.KEY_THIS, joinPoint.getTarget());
// 添加全局默认的 StpLogic 对象,使之可以在表达式中通过 stp.checkLogin() 方式调用校验方法
rootMap.put(SaCheckELRootMap.KEY_STP, StpUtil.getStpLogic());
// 添加 JoinPoint 对象,使开发者在扩展时可以根据 JoinPoint 对象获取更多信息
rootMap.put(SaCheckELRootMap.KEY_JOIN_POINT, joinPoint);
// 执行开发者自定义的增强策略
SaAnnotationStrategy.instance.checkELRootMapExtendFunction.accept(rootMap);
// 2、表达式解析方案构建
// 创建表达式解析上下文
MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(rootMap, method, args, pnd);
// 添加属性访问器,使之可以解析 Map 对象的属性作为根上下文
context.addPropertyAccessor(new MapAccessor());
// 设置 Bean 解析器,使之可以在表达式中引用 Spring 容器管理的所有 Bean 对象
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
// 3、开始校验
// 先校验 Method 所属 Class 上的注解表达式
SaCheckEL ofClass = (SaCheckEL) SaAnnotationStrategy.instance.getAnnotation.apply(method.getDeclaringClass(), SaCheckEL.class);
if (ofClass != null) {
parser.parseExpression(ofClass.value()).getValue(context);
}
// 再校验 Method 上的注解表达式
SaCheckEL ofMethod = (SaCheckEL) SaAnnotationStrategy.instance.getAnnotation.apply(method, SaCheckEL.class);
if (ofMethod != null) {
parser.parseExpression(ofMethod.value()).getValue(context);
}
}
/**
* 如果是可变长参数,则展开并返回,否则原样返回
*
* @param method /
* @param args /
* @return /
*/
private Object[] extractArgs(Method method, Object[] args) {
if (!method.isVarArgs()) {
return args;
} else {
Object[] varArgs = ObjectUtils.toObjectArray(args[args.length - 1]);
Object[] combinedArgs = new Object[args.length - 1 + varArgs.length];
System.arraycopy(args, 0, combinedArgs, 0, args.length - 1);
System.arraycopy(varArgs, 0, combinedArgs, args.length - 1, varArgs.length);
return combinedArgs;
}
}
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.aop;
import cn.dev33.satoken.error.SaErrorCode;
import cn.dev33.satoken.exception.SaTokenException;
import java.lang.reflect.Method;
import java.util.HashMap;
/**
* Sa-Token 注解鉴权 EL 表达式解析器的根数据对象
*
* @author click33
* @since 1.40.0
*/
public class SaCheckELRootMap extends HashMap<String, Object> {
/**
* KEY标记被切入的函数
*/
public static final String KEY_METHOD = "method";
/**
* KEY标记被切入的函数参数
*/
public static final String KEY_ARGS = "args";
/**
* KEY标记被切入的目标对象
*/
public static final String KEY_TARGET = "target";
/**
* KEY标记注解所在类对象引用
*/
public static final String KEY_THIS = "this";
/**
* KEY标记全局默认 StpLogic 对象
*/
public static final String KEY_STP = "stp";
/**
* KEY标记本次切入的 JoinPoint 对象
*/
public static final String KEY_JOIN_POINT = "joinPoint";
public SaCheckELRootMap(Method method, Object[] args, Object target) {
this.put(KEY_METHOD, method);
this.put(KEY_ARGS, args);
this.put(KEY_TARGET, target);
}
/**
* 获取 被切入的函数
*
* @return method 被切入的函数
*/
public Method getMethod() {
return (Method) this.get(KEY_METHOD);
}
/**
* 获取 被切入的函数参数
*
* @return args 被切入的函数参数
*/
public Object[] getArgs() {
return (Object[]) this.get(KEY_ARGS);
}
/**
* 获取 被切入的目标对象
*
* @return target 被切入的目标对象
*/
public Object getTarget() {
return this.get(KEY_TARGET);
}
/**
* 获取 注解所在类对象引用
*
* @return this 注解所在类对象引用
*/
public Object getThis() {
return this.get(KEY_THIS);
}
/**
* 获取本次切入的 JoinPoint 对象
*/
public Object getJoinPoint() {
return this.get(KEY_JOIN_POINT);
}
/**
* 断言函数, 表达式执行结果为true才能通过
*
* @param flag 执行结果
*/
public void NEED(boolean flag) {
NEED(flag, SaErrorCode.CODE_UNDEFINED, "未通过 EL 表达式校验");
}
/**
* 断言函数, 表达式执行结果为true才能通过并在未通过时抛出 SaTokenException 异常,异常描述信息为 errorMessage
*
* @param flag 执行结果
*/
public void NEED(boolean flag, String errorMessage) {
NEED(flag, SaErrorCode.CODE_UNDEFINED, errorMessage);
}
/**
* 断言函数, 表达式执行结果为true才能通过并在未通过时抛出 SaTokenException 异常,异常码为 errorCode异常描述信息为 errorMessage
*
* @param flag 执行结果
*/
public void NEED(boolean flag, int errorCode, String errorMessage) {
if(!flag) {
throw new SaTokenException(errorCode, errorMessage);
}
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.dev33.satoken.aop.SaCheckELAspect

View File

@@ -0,0 +1,15 @@
{
"cn.dev33.satoken.annotation.SaCheckEL@value": {
"method": {
"parameters": true,
"parametersPrefix": [
"p",
"a"
]
},
"fields": {
"root": "cn.dev33.satoken.aop.SaCheckELRootMap",
"stp": "cn.dev33.satoken.stp.StpLogic"
}
}
}