一、权限拦截失效问题
1.1 问题现象
在项目运行过程中发现,部分需要权限验证的接口(如 /client/getChatModels)在未携带 token 的情况下仍然可以正常访问,权限拦截机制完全失效。
测试验证:
bash
# 未携带 token 的请求
curl http://localhost:8100/client/getChatModels
# 期望:返回 401 Unauthorized
# 实际:返回 200 OK,成功获取数据
1.2 问题分析
1.2.1 排查思路
- 检查接口定义:确认接口路径和注解配置
- 检查拦截器配置:验证 Sa-Token 拦截器是否正确注册
- 检查路由规则 :分析
SaRouter的匹配和排除逻辑
1.2.2 核心代码分析
定位到 AuthConfiguration.java 中的路由配置:
java
@Bean
public SaServletFilter saServletFilter() {
return new SaServletFilter()
.addInclude("/**")
.addExclude("/favicon.ico")
.setAuth(obj -> {
SaRouter
.match("/upms/**", "/aigc/**", "/app/**")
.check(StpUtil::checkLogin)
.match("/client/**")
.check(ClientStpUtil::checkLogin)
.notMatch(skipUrl) // ❌ 问题所在
.notMatch(authProps.getSkipUrl().toArray(new String[0]))
;
})
.setError(this::handleError);
}
问题根源:
Sa-Token 的 SaRouter 采用链式调用设计,其执行顺序至关重要:
.match(pattern)- 定义匹配规则.notMatch(pattern)- 定义排除规则.check(handler)- 执行权限校验
错误的调用顺序:
.match() → .check() → .match() → .check() → .notMatch() → .notMatch()
在上述代码中,.notMatch() 放置在所有 .check() 之后,导致:
- 排除规则可能作用于所有前置的
.match()规则 - 或者排除规则完全失效,取决于 Sa-Token 的内部实现
1.3 解决方案
1.3.1 核心思路
将路由规则分离配置 ,确保每个规则链的调用顺序为:match → notMatch → check
1.3.2 修改后的代码
java
@Bean
public SaServletFilter saServletFilter() {
return new SaServletFilter()
.addInclude("/**")
.addExclude("/favicon.ico")
.setAuth(obj -> {
// 管理端路由:/upms/**, /aigc/**, /app/**
SaRouter
.match("/upms/**", "/aigc/**", "/app/**")
.check(StpUtil::checkLogin);
// 客户端路由:/client/**(排除白名单)
SaRouter
.match("/client/**")
.notMatch(skipUrl) // ✅ 在 check 之前
.notMatch(authProps.getSkipUrl().toArray(new String[0]))
.check(ClientStpUtil::checkLogin); // ✅ 最后执行校验
})
.setError(this::handleError);
}
改进要点:
- 规则分离:管理端和客户端路由独立配置
- 顺序优化 :
.notMatch()位于.check()之前 - 语义清晰:每个规则链的意图一目了然
1.3.3 白名单配置
java
private final String[] skipUrl = new String[]{
"/auth/login", // 登录接口
"/auth/logout", // 登出接口
"/auth/register", // 注册接口
"/auth/info", // 用户信息(需根据业务调整)
"/client/auth/**" // 客户端认证相关接口
};
1.4 验证效果
修改后重启应用,再次测试:
bash
# 未携带 token 的请求
curl http://localhost:8100/client/getChatModels
# 返回:401 Unauthorized ✅
# 携带 token 的请求
curl -H "Authorization: Bearer your-token" http://localhost:8100/client/getChatModels
# 返回:200 OK,成功获取数据 ✅
二、Redis Key 前缀规范化问题
2.1 问题现象
项目中 Sa-Token 的 Redis 缓存 Key 格式不符合预期规范。
期望格式:
Authorization:login:token:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
实际格式:
langchat:auth:token:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
2.2 问题分析
2.2.1 Sa-Token 的缓存机制
Sa-Token 支持自定义 SaTokenDao 接口实现,用于控制 token 的存储逻辑。项目中实现了自定义的 TokenDaoRedis。
2.2.2 定位核心代码
CacheConst.java - 缓存常量定义:
java
public interface CacheConst {
/**
* 系统所有Redis缓存Key前缀
*/
String REDIS_KEY_PREFIX = "langchat:";
/**
* Auth缓存前缀
*/
String AUTH_PREFIX = REDIS_KEY_PREFIX + "auth:"; // langchat:auth:
/**
* Auth Session缓存前缀
*/
String AUTH_SESSION_PREFIX = AUTH_PREFIX + "session:"; // langchat:auth:session:
}
TokenDaoRedis.java - Redis DAO 实现:
java
@Component
public class TokenDaoRedis implements SaTokenDao {
private static final String prefix = CacheConst.AUTH_PREFIX; // ❌ 使用业务前缀
private static String sub(String key) {
int index = StrUtil.ordinalIndexOf(key, ":", 2);
return prefix + StrUtil.subSuf(key, index + 1);
}
@Override
public String get(String key) {
key = sub(key); // Key 转换
return stringRedisTemplate.opsForValue().get(key);
}
// ... 其他方法
}
2.2.3 Key 转换流程分析
Sa-Token 默认的 Key 格式为:satoken:login:token:xxx
经过 sub() 方法转换:
- 找到第 2 个冒号的位置(
satoken:login:之后) - 截取
token:xxx - 拼接前缀:
langchat:auth:token:xxx
问题所在: 前缀硬编码为 CacheConst.AUTH_PREFIX,无法满足规范化需求。
2.3 解决方案
2.3.1 修改 Key 前缀
java
// 修改前
private static final String prefix = CacheConst.AUTH_PREFIX; // langchat:auth:
// 修改后
private static final String prefix = "Authorization:"; // ✅ 符合规范
2.3.2 优化 Key 转换逻辑
修改前的 sub() 方法:
java
private static String sub(String key) {
int index = StrUtil.ordinalIndexOf(key, ":", 2); // 找第 2 个冒号
return prefix + StrUtil.subSuf(key, index + 1);
}
存在的问题:
- 依赖第三方工具类
StrUtil - 逻辑不够直观,维护成本高
优化后的 sub() 方法:
java
private static String sub(String key) {
// 将 Sa-Token 默认的 key 格式转换为规范格式
// satoken:login:token:xxx -> Authorization:login:token:xxx
int index = key.indexOf(":");
if (index > 0) {
return prefix + key.substring(index + 1);
}
return prefix + key;
}
改进要点:
- 使用 Java 原生 API,无外部依赖
- 逻辑清晰:找到第 1 个冒号,截取后续部分
- 注释说明转换规则
2.4 转换效果验证
转换示例:
| Sa-Token 原始 Key | 转换后的 Redis Key |
|---|---|
satoken:login:token:abc123 |
Authorization:login:token:abc123 ✅ |
satoken:login:session:user123 |
Authorization:login:session:user123 ✅ |
三、Bearer 前缀处理问题
3.1 问题现象
在某些场景下,Sa-Token 内部获取到的 tokenValue 包含了 "Bearer " 前缀,导致 token 验证失败。
问题代码路径:
StpLogic.getLoginIdNotHandle(tokenValue)
→ tokenValue = "Bearer eyJ0eXAiOiJKV1Qi..."
→ 验证失败
3.2 问题分析
3.2.1 HTTP 请求头规范
根据 RFC 6750,Bearer Token 的标准格式为:
http
Authorization: Bearer <token>
客户端发送请求时通常会携带 "Bearer " 前缀,但在服务端进行 token 验证时需要去除该前缀。
3.2.2 Sa-Token 的 Token 提取机制
Sa-Token 提供了 tokenPrefix 配置项,用于自动处理 token 前缀:
java
SaTokenConfig config = new SaTokenConfig();
config.setTokenPrefix("Bearer"); // 设置前缀,框架会自动去除
项目中的配置缺失:
java
@Bean
@Primary
public SaTokenConfig getTokenConfig() {
return new SaTokenConfig()
.setIsPrint(false)
.setTokenName("Authorization")
.setTimeout(24 * 60 * 60)
.setTokenStyle("uuid")
.setIsLog(false)
.setIsReadCookie(false);
// ❌ 缺少 tokenPrefix 配置
}
3.3 解决方案
3.3.1 添加 tokenPrefix 配置
java
@Bean
@Primary
public SaTokenConfig getTokenConfig() {
return new SaTokenConfig()
.setIsPrint(false)
.setTokenName("Authorization")
.setTokenPrefix("Bearer") // ✅ 新增配置
.setTimeout(24 * 60 * 60)
.setTokenStyle("uuid")
.setIsLog(false)
.setIsReadCookie(false);
}
3.3.2 工作原理
配置 tokenPrefix 后,Sa-Token 在提取 token 时会自动执行以下逻辑:
- 从请求头获取
Authorization的值:Bearer eyJ0eXAiOiJKV1Qi... - 检查是否以
Bearer开头(忽略大小写) - 去除前缀,得到纯净的 token:
eyJ0eXAiOiJKV1Qi... - 使用纯净的 token 进行后续验证
3.4 辅助工具方法
项目中已有手动处理 Bearer 前缀的工具方法(位于 AuthUtil.java 和 ClientAuthUtil.java):
java
/**
* 截取前端Token字符串中不包含`Bearer`的部分
*/
public static String getToken(String token) {
if (token != null && token.toLowerCase().startsWith("bearer")) {
return token.replace("bearer", "").trim();
}
return token;
}
建议: 配置 tokenPrefix 后,该工具方法可作为备用方案,用于特殊场景的手动处理。
四、多账号体系 Key 冲突问题
4.1 问题现象
项目使用了 Sa-Token 的多账号体系功能:
- 管理端:
StpUtil(登录类型:login) - 客户端:
ClientStpUtil(登录类型:client-login)
导致客户端的 Redis Key 格式为:
Authorization:client-login:token:xxx // ❌ 不符合预期
期望格式:
Authorization:login:token:xxx // ✅ 统一格式
4.2 问题分析
4.2.1 多账号体系原理
Sa-Token 支持在同一应用中定义多个账号体系,每个体系拥有独立的:
- Token 存储空间
- Session 会话
- 权限体系
ClientStpUtil.java 配置:
java
public class ClientStpUtil {
/**
* 多账号体系下的类型标识
*/
public static final String TYPE = "client-login"; // ❌ 导致 key 包含 client-login
/**
* 底层使用的 StpLogic 对象
*/
public static StpLogic stpLogic = new StpLogic(TYPE);
}
4.2.2 Key 生成规则
Sa-Token 生成 Key 时会使用登录类型作为中间段:
satoken:{loginType}:token:{tokenValue}
因此:
- 管理端:
satoken:login:token:xxx - 客户端:
satoken:client-login:token:xxx
4.3 解决方案
4.3.1 在 Key 转换时统一格式
修改 TokenDaoRedis.java 的 sub() 方法,在转换过程中将 client-login 替换为 login:
java
private static String sub(String key) {
// 将 Sa-Token 默认的 key 格式转换为 Authorization:login:token:xxx
// 1. satoken:login:token:xxx -> Authorization:login:token:xxx
// 2. satoken:client-login:token:xxx -> Authorization:login:token:xxx
int index = key.indexOf(":");
if (index > 0) {
String suffix = key.substring(index + 1);
// 将 client-login 替换为 login,统一使用 login
suffix = suffix.replace("client-login:", "login:"); // ✅ 关键逻辑
return prefix + suffix;
}
return prefix + key;
}
4.3.2 转换流程示例
管理端 Token:
原始 Key:satoken:login:token:abc123
↓
截取后: login:token:abc123
↓
替换后: login:token:abc123 (无变化)
↓
最终 Key:Authorization:login:token:abc123 ✅
客户端 Token:
原始 Key:satoken:client-login:token:xyz789
↓
截取后: client-login:token:xyz789
↓
替换后: login:token:xyz789 ← 关键转换
↓
最终 Key:Authorization:login:token:xyz789 ✅
4.4 注意事项
4.4.1 潜在风险
Token 冲突问题: 如果管理端和客户端生成了相同的 token 值,可能会导致覆盖。
缓解措施:
- Sa-Token 的 UUID 模式生成的 token 碰撞概率极低
- 可以考虑在 token 生成时添加业务标识
- 定期监控 Redis Key 的使用情况
4.4.2 替代方案
如果业务上需要严格区分管理端和客户端的 token,可以采用以下方案:
方案 1:保留类型标识
java
// 不进行 replace 操作,保留完整的登录类型
suffix = suffix; // client-login 保持不变
方案 2:使用不同的前缀
java
if (key.contains("client-login")) {
return "Authorization:client:" + suffix;
} else {
return "Authorization:admin:" + suffix;
}
五、完整配置总览
5.1 TokenDaoRedis.java(完整版)
java
package cn.tycoding.langchat.auth.config;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.strategy.SaStrategy;
// ... 其他导入
/**
* Sa-Token 的 Redis 持久化实现
*
* @author tycoding
* @since 2024/1/5
*/
@Slf4j
@Component
public class TokenDaoRedis implements SaTokenDao {
// ==================== 配置常量 ====================
/**
* Redis Key 前缀
* 规范:Authorization:{loginType}:{dataType}:{value}
*/
private static final String prefix = "Authorization:";
// ==================== Key 转换逻辑 ====================
/**
* 将 Sa-Token 默认的 key 格式转换为规范格式
*
* <p>转换规则:
* <ul>
* <li>satoken:login:token:xxx → Authorization:login:token:xxx</li>
* <li>satoken:client-login:token:xxx → Authorization:login:token:xxx</li>
* </ul>
*
* @param key Sa-Token 原始 key
* @return 转换后的 Redis key
*/
private static String sub(String key) {
int index = key.indexOf(":");
if (index > 0) {
String suffix = key.substring(index + 1);
// 统一登录类型标识
suffix = suffix.replace("client-login:", "login:");
return prefix + suffix;
}
return prefix + key;
}
// ==================== Redis 模板 ====================
public StringRedisTemplate stringRedisTemplate;
public RedisTemplate<String, Object> objectRedisTemplate;
public ObjectMapper objectMapper;
public boolean isInit;
// ==================== 初始化 ====================
@Autowired
public void init(RedisConnectionFactory connectionFactory) {
if (this.isInit) {
return;
}
// 配置序列化器
StringRedisSerializer keySerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
// 配置 ObjectMapper
try {
Field field = GenericJackson2JsonRedisSerializer.class.getDeclaredField("mapper");
field.setAccessible(true);
this.objectMapper = (ObjectMapper) field.get(valueSerializer);
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 配置时间类型转换
JavaTimeModule timeModule = new JavaTimeModule();
timeModule.addSerializer(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
// ... 其他时间类型配置
this.objectMapper.registerModule(timeModule);
// 自定义 Session 生成策略
SaStrategy.instance.createSession = (sessionId) -> new TokenDaoCustomized(sessionId);
} catch (Exception e) {
log.error("ObjectMapper 配置失败", e);
}
// 初始化 RedisTemplate
StringRedisTemplate stringTemplate = new StringRedisTemplate();
stringTemplate.setConnectionFactory(connectionFactory);
stringTemplate.afterPropertiesSet();
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);
template.setValueSerializer(valueSerializer);
template.setHashValueSerializer(valueSerializer);
template.afterPropertiesSet();
this.stringRedisTemplate = stringTemplate;
this.objectRedisTemplate = template;
this.isInit = true;
}
// ==================== SaTokenDao 接口实现 ====================
@Override
public String get(String key) {
key = sub(key);
return stringRedisTemplate.opsForValue().get(key);
}
@Override
public void set(String key, String value, long timeout) {
key = sub(key);
if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
if (timeout == SaTokenDao.NEVER_EXPIRE) {
stringRedisTemplate.opsForValue().set(key, value);
} else {
stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
}
@Override
public void delete(String key) {
key = sub(key);
stringRedisTemplate.delete(key);
}
// ... 其他方法实现(update, getTimeout, updateTimeout 等)
}
5.2 TokenConfiguration.java(完整版)
java
package cn.tycoding.langchat.auth.config;
import cn.dev33.satoken.config.SaTokenConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* Sa-Token 配置
*
* @author tycoding
* @since 2024/1/5
*/
@Configuration
public class TokenConfiguration {
@Bean
@Primary
public SaTokenConfig getTokenConfig() {
return new SaTokenConfig()
.setIsPrint(false) // 关闭启动时的 banner 输出
.setTokenName("Authorization") // Token 名称(请求头/参数名)
.setTokenPrefix("Bearer") // Token 前缀(自动去除)
.setTimeout(24 * 60 * 60) // Token 有效期(秒)
.setTokenStyle("uuid") // Token 风格:uuid
.setIsLog(false) // 关闭日志输出
.setIsReadCookie(false); // 不从 Cookie 读取 Token
}
}
5.3 AuthConfiguration.java(完整版)
java
package cn.tycoding.langchat.auth.config;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.tycoding.langchat.biz.utils.ClientStpUtil;
import cn.tycoding.langchat.common.properties.AuthProps;
import cn.tycoding.langchat.common.utils.R;
// ... 其他导入
/**
* Sa-Token 路由拦截配置
*
* @author tycoding
* @since 2024/1/5
*/
@Slf4j
@Configuration
@AllArgsConstructor
public class AuthConfiguration {
private final SpringContextHolder contextHolder;
private final AuthProps authProps;
/**
* 白名单配置
*/
private final String[] skipUrl = new String[]{
"/auth/login", // 登录
"/auth/logout", // 登出
"/auth/register", // 注册
"/auth/info", // 用户信息
"/client/auth/**", // 客户端认证相关
};
/**
* Sa-Token 路由拦截器
*/
@Bean
public SaServletFilter saServletFilter() {
return new SaServletFilter()
.addInclude("/**") // 拦截所有路由
.addExclude("/favicon.ico") // 排除 favicon
.setAuth(obj -> {
// 管理端路由:需要管理员登录
SaRouter
.match("/upms/**", "/aigc/**", "/app/**")
.check(StpUtil::checkLogin);
// 客户端路由:需要客户端登录(排除白名单)
SaRouter
.match("/client/**")
.notMatch(skipUrl)
.notMatch(authProps.getSkipUrl().toArray(new String[0]))
.check(ClientStpUtil::checkLogin);
})
.setError(this::handleError); // 异常处理
}
/**
* 异常处理
*/
private String handleError(Throwable e) {
// 记录未授权访问日志
if (e instanceof NotPermissionException || e instanceof NotRoleException) {
String username = getUsername();
SysLog sysLog = SysLogUtil.build(SysLogUtil.TYPE_FAIL,
HttpStatus.UNAUTHORIZED.getReasonPhrase(), null, null, username);
SpringContextHolder.publishEvent(new LogEvent(sysLog));
}
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(
RequestContextHolder.getRequestAttributes())).getRequest();
log.error("Unauthorized request: {}", URLUtil.getPath(request.getRequestURI()));
// 返回 401 响应
SaHolder.getResponse()
.setStatus(HttpStatus.UNAUTHORIZED.value())
.setHeader("Content-Type", "application/json;charset=UTF-8");
return JSON.toJSONString(R.fail(HttpStatus.UNAUTHORIZED));
}
/**
* 获取当前用户名(兼容管理端和客户端)
*/
private String getUsername() {
try {
return AuthUtil.getUsername();
} catch (Exception e) {
try {
return ClientAuthUtil.getUsername();
} catch (Exception ex) {
return null;
}
}
}
}
六、测试验证
6.1 权限拦截测试
测试用例 1:未携带 Token
bash
curl -X GET http://localhost:8100/client/getChatModels
期望结果:
json
{
"code": 401,
"message": "Unauthorized"
}
测试用例 2:携带有效 Token
bash
curl -X GET http://localhost:8100/client/getChatModels \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
期望结果:
json
{
"code": 200,
"data": [
{
"id": "1",
"name": "GPT-4",
"provider": "OpenAI"
}
]
}
6.2 Redis Key 格式验证
连接 Redis 查看 Key:
bash
redis-cli
127.0.0.1:6379> KEYS Authorization:*
期望输出:
1) "Authorization:login:token:abc123def456..."
2) "Authorization:login:session:user_1234"
3) "Authorization:login:token:xyz789ghi012..."
验证要点:
- ✅ 前缀为
Authorization: - ✅ 登录类型统一为
login: - ✅ 无
client-login字样
6.3 Bearer 前缀处理验证
测试代码:
java
@RestController
@RequestMapping("/test")
public class TokenTestController {
@GetMapping("/token-info")
public R getTokenInfo() {
String tokenValue = StpUtil.getTokenValue();
return R.ok(Map.of(
"tokenValue", tokenValue,
"hasBearer", tokenValue.toLowerCase().startsWith("bearer")
));
}
}
请求:
bash
curl -X GET http://localhost:8100/test/token-info \
-H "Authorization: Bearer abc123"
期望响应:
json
{
"code": 200,
"data": {
"tokenValue": "abc123",
"hasBearer": false
}
}
验证要点:
- ✅
tokenValue不包含 "Bearer " 前缀 - ✅
hasBearer为false
七、性能优化建议
7.1 Redis 连接池优化
yaml
spring:
data:
redis:
# 连接池配置
jedis:
pool:
max-active: 20 # 最大连接数
max-idle: 10 # 最大空闲连接
min-idle: 5 # 最小空闲连接
max-wait: 2000 # 最大等待时间(毫秒)
# 超时配置
timeout: 3000 # 连接超时(毫秒)
# 命令执行超时
command-timeout: 3000
7.2 Token 过期策略
java
@Bean
@Primary
public SaTokenConfig getTokenConfig() {
return new SaTokenConfig()
.setTimeout(24 * 60 * 60) // Token 有效期:24小时
.setActiveTimeout(30 * 60) // Token 最低活跃度:30分钟
.setIsConcurrent(true) // 允许并发登录
.setIsShare(false) // 不共享 Token
.setMaxLoginCount(3); // 最大登录设备数:3
}
说明:
activeTimeout:30 分钟无操作则冻结 TokenisConcurrent:允许同一账号多设备登录maxLoginCount:限制最大登录设备数,超出则踢出最早的
7.3 缓存预热
在应用启动时预加载常用配置到 Redis:
java
@Component
public class CacheWarmer implements ApplicationRunner {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void run(ApplicationArguments args) {
// 预热系统配置
warmupSystemConfig();
// 预热权限数据
warmupPermissions();
}
private void warmupSystemConfig() {
// 加载系统配置到 Redis
// ...
}
private void warmupPermissions() {
// 加载权限数据到 Redis
// ...
}
}
八、常见问题 FAQ
Q1:修改配置后需要清理旧的 Redis Key 吗?
A: 是的,建议清理旧数据以避免混淆。
清理方案:
bash
# 方案 1:清理特定前缀的 Key
redis-cli --scan --pattern "langchat:auth:*" | xargs redis-cli del
# 方案 2:清空整个数据库(谨慎使用)
redis-cli FLUSHDB
注意: 清理后所有用户需要重新登录。
Q2:多账号体系是否会导致 Token 冲突?
A: 理论上可能,但实际概率极低。
原因:
- Sa-Token 的 UUID 模式使用
java.util.UUID.randomUUID() - UUID 的碰撞概率约为 1/2^122
缓解措施:
- 使用 JWT 模式,在 token 中携带账号类型标识
- 定期监控 Redis Key 的数量和分布
Q3:如何区分管理端和客户端的 Token?
A: 有三种方案:
方案 1:从 Session 中获取(推荐)
java
// 判断当前登录的账号类型
if (StpUtil.isLogin()) {
// 管理端用户
} else if (ClientStpUtil.isLogin()) {
// 客户端用户
}
方案 2:在 Token 中添加标识(JWT 模式)
java
SaTokenConfig config = new SaTokenConfig()
.setTokenStyle("jwt")
.setJwtSecretKey("your-secret-key");
// 登录时设置标识
StpUtil.login(userId, new SaLoginModel()
.setExtra("userType", "admin"));
// 获取标识
String userType = StpUtil.getExtra("userType").toString();
方案 3:使用不同的 Redis 前缀(不推荐)
保留 client-login 标识,使用不同的前缀区分。
Q4:Bearer 前缀必须配置吗?
A: 不是必须的,但强烈推荐。
不配置的后果:
- 需要手动去除 "Bearer " 前缀
- 增加代码复杂度
- 容易出现遗漏
最佳实践: 在 SaTokenConfig 中统一配置 tokenPrefix。
Q5:如何实现 Token 自动续期?
A: 配置 activeTimeout 即可:
java
SaTokenConfig config = new SaTokenConfig()
.setTimeout(24 * 60 * 60) // 24小时过期
.setActiveTimeout(30 * 60); // 30分钟无操作冻结
// 每次请求自动续期
SaStrategy.instance.updateLastActiveToNow = (loginId) -> {
// Sa-Token 会自动调用此方法更新活跃时间
};
说明: 用户每次请求都会刷新 activeTimeout,保持活跃状态。