commit:升级到vue3,更新最近工作流技术栈,支持sa-token

This commit is contained in:
Jerry
2024-07-05 22:42:33 +08:00
parent bbcc608584
commit 565ecb6371
1751 changed files with 236790 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
<?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/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>common</artifactId>
<groupId>com.orangeforms</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-satoken</artifactId>
<version>1.0.0</version>
<name>common-satoken</name>
<packaging>jar</packaging>
<properties>
<sa-token.version>1.37.0</sa-token.version>
</properties>
<dependencies>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- Sa-Token 整合Redis (使用fastjson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-fastjson</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- Sa-Token插件权限缓存与业务缓存分离 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-alone-redis</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 为satoken提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.orangeforms</groupId>
<artifactId>common-redis</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,16 @@
package com.orangeforms.common.satoken.annotation;
import java.lang.annotation.*;
/**
* 所有标记该注解的接口不能使用SaToken进行权限验证。
* 必须通过橙单自身的动态验证完成即基于URL的验证。
*
* @author Jerry
* @date 2024-07-02
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SaTokenDenyAuth {
}

View File

@@ -0,0 +1,26 @@
package com.orangeforms.common.satoken.listener;
import com.orangeforms.common.satoken.util.SaTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
/**
* 后台服务启动的时候扫描服务中标有权限字并同步到Redis以供接口查询所有使用到的权限字。
*
* @author Jerry
* @date 2024-07-02
*/
@Component
public class SaTokenPermCodeScanListener implements ApplicationListener<ApplicationReadyEvent> {
@Autowired
private SaTokenUtil saTokenUtil;
@Override
public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
saTokenUtil.collectPermCodes(event);
}
}

View File

