refactor(common-satoken): 收敛登录助手、权限解析与 DAO 缓存逻辑

- LoginHelper 改成更稳的空值/未登录处理,isLogin() 直接走 StpUtil.isLogin()
  - SaPermissionImpl 把菜单/角色权限提取收敛成一套逻辑
  - PlusSaTokenDao 抽了公共写入、读取、TTL 转换逻辑,并修了 searchData 的本地缓存键
  - SaTokenExceptionHandler 合并了角色/权限异常处理
This commit is contained in:
疯狂的狮子Li
2026-05-16 15:58:41 +08:00
parent 3ca1e6d45d
commit 63cd82f4a8
4 changed files with 133 additions and 104 deletions
@@ -37,8 +37,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
*/
@Override
public String get(String key) {
Object o = CAFFEINE.get(key, k -> RedisUtils.getCacheObject(key));
return (String) o;
return getCacheValue(key);
}
/**
@@ -46,16 +45,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
*/
@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 {
RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
}
CAFFEINE.invalidate(key);
writeValue(key, value, timeout);
}
/**
@@ -65,7 +55,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
public void update(String key, String value) {
if (RedisUtils.hasKey(key)) {
RedisUtils.setCacheObject(key, value, true);
CAFFEINE.invalidate(key);
invalidate(key);
}
}
@@ -75,7 +65,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
@Override
public void delete(String key) {
if (RedisUtils.deleteObject(key)) {
CAFFEINE.invalidate(key);
invalidate(key);
}
}
@@ -84,9 +74,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
*/
@Override
public long getTimeout(String key) {
long timeout = RedisUtils.getTimeToLive(key);
// 加1的目的 解决sa-token使用秒 redis是毫秒导致1秒的精度问题 手动补偿
return timeout < 0 ? timeout : timeout / 1000 + 1;
return toTimeoutSeconds(RedisUtils.getTimeToLive(key));
}
/**
@@ -103,8 +91,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
*/
@Override
public Object getObject(String key) {
Object o = CAFFEINE.get(key, k -> RedisUtils.getCacheObject(key));
return o;
return getCacheValue(key);
}
/**
@@ -113,11 +100,9 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
* @param key 键名称
* @return object
*/
@SuppressWarnings("unchecked")
@Override
public <T> T getObject(String key, Class<T> classType) {
Object o = CAFFEINE.get(key, k -> RedisUtils.getCacheObject(key));
return (T) o;
return classType.cast(getCacheValue(key));
}
/**
@@ -125,16 +110,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
*/
@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 {
RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
}
CAFFEINE.invalidate(key);
writeValue(key, object, timeout);
}
/**
@@ -144,7 +120,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
public void updateObject(String key, Object object) {
if (RedisUtils.hasKey(key)) {
RedisUtils.setCacheObject(key, object, true);
CAFFEINE.invalidate(key);
invalidate(key);
}
}
@@ -154,7 +130,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
@Override
public void deleteObject(String key) {
if (RedisUtils.deleteObject(key)) {
CAFFEINE.invalidate(key);
invalidate(key);
}
}
@@ -163,9 +139,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
*/
@Override
public long getObjectTimeout(String key) {
long timeout = RedisUtils.getTimeToLive(key);
// 加1的目的 解决sa-token使用秒 redis是毫秒导致1秒的精度问题 手动补偿
return timeout < 0 ? timeout : timeout / 1000 + 1;
return toTimeoutSeconds(RedisUtils.getTimeToLive(key));
}
/**
@@ -182,11 +156,63 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
@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);
String pattern = prefix + "*" + keyword + "*";
String cacheKey = pattern + start + ":" + size + ":" + 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;
}
}
@@ -3,16 +3,16 @@ 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.enums.UserType;
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.core.utils.StringUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.api.model.LoginUser;
import java.util.Collection;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
/**
* sa-token 权限管理实现类
@@ -30,26 +30,7 @@ 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) {
// 其他端 自行根据业务编写
}
if (CollUtil.isNotEmpty(loginUser.getMenuPermission())) {
// SYS_USER 默认返回权限
return new ArrayList<>(loginUser.getMenuPermission());
} else {
return new ArrayList<>();
}
return resolvePermissionList(loginId, LoginUser::getMenuPermission, PermissionService::getMenuPermission);
}
/**
@@ -61,26 +42,33 @@ public class SaPermissionImpl implements StpInterface {
*/
@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)) {
List<String> list = StringUtils.splitList(loginId.toString(), ":");
return new ArrayList<>(permissionService.getRolePermission(Long.parseLong(list.get(1))));
} else {
throw new ServiceException("PermissionService 实现类不存在");
return new ArrayList<>(remotePermissionExtractor.apply(permissionService, resolveUserId(loginId)));
}
throw new ServiceException("PermissionService 实现类不存在");
}
UserType userType = UserType.getUserType(loginUser.getUserType());
if (userType == UserType.APP_USER) {
// 其他端 自行根据业务编写
}
if (CollUtil.isNotEmpty(loginUser.getRolePermission())) {
// SYS_USER 默认返回权限
return new ArrayList<>(loginUser.getRolePermission());
} else {
return new ArrayList<>();
Collection<String> permissionList = localPermissionExtractor.apply(loginUser);
if (CollUtil.isNotEmpty(permissionList)) {
return new ArrayList<>(permissionList);
}
return new ArrayList<>();
}
/**
@@ -96,4 +84,19 @@ public class SaPermissionImpl implements StpInterface {
}
}
/**
* 从登录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));
}
}
@@ -26,24 +26,11 @@ public class SaTokenExceptionHandler {
* @param request 当前请求
* @return 统一失败响应
*/
@ExceptionHandler(NotPermissionException.class)
public R<Void> handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
@ExceptionHandler({NotPermissionException.class, NotRoleException.class})
public R<Void> handleNotAccessException(RuntimeException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
return R.fail(HttpStatus.HTTP_FORBIDDEN, "没有访问权限,请联系管理员授权");
}
/**
* 处理角色权限校验失败异常。
*
* @param e 异常信息
* @param request 当前请求
* @return 统一失败响应
*/
@ExceptionHandler(NotRoleException.class)
public R<Void> handleNotRoleException(NotRoleException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',角色权限校验失败'{}'", requestURI, e.getMessage());
String reason = e instanceof NotRoleException ? "角色权限校验失败" : "权限码校验失败";
log.error("请求地址'{}',{}'{}'", requestURI, reason, e.getMessage());
return R.fail(HttpStatus.HTTP_FORBIDDEN, "没有访问权限,请联系管理员授权");
}
@@ -3,6 +3,7 @@ package org.dromara.common.satoken.utils;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.dev33.satoken.exception.NotLoginException;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.useragent.UserAgent;
@@ -98,13 +99,12 @@ public class LoginHelper {
*
* @return 登录用户
*/
@SuppressWarnings("unchecked")
public static <T extends LoginUser> T getLoginUser() {
SaSession session = StpUtil.getTokenSession();
if (ObjectUtil.isNull(session)) {
try {
return getLoginUser(StpUtil.getTokenSession());
} catch (NotLoginException e) {
return null;
}
return (T) session.get(LOGIN_USER_KEY);
}
/**
@@ -113,9 +113,27 @@ public class LoginHelper {
* @param token Token
* @return 登录用户
*/
@SuppressWarnings("unchecked")
public static <T extends LoginUser> T getLoginUser(String token) {
SaSession session = StpUtil.getTokenSessionByToken(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;
}
@@ -225,12 +243,7 @@ public class LoginHelper {
* @return 是否已登录
*/
public static boolean isLogin() {
try {
StpUtil.checkLogin();
return true;
} catch (Exception e) {
return false;
}
return StpUtil.isLogin();
}
}