This commit is contained in:
2025-06-27 10:23:57 +08:00
parent 15d7ef8771
commit b94549185c
774 changed files with 3543 additions and 3558 deletions

View File

@ -0,0 +1,54 @@
package com.fuyuanshen.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 com.fuyuanshen.common.core.factory.YmlPropertySourceFactory;
import com.fuyuanshen.common.satoken.core.dao.PlusSaTokenDao;
import com.fuyuanshen.common.satoken.core.service.SaPermissionImpl;
import com.fuyuanshen.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 {
@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();
}
}

View File

@ -0,0 +1,196 @@
package com.fuyuanshen.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 com.fuyuanshen.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) {
Object o = CAFFEINE.get(key, k -> RedisUtils.getCacheObject(key));
return (String) o;
}
/**
* 写入Value并设定存活时间 (单位: 秒)
*/
@Override
public void set(String key, String value, long timeout) {
if (timeout == 0 || timeout <= NOT_VALUE_EXPIRE) {
return;
}
// 判断是否为永不过期
if (timeout == NEVER_EXPIRE) {
RedisUtils.setCacheObject(key, value);
} else {
if (RedisUtils.hasKey(key)) {
RedisUtils.setCacheObject(key, value, true);
} else {
RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
}
}
CAFFEINE.invalidate(key);
}
/**
* 修修改指定key-value键值对 (过期时间不变)
*/
@Override
public void update(String key, String value) {
if (RedisUtils.hasKey(key)) {
RedisUtils.setCacheObject(key, value, true);
CAFFEINE.invalidate(key);
}
}
/**
* 删除Value
*/
@Override
public void delete(String key) {
RedisUtils.deleteObject(key);
}
/**
* 获取Value的剩余存活时间 (单位: 秒)
*/
@Override
public long getTimeout(String key) {
long timeout = RedisUtils.getTimeToLive(key);
// 加1的目的 解决sa-token使用秒 redis是毫秒导致1秒的精度问题 手动补偿
return timeout < 0 ? timeout : timeout / 1000 + 1;
}
/**
* 修改Value的剩余存活时间 (单位: 秒)
*/
@Override
public void updateTimeout(String key, long timeout) {
RedisUtils.expire(key, Duration.ofSeconds(timeout));
}
/**
* 获取Object如无返空
*/
@Override
public Object getObject(String key) {
Object o = CAFFEINE.get(key, k -> RedisUtils.getCacheObject(key));
return o;
}
/**
* 获取 Object (指定反序列化类型),如无返空
*
* @param key 键名称
* @return object
*/
@SuppressWarnings("unchecked cast")
@Override
public <T> T getObject(String key, Class<T> classType) {
Object o = CAFFEINE.get(key, k -> RedisUtils.getCacheObject(key));
return (T) o;
}
/**
* 写入Object并设定存活时间 (单位: 秒)
*/
@Override
public void setObject(String key, Object object, long timeout) {
if (timeout == 0 || timeout <= NOT_VALUE_EXPIRE) {
return;
}
// 判断是否为永不过期
if (timeout == NEVER_EXPIRE) {
RedisUtils.setCacheObject(key, object);
} else {
if (RedisUtils.hasKey(key)) {
RedisUtils.setCacheObject(key, object, true);
} else {
RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
}
}
CAFFEINE.invalidate(key);
}
/**
* 更新Object (过期时间不变)
*/
@Override
public void updateObject(String key, Object object) {
if (RedisUtils.hasKey(key)) {
RedisUtils.setCacheObject(key, object, true);
CAFFEINE.invalidate(key);
}
}
/**
* 删除Object
*/
@Override
public void deleteObject(String key) {
RedisUtils.deleteObject(key);
}
/**
* 获取Object的剩余存活时间 (单位: 秒)
*/
@Override
public long getObjectTimeout(String key) {
long timeout = RedisUtils.getTimeToLive(key);
// 加1的目的 解决sa-token使用秒 redis是毫秒导致1秒的精度问题 手动补偿
return timeout < 0 ? timeout : timeout / 1000 + 1;
}
/**
* 修改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 keyStr = prefix + "*" + keyword + "*";
return (List<String>) CAFFEINE.get(keyStr, k -> {
Collection<String> keys = RedisUtils.keys(keyStr);
List<String> list = new ArrayList<>(keys);
return SaFoxUtil.searchList(list, start, size, sortType);
});
}
}

View File

@ -0,0 +1,77 @@
package com.fuyuanshen.common.satoken.core.service;
import cn.dev33.satoken.stp.StpInterface;
import cn.hutool.core.util.ObjectUtil;
import com.fuyuanshen.common.core.domain.model.LoginUser;
import com.fuyuanshen.common.core.enums.UserType;
import com.fuyuanshen.common.core.exception.ServiceException;
import com.fuyuanshen.common.core.service.PermissionService;
import com.fuyuanshen.common.core.utils.SpringUtils;
import com.fuyuanshen.common.core.utils.StringUtils;
import com.fuyuanshen.common.satoken.utils.LoginHelper;
import java.util.ArrayList;
import java.util.List;
/**
* sa-token 权限管理实现类
*
* @author Lion Li
*/
public class SaPermissionImpl implements StpInterface {
/**
* 获取菜单权限列表
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
LoginUser loginUser = LoginHelper.getLoginUser();
if (ObjectUtil.isNull(loginUser) || !loginUser.getLoginId().equals(loginId)) {
PermissionService permissionService = getPermissionService();
if (ObjectUtil.isNotNull(permissionService)) {
List<String> list = StringUtils.splitList(loginId.toString(), ":");
return new ArrayList<>(permissionService.getMenuPermission(Long.parseLong(list.get(1))));
} else {
throw new ServiceException("PermissionService 实现类不存在");
}
}
UserType userType = UserType.getUserType(loginUser.getUserType());
if (userType == UserType.APP_USER) {
// 其他端 自行根据业务编写
}
// SYS_USER 默认返回权限
return new ArrayList<>(loginUser.getMenuPermission());
}
/**
* 获取角色权限列表
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
LoginUser loginUser = LoginHelper.getLoginUser();
if (ObjectUtil.isNull(loginUser) || !loginUser.getLoginId().equals(loginId)) {
PermissionService permissionService = getPermissionService();
if (ObjectUtil.isNotNull(permissionService)) {
List<String> list = StringUtils.splitList(loginId.toString(), ":");
return new ArrayList<>(permissionService.getRolePermission(Long.parseLong(list.get(1))));
} else {
throw new ServiceException("PermissionService 实现类不存在");
}
}
UserType userType = UserType.getUserType(loginUser.getUserType());
if (userType == UserType.APP_USER) {
// 其他端 自行根据业务编写
}
// SYS_USER 默认返回权限
return new ArrayList<>(loginUser.getRolePermission());
}
private PermissionService getPermissionService() {
try {
return SpringUtils.getBean(PermissionService.class);
} catch (Exception e) {
return null;
}
}
}

View File

@ -0,0 +1,52 @@
package com.fuyuanshen.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 com.fuyuanshen.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 {
/**
* 权限码异常
*/
@ExceptionHandler(NotPermissionException.class)
public R<Void> handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',权限码校验失败'{}'", requestURI, e.getMessage());
return R.fail(HttpStatus.HTTP_FORBIDDEN, "没有访问权限,请联系管理员授权");
}
/**
* 角色权限异常
*/
@ExceptionHandler(NotRoleException.class)
public R<Void> handleNotRoleException(NotRoleException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',角色权限校验失败'{}'", requestURI, e.getMessage());
return R.fail(HttpStatus.HTTP_FORBIDDEN, "没有访问权限,请联系管理员授权");
}
/**
* 认证失败
*/
@ExceptionHandler(NotLoginException.class)
public R<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源");
}
}