@@ -0,0 +1,283 @@
package com.orangeforms.common.satoken.util;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.orangeforms.common.core.cache.CacheConfig;
import com.orangeforms.common.core.constant.ApplicationConstant;
import com.orangeforms.common.core.constant.ErrorCodeEnum;
import com.orangeforms.common.core.object.LoginUserInfo;
import com.orangeforms.common.core.object.ResponseResult;
import com.orangeforms.common.core.object.TokenData;
import com.orangeforms.common.core.util.AopTargetUtil;
import com.orangeforms.common.core.util.MyCommonUtil;
import com.orangeforms.common.core.util.RedisKeyUtil;
import com.orangeforms.common.satoken.annotation.SaTokenDenyAuth;
import org.redisson.api.RMap;
import org.redisson.api.RSet;
import org.redisson.api.RTopic;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.*;
/**
* 通用工具方法。
*
* @author Jerry
* @date 2024-07-02
*/
@Component
public class SaTokenUtil {
@Autowired
private RedissonClient redissonClient;
@Resource(name = "caffeineCacheManager")
private CacheManager cacheManager;
@Value("${spring.application.name}")
private String applicationName;
public static final String SA_TOKEN_PERM_CODES_KEY = "SaTokenPermCodes";
public static final String SA_TOKEN_PERM_CODES_PUBLISH_TOPIC = "SaTokenPermCodesTopic";
/**
* 处理免验证接口。目前仅用于微服务的业务服务。
*/
public void handleNoAuthIntercept() {
if (!StpUtil.isLogin()) {
return;
}
SaSession session = StpUtil.getTokenSession();
if (session != null) {
TokenData tokenData = JSON.toJavaObject(
(JSONObject) session.get(TokenData.REQUEST_ATTRIBUTE_NAME), TokenData.class);
TokenData.addToRequest(tokenData);
tokenData.setToken(session.getToken());
}
}
/**
* 处理权限验证,通常在拦截器中调用。用于微服务中业务服务。
*
* @param request 当前请求。
* @param handler 拦截器中的处理器。
* @return 拦截验证处理结果。
*/
public ResponseResult<Void> handleAuthInterceptEx(HttpServletRequest request, Object handler) {
String appCode = MyCommonUtil.getAppCodeFromRequest();
if (StrUtil.isNotBlank(appCode)) {
String token = request.getHeader(TokenData.REQUEST_ATTRIBUTE_NAME);
if (StrUtil.isBlank(token)) {
String errorMessage = "第三方登录没有包含Token信息";
return ResponseResult.error(
HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED_LOGIN, errorMessage);
}
TokenData tokenData = JSON.parseObject(token, TokenData.class);
TokenData.addToRequest(tokenData);
return ResponseResult.success();
}
String dontAuth = request.getHeader(ApplicationConstant.HTTP_HEADER_DONT_AUTH);
if (BooleanUtil.toBoolean(dontAuth)) {
this.handleNoAuthIntercept();
return ResponseResult.success();
}
return this.handleAuthIntercept(request, handler);
}
/**
* 处理权限验证,通常在拦截器中调用。通常用于单体服务。
*
* @param request 当前请求。
* @param handler 拦截器中的处理器。
* @return 拦截验证处理结果。
*/
public ResponseResult<Void> handleAuthIntercept(HttpServletRequest request, Object handler) {
if (!(handler instanceof HandlerMethod)) {
return ResponseResult.success();
}
Method method = ((HandlerMethod) handler).getMethod();
String errorMessage;
//如果没有登录则直接交给satoken注解去验证。
if (!StpUtil.isLogin()) {
// 如果此 Method 或其所属 Class 标注了 @SaIgnore则忽略掉鉴权
if (BooleanUtil.isTrue(SaStrategy.instance.isAnnotationPresent.apply(method, SaIgnore.class))) {
return ResponseResult.success();
}
errorMessage = "非免登录接口必须包含Token信息";
return ResponseResult.error(HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED_LOGIN, errorMessage);
}
//对于已经登录的用户一定存在session对象。
SaSession session = StpUtil.getTokenSession();
if (session == null) {
errorMessage = "用户会话已过期,请重新登录!";
return ResponseResult.error(HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED_LOGIN, errorMessage);
}
TokenData tokenData = JSON.toJavaObject(
(JSONObject) session.get(TokenData.REQUEST_ATTRIBUTE_NAME), TokenData.class);
TokenData.addToRequest(tokenData);
//将最初前端请求使用的token数据赋值给TokenData对象以便于再次调用其他API接口时直接使用。
tokenData.setToken(session.getToken());
//如果是管理员可以直接跳过验证了。
//基于橙单内部的权限规则优先验证,主要用于内部的白名单接口,以及在线表单和工作流那些动态接口的权限验证。
if (Boolean.TRUE.equals(tokenData.getIsAdmin())
|| this.hasPermission(tokenData.getSessionId(), request.getRequestURI())) {
return ResponseResult.success();
}
//对于应由白名单鉴权的接口都会添加SaTokenDenyAuth注解因此这里需要判断一下
//对于此类接口无需SaToken验证了而是直接返回未授权因为基于url的鉴权在上面的hasPermission中完成了。
if (method.getAnnotation(SaTokenDenyAuth.class) != null) {
return ResponseResult.error(HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.NO_OPERATION_PERMISSION);
}
try {
//执行基于stoken的注解鉴权。
SaStrategy.instance.checkMethodAnnotation.accept(method);
} catch (SaTokenException e) {
return ResponseResult.error(HttpServletResponse.SC_UNAUTHORIZED, ErrorCodeEnum.NO_OPERATION_PERMISSION);
}
return ResponseResult.success();
}
/**
* 构建satoken的登录Id。
*
* @return 拼接后的完整登录Id。
*/
public static String makeLoginId(LoginUserInfo userInfo) {
StringBuilder sb = new StringBuilder(128);
sb.append("SATOKEN_LOGIN:");
if (userInfo.getTenantId() != null) {
sb.append(userInfo.getTenantId()).append(":");
}
sb.append(userInfo.getLoginName()).append(":").append(userInfo.getUserId());
return sb.toString();
}
/**
* 获取所有的权限字列表数据。
*
* @return 所有的权限字列表数据。
*/
public List<String> getAllPermCodes() {
RMap<String, Set<String>> permCodeMap = redissonClient.getMap(SA_TOKEN_PERM_CODES_KEY);
if (!permCodeMap.isExists()) {
return CollUtil.empty(String.class);
}
Set<String> permCodeSet = new TreeSet<>();
for (RMap.Entry<String, Set<String>> entry : permCodeMap.entrySet()) {
CollUtil.addAll(permCodeSet, permCodeMap.get(entry.getKey()));
}
return new LinkedList<>(permCodeSet);
}
/**
* 获取所有租户运营应用的权限字列表数据。
*
* @return 所有的权限字列表数据。
*/
public List<String> getAllTenantPermCodes() {
RMap<String, Set<String>> permCodeMap = redissonClient.getMap(SA_TOKEN_PERM_CODES_KEY);
if (!permCodeMap.isExists()) {
return CollUtil.empty(String.class);
}
Set<String> permCodeSet = new TreeSet<>();
for (RMap.Entry<String, Set<String>> entry : permCodeMap.entrySet()) {
if (!entry.getKey().equals(ApplicationConstant.TENANT_ADMIN_APP_NAME)) {
CollUtil.addAll(permCodeSet, permCodeMap.get(entry.getKey()));
}
}
return new LinkedList<>(permCodeSet);
}
/**
* 获取所有租户管理应用的权限字列表数据。
*
* @return 所有的权限字列表数据。
*/
public List<String> getAllTenantAdminPermCodes() {
RMap<String, Set<String>> permCodeMap = redissonClient.getMap(SA_TOKEN_PERM_CODES_KEY);
if (!permCodeMap.isExists()) {
return CollUtil.empty(String.class);
}
Set<String> permCodeSet = new TreeSet<>();
for (RMap.Entry<String, Set<String>> entry : permCodeMap.entrySet()) {
if (entry.getKey().equals(ApplicationConstant.TENANT_ADMIN_APP_NAME)) {
CollUtil.addAll(permCodeSet, permCodeMap.get(entry.getKey()));
}
}
return new LinkedList<>(permCodeSet);
}
/**
* 收集当前服务的SaToken权限字列表并缓存到Redis便于统一查询。
*
* @param event 服务应用的启动事件。
*/
public void collectPermCodes(ApplicationReadyEvent event) {
redissonClient.getTopic(SA_TOKEN_PERM_CODES_PUBLISH_TOPIC)
.addListener(String.class, (channel, message) -> this.doCollect(event));
this.doCollect(event);
}
/**
* 向所有已启动的服务发送权限字同步事件。
*/
public void publishCollectPermCodes() {
RTopic topic = redissonClient.getTopic(SA_TOKEN_PERM_CODES_PUBLISH_TOPIC);
topic.publish(null);
}
private void doCollect(ApplicationReadyEvent event) {
Map<String, Object> controllerMap = event.getApplicationContext().getBeansWithAnnotation(RestController.class);
Set<String> permCodes = new HashSet<>();
for (Map.Entry<String, Object> entry : controllerMap.entrySet()) {
Object targetBean = AopTargetUtil.getTarget(entry.getValue());
Method[] methods = ReflectUtil.getPublicMethods(targetBean.getClass());
Arrays.stream(methods)
.map(m -> m.getAnnotation(SaCheckPermission.class))
.filter(Objects::nonNull)
.forEach(anno -> Collections.addAll(permCodes, anno.value()));
}
RMap<String, Set<String>> permCodeMap = redissonClient.getMap(SA_TOKEN_PERM_CODES_KEY);
permCodeMap.put(applicationName, permCodes);
}
@SuppressWarnings("unchecked")
private boolean hasPermission(String sessionId, String url) {
// 为了提升效率先检索Caffeine的一级缓存如果不存在再检索Redis的二级缓存并将结果存入一级缓存。
Set<String> localPermSet;
String permKey = RedisKeyUtil.makeSessionPermIdKey(sessionId);
Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.USER_PERMISSION_CACHE.name());
Assert.notNull(cache, "Cache USER_PERMISSION_CACHE can't be NULL.");
Cache.ValueWrapper wrapper = cache.get(permKey);
if (wrapper == null) {
RSet<String> permSet = redissonClient.getSet(permKey);
localPermSet = permSet.readAll();
cache.put(permKey, localPermSet);
} else {
localPermSet = (Set<String>) wrapper.get();
}
return CollUtil.contains(localPermSet, url);
}
}

