问题背景:
项目前后端分离,后端sa-token鉴权,会话存储使用 sa-token-redis-jackson,所有登录会话写入 Redis。
配置片段:
sa-token:
token-name: Authorization
token-prefix: Bearer
is-read-header: true
前端登录后,后续请求统一携带Authorization: Bearer <token>
问题场景:
前端调用登录接口成功,返回信息:
{
"flag": 1,
"desc": null,
"data": {
"id": 1,
"username": "admin",
"token": "245652rgfze13e-cdf862-4rtwr55b-91fgfb0-755aac3dfgdga362d",
"tokenType": "Bearer"
}
}
但后续调用其他接口提示401。
浏览器 Network 显示请求头已正确携带token:
GET /admin/api_v1/user/info HTTP/1.1
Authorization: Bearer 245652rgfze13e-cdf862-4rtwr55b-91fgfb0-755aac3dfgdga362d
后端日志(SaTokenExceptionHandler)抛 NotLoginException → HTTP 401。
Redis 中数据完全正常:
satoken:admin:token:245652rgfze13e-cdf862-4rtwr55b-91fgfb0-755aac3dfgdga362d = 1
satoken:admin:session:1 = <SaSession, loginId=1>
token→loginId 映射存在、账号会话存在,token 也正确回传,却仍然 401。
问题排查及原因:
401 来源 :SaTokenExceptionHandler 捕获 NotLoginException校验环节读不到会话。
Redis 里的 token key 是纯 uuid,请求头是 Bearer 245652...,数据都在却查不到,怀疑是校验时 token-prefix没生效,未剥离前缀。
经排查:问题出在SaTokenConfiguration初始化配置的时机 与取值来源。
代码片段:
@PostConstruct
public void registerStpLogic() {
SaTokenConfig global = SaManager.getConfig(); // ←
SaTokenConfig adminConfig = copyConfig(global);
adminConfig.setMaxLoginCount(1);
adminConfig.setIsConcurrent(false);
StpAdminUtil.getStpLogic().setConfig(adminConfig);
...
}
SaManager.getConfig()的行为:
public static SaTokenConfig getConfig() {
if (config == null) {
config = new SaTokenConfig();
}
return config;
}
new SaTokenConfig()的默认tokenPrefix为null(Sa-Token 默认不带前缀)。
yml 中的token-prefix: Bearer 由 Sa-Token 自动配置在某一时刻调用 SaManager.setConfig(...)才写入。
引起时序静态问题:
若 本类 @PostConstruct 先执行
→ SaManager.getConfig() 时全局 config 尚未被 setConfig
→ 返回 tokenPrefix=null 的默认对象
→ copyConfig 把 null 前缀拷进 admin/member 配置
之后 Sa-Token 自动配置才 setConfig(带 Bearer 的全局配置)
→ 但 的 StpLogic 已用了 null 前缀副本,不再更新
导致的结果:
登陆生成token、写 Redis→ 前端拿到正确 token ✓
校验配置tokenPrefix=null → 不剥前缀 → 拿整串查 Redis → 查不到 → 401 ✗
问题修复:
将配置来源从「静态、时序不确定」的SaManager.getconfig(),改为「Spring 保证已从 yml 绑定完成」的SaTokenConfig bean 构造注入。
private final RequestContextInterceptor requestContextInterceptor;
private final SaTokenConfig saTokenConfig;
public SaTokenConfiguration(RequestContextInterceptor requestContextInterceptor,
SaTokenConfig saTokenConfig) {
this.requestContextInterceptor = requestContextInterceptor;
this.saTokenConfig = saTokenConfig;
}
@PostConstruct
public void registerStpLogic() {
// 使用 Spring 已绑定 yml 的 SaTokenConfig,确保前缀处理已生效
SaTokenConfig global = saTokenConfig;
SaTokenConfig adminConfig = copyConfig(global);
adminConfig.setMaxLoginCount(1);
adminConfig.setIsConcurrent(false);
StpAdminUtil.getStpLogic().setConfig(adminConfig);
SaManager.putStpLogic(StpAdminUtil.getStpLogic());
SaManager.putStpLogic(StpMemberUtil.getStpLogic());
}
修改逻辑是Spring 依赖注入提供顺序保证:
要构造 SaTokenConfiguration,Spring必须先创建并绑定完成它依赖的SaTokenConfig bean才能注入。
而SaTokenConfig由 Sa-Token用@ConfigurationProperties("sa-token")从yml 绑定,bean创建完成 = yml绑定完成。
本质是把顺序不保证的静态取值换成Spring 保证顺序的依赖注入,消除时序竞态。
如上修改后,问题修复。