View File

@ -0,0 +1,216 @@
package com.fuyuanshen.common.satoken.utils;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import com.fuyuanshen.common.core.constant.SystemConstants;
import com.fuyuanshen.common.core.constant.TenantConstants;
import com.fuyuanshen.common.core.domain.model.LoginUser;
import com.fuyuanshen.common.core.enums.UserType;
import java.util.Set;
/**
* 登录鉴权助手
* <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 TENANT_KEY = "tenantId";
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";
/**
* 登录系统 基于 设备类型
* 针对相同用户体系不同设备
*
* @param loginUser 登录用户信息
* @param model 配置参数
*/
public static void login(LoginUser loginUser, SaLoginParameter model) {
model = ObjectUtil.defaultIfNull(model, new SaLoginParameter());
StpUtil.login(loginUser.getLoginId(),
model.setExtra(TENANT_KEY, loginUser.getTenantId())
.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);
}
/**
* 获取用户(多级缓存)
*/
@SuppressWarnings("unchecked cast")
public static <T extends LoginUser> T getLoginUser() {
SaSession session = StpUtil.getTokenSession();
if (ObjectUtil.isNull(session)) {
return null;
}
return (T) session.get(LOGIN_USER_KEY);
}
/**
* 获取用户基于token
*/
@SuppressWarnings("unchecked cast")
public static <T extends LoginUser> T getLoginUser(String token) {
SaSession session = StpUtil.getTokenSessionByToken(token);
if (ObjectUtil.isNull(session)) {
return null;
}
return (T) session.get(LOGIN_USER_KEY);
}
/**
* 获取用户id
*/
public static Long getUserId() {
return Convert.toLong(getExtra(USER_KEY));
}
/**
* 获取用户id
*/
public static String getUserIdStr() {
return Convert.toStr(getExtra(USER_KEY));
}
/**
* 获取用户账户
*/
public static String getUsername() {
return Convert.toStr(getExtra(USER_NAME_KEY));
}
/**
* 获取租户ID
*/
public static String getTenantId() {
return Convert.toStr(getExtra(TENANT_KEY));
}
/**
* 获取部门ID
*/
public static Long getDeptId() {
return Convert.toLong(getExtra(DEPT_KEY));
}
/**
* 获取部门名
*/
public static String getDeptName() {
return Convert.toStr(getExtra(DEPT_NAME_KEY));
}
/**
* 获取部门类别编码
*/
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;
}
}
/**
* 获取用户类型
*/
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_ID.equals(userId);
}
/**
* 是否为超级管理员
*
* @return 结果
*/
public static boolean isSuperAdmin() {
return isSuperAdmin(getUserId());
}
/**
* 是否为租户管理员
*
* @param rolePermission 角色权限标识组
* @return 结果
*/
public static boolean isTenantAdmin(Set<String> rolePermission) {
if (CollUtil.isEmpty(rolePermission)) {
return false;
}
return rolePermission.contains(TenantConstants.TENANT_ADMIN_ROLE_KEY);
}
/**
* 是否为租户管理员
*
* @return 结果
*/
public static boolean isTenantAdmin() {
LoginUser loginUser = getLoginUser();
if (loginUser == null) {
return false;
}
return Convert.toBool(isTenantAdmin(loginUser.getRolePermission()));
}
/**
* 检查当前用户是否已登录
*
* @return 结果
*/
public static boolean isLogin() {
try {
return getLoginUser() != null;
} catch (Exception e) {
return false;
}
}
}

View File

@ -0,0 +1 @@
com.fuyuanshen.common.satoken.config.SaTokenConfig

View File

@ -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"