View File

@@ -0,0 +1,62 @@
package com.orangeforms.common.satoken.util;
import cn.dev33.satoken.stp.StpInterface;
import com.orangeforms.common.core.cache.CacheConfig;
import com.orangeforms.common.core.object.TokenData;
import com.orangeforms.common.core.util.RedisKeyUtil;
import org.redisson.api.RSet;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* 自定义权限加载接口实现类
*
* @author Jerry
* @date 2024-07-02
*/
@Component
public class StpInterfaceImpl implements StpInterface {
@Autowired
private RedissonClient redissonClient;
@Resource(name = "caffeineCacheManager")
private CacheManager cacheManager;
/**
* 返回一个账号所拥有的权限码集合
*/
@SuppressWarnings("unchecked")
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
TokenData tokenData = TokenData.takeFromRequest();
String permCodeKey = RedisKeyUtil.makeSessionPermCodeKey(tokenData.getSessionId());
Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.USER_PERM_CODE_CACHE.name());
Assert.notNull(cache, "Cache USER_PERM_CODE_CACHE can't be NULL");
Cache.ValueWrapper wrapper = cache.get(permCodeKey);
if (wrapper != null) {
return (List<String>) wrapper.get();
}
RSet<String> permCodeSet = redissonClient.getSet(permCodeKey);
Set<String> localPermCodeSet = permCodeSet.readAll();
List<String> permCodeList = new ArrayList<>(localPermCodeSet);
cache.put(permCodeKey, permCodeList);
return permCodeList;
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
return new ArrayList<>();
}
}