init: 导入RuoYi‑Vue‑Plus 6.X完整代码

This commit is contained in:
admin
2026-08-09 17:51:11 +08:00
commit 51cac459b3
914 changed files with 103833 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
<?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>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-satoken</artifactId>
<description>
ruoyi-common-satoken 权限认证
</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- RuoYi Common Redis-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<!-- api模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-api</artifactId>
</dependency>
<!-- spring-webmvc 依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot4-starter</artifactId>
</dependency>
<!-- Sa-Token 整合 jwt -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<exclusions>
<exclusion>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 本地缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,59 @@
package org.dromara.common.satoken.config;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpLogic;
import org.dromara.common.core.factory.YmlPropertySourceFactory;
import org.dromara.common.satoken.core.dao.PlusSaTokenDao;
import org.dromara.common.satoken.core.service.SaPermissionImpl;
import org.dromara.common.satoken.handler.SaTokenExceptionHandler;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
/**
* sa-token 配置
*
* @author Lion Li
*/
@AutoConfiguration
@PropertySource(value = "classpath:common-satoken.yml", factory = YmlPropertySourceFactory.class)
public class SaTokenConfig {
/**
* 创建 Sa-Token JWT 登录逻辑。
*
* @return JWT 登录逻辑
*/
@Bean
public StpLogic getStpLogicJwt() {
// Sa-Token 整合 jwt (简单模式)
return new StpLogicJwtForSimple();
}
/**
* 权限接口实现(使用bean注入方便用户替换)
*/
@Bean
public StpInterface stpInterface() {
return new SaPermissionImpl();
}
/**
* 自定义dao层存储
*/
@Bean
public SaTokenDao saTokenDao() {
return new PlusSaTokenDao();
}
/**
* 异常处理器
*/
@Bean
public SaTokenExceptionHandler saTokenExceptionHandler() {
return new SaTokenExceptionHandler();
}
}
@@ -0,0 +1,219 @@
package org.dromara.common.satoken.core.dao;
import cn.dev33.satoken.dao.auto.SaTokenDaoBySessionFollowObject;
import cn.dev33.satoken.util.SaFoxUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.redis.utils.RedisUtils;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Sa-Token持久层接口(使用框架自带RedisUtils实现 协议统一)
* <p>
* 采用 caffeine + redis 多级缓存 优化并发查询效率
* <p>
* SaTokenDaoBySessionFollowObject 是 SaTokenDao 子集简化了session方法处理
*
* @author Lion Li
*/
public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
private static final Cache<String, Object> CAFFEINE = Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(5, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(1000)
.build();
/**
* 获取Value,如无返空
*/
@Override
public String get(String key) {
return getCacheValue(key);
}
/**
* 写入Value,并设定存活时间 (单位: 秒)
*/
@Override
public void set(String key, String value, long timeout) {
writeValue(key, value, timeout);
}
/**
* 修修改指定key-value键值对 (过期时间不变)
*/
@Override
public void update(String key, String value) {
if (RedisUtils.hasKey(key)) {
RedisUtils.setCacheObject(key, value, true);
invalidate(key);
}
}
/**
* 删除Value
*/
@Override
public void delete(String key) {
if (RedisUtils.deleteObject(key)) {
invalidate(key);
}
}
/**
* 获取Value的剩余存活时间 (单位: 秒)
*/
@Override
public long getTimeout(String key) {
return toTimeoutSeconds(RedisUtils.getTimeToLive(key));
}
/**
* 修改Value的剩余存活时间 (单位: 秒)
*/
@Override
public void updateTimeout(String key, long timeout) {
RedisUtils.expire(key, Duration.ofSeconds(timeout));
}
/**
* 获取Object,如无返空
*/
@Override
public Object getObject(String key) {
return getCacheValue(key);
}
/**
* 获取 Object (指定反序列化类型),如无返空
*
* @param key 键名称
* @return object
*/
@Override
public <T> T getObject(String key, Class<T> classType) {
return classType.cast(getCacheValue(key));
}
/**
* 写入Object,并设定存活时间 (单位: 秒)
*/
@Override
public void setObject(String key, Object object, long timeout) {
writeValue(key, object, timeout);
}
/**
* 更新Object (过期时间不变)
*/
@Override
public void updateObject(String key, Object object) {
if (RedisUtils.hasKey(key)) {
RedisUtils.setCacheObject(key, object, true);
invalidate(key);
}
}
/**
* 删除Object
*/
@Override
public void deleteObject(String key) {
if (RedisUtils.deleteObject(key)) {
invalidate(key);
}
}
/**
* 获取Object的剩余存活时间 (单位: 秒)
*/
@Override
public long getObjectTimeout(String key) {
return toTimeoutSeconds(RedisUtils.getTimeToLive(key));
}
/**
* 修改Object的剩余存活时间 (单位: 秒)
*/
@Override
public void updateObjectTimeout(String key, long timeout) {
RedisUtils.expire(key, Duration.ofSeconds(timeout));
}
/**
* 搜索数据
*/
@SuppressWarnings("unchecked")
@Override
public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
String pattern = prefix + "*" + keyword + "*";
String cacheKey = pattern + start + StringUtils.COLON + size + StringUtils.COLON + sortType;
return (List<String>) CAFFEINE.get(cacheKey, k -> {
Collection<String> keys = RedisUtils.keys(pattern);
List<String> list = new ArrayList<>(keys);
return SaFoxUtil.searchList(list, start, size, sortType);
});
}
/**
* 从缓存读取对象。
*
* @param key 缓存键
* @return 缓存值
*/
@SuppressWarnings("unchecked")
private <T> T getCacheValue(String key) {
return (T) CAFFEINE.get(key, RedisUtils::getCacheObject);
}
/**
* 写入缓存值并刷新本地缓存。
*
* @param key 缓存键
* @param value 缓存值
* @param timeout 超时时间
*/
private void writeValue(String key, Object value, long timeout) {
if (timeout == 0 || timeout <= NOT_VALUE_EXPIRE) {
return;
}
if (timeout == NEVER_EXPIRE) {
RedisUtils.setCacheObject(key, value);
} else {
RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
}
invalidate(key);
}
/**
* 清除本地缓存。
*
* @param key 缓存键
*/
private void invalidate(String key) {
CAFFEINE.invalidate(key);
}
/**
* 将 Redis TTL 转为秒。
*
* @param timeoutRedis Redis TTL 毫秒值
* @return Sa-Token 需要的秒值
*/
private long toTimeoutSeconds(long timeoutRedis) {
// 加1的目的 解决sa-token使用秒 redis是毫秒导致1秒的精度问题 手动补偿
return timeoutRedis < 0 ? timeoutRedis : timeoutRedis / 1000 + 1;
}
}
@@ -0,0 +1,102 @@
package org.dromara.common.satoken.core.service;
import cn.dev33.satoken.stp.StpInterface;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.service.PermissionService;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.api.model.LoginUser;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;
/**
* sa-token 权限管理实现类
*
* @author Lion Li
*/
public class SaPermissionImpl implements StpInterface {
/**
* 获取指定登录对象的菜单权限列表。
*
* @param loginId 登录ID
* @param loginType 登录类型
* @return 菜单权限列表
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
return resolvePermissionList(loginId, LoginUser::getMenuPermission, PermissionService::getMenuPermission);
}
/**
* 获取指定登录对象的角色权限列表。
*
* @param loginId 登录ID
* @param loginType 登录类型
* @return 角色权限列表
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
return resolvePermissionList(loginId, LoginUser::getRolePermission, PermissionService::getRolePermission);
}
/**
* 解析当前登录对象的权限列表。
*
* @param loginId 登录ID
* @param localPermissionExtractor 当前登录用户权限提取器
* @param remotePermissionExtractor 远程权限提取器
* @return 权限列表
*/
private List<String> resolvePermissionList(Object loginId,
java.util.function.Function<LoginUser, Collection<String>> localPermissionExtractor,
BiFunction<PermissionService, Long, Collection<String>> remotePermissionExtractor) {
LoginUser loginUser = LoginHelper.getLoginUser();
if (ObjectUtil.isNull(loginUser) || !loginUser.getLoginId().equals(loginId)) {
PermissionService permissionService = getPermissionService();
if (ObjectUtil.isNotNull(permissionService)) {
return new ArrayList<>(remotePermissionExtractor.apply(permissionService, resolveUserId(loginId)));
}
throw new ServiceException("PermissionService 实现类不存在");
}
Collection<String> permissionList = localPermissionExtractor.apply(loginUser);
if (CollUtil.isNotEmpty(permissionList)) {
return new ArrayList<>(permissionList);
}
return new ArrayList<>();
}
/**
* 获取权限服务实现。
*
* @return 权限服务
*/
private PermissionService getPermissionService() {
try {
return SpringUtils.getBean(PermissionService.class);
} catch (Exception e) {
return null;
}
}
/**
* 从登录ID中提取用户ID。
*
* @param loginId 登录ID
* @return 用户ID
*/
private Long resolveUserId(Object loginId) {
String loginIdStr = loginId.toString();
int separatorIndex = loginIdStr.indexOf(':');
if (separatorIndex < 0 || separatorIndex == loginIdStr.length() - 1) {
throw new ServiceException("登录ID格式错误");
}
return Long.parseLong(loginIdStr.substring(separatorIndex + 1));
}
}
@@ -0,0 +1,58 @@
package org.dromara.common.satoken.handler;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.hutool.http.HttpStatus;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* SaToken异常处理器
*
* @author Lion Li
*/
@Slf4j
@RestControllerAdvice
public class SaTokenExceptionHandler {
/**
* 处理权限码校验失败异常。
*
* @param e 异常信息
* @param request 当前请求
* @return 统一失败响应
*/
@ExceptionHandler({NotPermissionException.class, NotRoleException.class})
public R<Void> handleNotAccessException(RuntimeException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
String reason = e instanceof NotRoleException ? "角色权限校验失败" : "权限码校验失败";
log.error("请求地址'{}',{}'{}'", requestURI, reason, e.getMessage());
return R.fail(HttpStatus.HTTP_FORBIDDEN, "没有访问权限,请联系管理员授权");
}
/**
* 处理未登录或登录态失效异常。
*
* @param e 异常信息
* @param request 当前请求
* @return 统一失败响应
*/
@ExceptionHandler(NotLoginException.class)
public R<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
String msg = switch (e.getType()) {
case NotLoginException.TOKEN_TIMEOUT,
NotLoginException.TOKEN_FREEZE -> "登录已过期,请重新登录";
case NotLoginException.BE_REPLACED -> "当前账号已在其他设备登录,您已被强制下线";
case NotLoginException.KICK_OUT -> "账号已被管理员强制下线";
default -> "登录状态异常,请重新登录";
};
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, msg);
}
}
@@ -0,0 +1,249 @@
package org.dromara.common.satoken.utils;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.enums.UserType;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ip.AddressUtils;
import org.dromara.system.api.model.LoginUser;
/**
* 登录鉴权助手
* <p>
* user_type 为 用户类型 同一个用户表 可以有多种用户类型 例如 pc,app
* deivce 为 设备类型 同一个用户类型 可以有 多种设备类型 例如 web,ios
* 可以组成 用户类型与设备类型多对多的 权限灵活控制
* <p>
* 多用户体系 针对 多种用户类型 但权限控制不一致
* 可以组成 多用户类型表与多设备类型 分别控制权限
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LoginHelper {
public static final String LOGIN_USER_KEY = "loginUser";
public static final String USER_KEY = "userId";
public static final String USER_NAME_KEY = "userName";
public static final String DEPT_KEY = "deptId";
public static final String DEPT_NAME_KEY = "deptName";
public static final String DEPT_CATEGORY_KEY = "deptCategory";
public static final String CLIENT_KEY = "clientid";
public static final String CLIENT_ACCESS_PATH_KEY = "clientAccessPath";
public static final String CLIENT_IP_WHITELIST_KEY = "clientIpWhitelist";
/**
* 登录系统 基于 设备类型
* 针对相同用户体系不同设备
*
* @param loginUser 登录用户信息
* @param model 配置参数
*/
public static void login(LoginUser loginUser, SaLoginParameter model) {
model = ObjectUtil.defaultIfNull(model, new SaLoginParameter());
fillRequestContext(loginUser, model);
StpUtil.login(loginUser.getLoginId(),
model.setExtra(USER_KEY, loginUser.getUserId())
.setExtra(USER_NAME_KEY, loginUser.getUsername())
.setExtra(DEPT_KEY, loginUser.getDeptId())
.setExtra(DEPT_NAME_KEY, loginUser.getDeptName())
.setExtra(DEPT_CATEGORY_KEY, loginUser.getDeptCategory())
);
StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
}
/**
* 在登录时补充当前请求上下文,避免登录态中的终端信息缺失。
*
* @param loginUser 登录用户
* @param model 登录参数
*/
private static void fillRequestContext(LoginUser loginUser, SaLoginParameter model) {
HttpServletRequest request = ServletUtils.getRequest();
if (ObjectUtil.isNull(request)) {
return;
}
String ip = ServletUtils.getClientIP(request);
if (StringUtils.isBlank(loginUser.getIpaddr())) {
loginUser.setIpaddr(ip);
}
if (StringUtils.isBlank(loginUser.getLoginLocation()) && StringUtils.isNotBlank(ip)) {
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
}
UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
if (StringUtils.isBlank(loginUser.getBrowser())) {
loginUser.setBrowser(userAgent.getBrowser().getName());
}
if (StringUtils.isBlank(loginUser.getOs())) {
loginUser.setOs(userAgent.getOs().getName());
}
if (StringUtils.isBlank(loginUser.getDeviceType()) && StringUtils.isNotBlank(model.getDeviceType())) {
loginUser.setDeviceType(model.getDeviceType());
}
}
/**
* 获取当前 Token 对应的登录用户信息。
*
* @return 登录用户
*/
public static <T extends LoginUser> T getLoginUser() {
try {
return getLoginUser(StpUtil.getTokenSession());
} catch (NotLoginException e) {
return null;
}
}
/**
* 根据指定 Token 获取登录用户信息。
*
* @param token Token
* @return 登录用户
*/
public static <T extends LoginUser> T getLoginUser(String token) {
if (StringUtils.isBlank(token)) {
return null;
}
SaSession session;
try {
session = StpUtil.getTokenSessionByToken(token);
} catch (NotLoginException e) {
return null;
}
return getLoginUser(session);
}
/**
* 从会话中读取登录用户。
*
* @param session 登录会话
* @return 登录用户
*/
@SuppressWarnings("unchecked")
private static <T extends LoginUser> T getLoginUser(SaSession session) {
if (ObjectUtil.isNull(session)) {
return null;
}
return (T) session.get(LOGIN_USER_KEY);
}
/**
* 获取当前登录用户 ID。
*
* @return 用户 ID
*/
public static Long getUserId() {
return Convert.toLong(getExtra(USER_KEY));
}
/**
* 获取当前登录用户 ID 字符串。
*
* @return 用户 ID 字符串
*/
public static String getUserIdStr() {
return Convert.toStr(getExtra(USER_KEY));
}
/**
* 获取当前登录用户名。
*
* @return 用户名
*/
public static String getUsername() {
return Convert.toStr(getExtra(USER_NAME_KEY));
}
/**
* 获取当前登录用户部门 ID。
*
* @return 部门 ID
*/
public static Long getDeptId() {
return Convert.toLong(getExtra(DEPT_KEY));
}
/**
* 获取当前登录用户部门名称。
*
* @return 部门名称
*/
public static String getDeptName() {
return Convert.toStr(getExtra(DEPT_NAME_KEY));
}
/**
* 获取当前登录用户部门类别编码。
*
* @return 部门类别编码
*/
public static String getDeptCategory() {
return Convert.toStr(getExtra(DEPT_CATEGORY_KEY));
}
/**
* 获取当前 Token 的扩展信息
*
* @param key 键值
* @return 对应的扩展数据
*/
private static Object getExtra(String key) {
try {
return StpUtil.getExtra(key);
} catch (Exception e) {
return null;
}
}
/**
* 获取当前登录用户类型。
*
* @return 用户类型
*/
public static UserType getUserType() {
String loginType = StpUtil.getLoginIdAsString();
return UserType.getUserType(loginType);
}
/**
* 是否为超级管理员
*
* @param userId 用户ID
* @return 是否为超级管理员
*/
public static boolean isSuperAdmin(Long userId) {
return SystemConstants.SUPER_ADMIN_USER_ID.equals(userId);
}
/**
* 是否为超级管理员
*
* @return 是否为超级管理员
*/
public static boolean isSuperAdmin() {
return isSuperAdmin(getUserId());
}
/**
* 检查当前用户是否已登录
*
* @return 是否已登录
*/
public static boolean isLogin() {
return StpUtil.isLogin();
}
}
@@ -0,0 +1 @@
org.dromara.common.satoken.config.SaTokenConfig
@@ -0,0 +1,13 @@
# 内置配置 不允许修改 如需修改请在 nacos 上写相同配置覆盖
# Sa-Token配置
sa-token:
# 允许动态设置 token 有效期
dynamic-active-timeout: true
# 允许从 请求参数 读取 token
is-read-body: true
# 允许从 header 读取 token
is-read-header: true
# 关闭 cookie 鉴权 从根源杜绝 csrf 漏洞风险
is-read-cookie: false
# token前缀
token-prefix: "Bearer"