必须用“用户id+设备类型”为维度存redis key,否则pc和app会互相误踢;因同一账号需支持多端并存,仅用userid会导致不同终端共享会话而强制互踢,违背业务刚需。

必须用“用户ID + 设备类型”为维度存Redis Key,否则PC和APP会互相误踢。
为什么不能只用 userId 作为Redis Key
很多团队一开始图省事,直接用 redisTemplate.opsForValue().set("login:" + userId, token),结果上线后发现:员工在手机APP登录后,PC端立刻掉线;或者反过来,PC登录后APP被强制下线。这不是bug,是设计缺陷——它把所有终端当成同一类设备了。
真实业务中,“同一账号多端并存”是刚需:销售用PC查CRM,同时用手机APP回客户消息。互踢只应发生在同类型设备之间。
- 全局限一(不推荐):一个账号全局只能一个会话 → 用
"login:all:" + userId - 分端限一(推荐):PC、APP、H5各自独立计数 → 用
"login:pc:" + userId、"login:app:" + userId - 同端限一(最严):连Chrome多个标签页都互踢 → 需结合浏览器指纹,Key里加
uaHash或fingerprint
redisTemplate.opsForValue().set() 要带 TTL,且必须与 Token 过期时间严格对齐
Token 有效期设 2 小时,但 Redis Key 没设过期,或设成 3 小时,就会导致校验失效:旧 Token 已过期,但 Redis 里还留着,新登录无法覆盖,用户卡在“被踢却登不进”的死循环里。
正确写法:
String redisKey = "sso:login:" + clientType + ":" + userId;
redisTemplate.opsForValue().set(
redisKey,
token,
Duration.ofMinutes(120) // 必须和 JWT 的 exp 字段完全一致
);
注意:sso: 前缀不是可选的——没前缀容易污染其他业务的 Redis Key 空间,尤其当团队共用一套 Redis 实例时。
拦截器里校验 token 时,必须用 .equals() 而非 ==,且要判空
常见错误是直接写 if (tokenFromRequest == validToken),Java 字符串比较用 == 只比引用,Redis 返回的字符串是新对象,永远不等,结果所有请求都被判“已踢出”。
还要防 Redis 返回 null(比如 Key 过期被自动清理,或网络抖动没读到):
- 如果
validToken为null,说明 Redis 里没记录,可能是首次登录、也可能是 Token 已被删但客户端还在用旧 Token → 应返回 401 - 如果
!tokenFromRequest.equals(validToken),才真正触发踢人逻辑
分布式部署下,sessionRepository 注入必须加 @Lazy
用 @EnableRedisHttpSession 时,Spring Session 会自动注册 FindByIndexNameSessionRepository,但如果你在配置类里直接 @Autowired private FindByIndexNameSessionRepository sessionRepository;,启动会报循环依赖:Circular reference involving containing bean 'RedisHttpSessionConfiguration'。
这不是配置错,是 Spring Boot 初始化顺序问题。唯一稳妥解法是加 @Lazy:
@Autowired @Lazy private FindByIndexNameSessionRepository extends Session> sessionRepository;
这个坑在单机开发环境几乎不暴露,一上 K8s 多副本就必现——因为每个 Pod 启动节奏不同,依赖链更容易卡死。
最易被忽略的一点:前端每次请求必须把 clientType 传过来,不能靠 User-Agent 猜。浏览器 UA 可伪造、可被 CDN 改写、移动端 WebView UA 还可能统一成“Mozilla/5.0”,一旦猜错,login:pc:123 和 login:app:123 就混成一个 Key,互踢逻辑彻底失效。











