Sa-Token实现多端登录控制:单地、多地与同端互斥策略
📅 2026/7/18 7:25:52
👁️ 阅读次数
📝 编程学习
1. 项目概述
在当今多终端并行的互联网环境中,账号登录策略的选择直接影响着用户体验和系统安全。作为Java开发者,我们经常需要实现类似腾讯QQ的登录机制:允许PC和手机同时在线,但禁止同一账号在两个手机上并行登录。这种"同端互斥登录"的需求在金融、社交、办公等场景中尤为常见。
Sa-Token作为轻量级Java权限认证框架,提供了开箱即用的多端登录管理方案。本文将深入解析三种典型登录模式的实现:
- 单地登录:传统会话管理,新登录强制旧会话失效
- 多地登录:允许同一账号多终端并行访问
- 同端互斥登录:按设备类型区分会话有效性
2. 核心概念解析
2.1 会话管理基础原理
现代Web应用的会话管理通常基于Token机制。当用户首次认证成功后,服务端会生成唯一Token返回客户端,后续请求通过携带该Token维持会话状态。Sa-Token在传统Token机制上扩展了多维度控制能力:
// 基础登录示例 StpUtil.login(10001); // 生成并返回Token String tokenValue = StpUtil.getTokenValue(); // 获取当前Token值2.2 设备类型标识策略
实现同端互斥的关键在于设备类型识别。Sa-Token支持通过第二参数指定设备类型:
// 指定设备类型登录 StpUtil.login(10001, "Mobile"); // 手机端 StpUtil.login(10001, "PC"); // 电脑端 StpUtil.login(10001, "Tablet"); // 平板端提示:设备类型字符串可自定义,建议采用全大写或全小写保持一致性
3. 多地登录实现
3.1 基础配置
多地登录是Sa-Token的默认模式,无需特殊配置:
# application.yml sa-token: is-concurrent: true # 默认值,允许并发登录 is-share: true # 默认值,Token共享模式3.2 登录与注销
// 登录控制器示例 @PostMapping("/login") public SaResult login(String username, String password) { if("admin".equals(username) && "123456".equals(password)) { StpUtil.login(10001); return SaResult.ok("登录成功").setData(StpUtil.getTokenInfo()); } return SaResult.error("认证失败"); } // 全端注销 @PostMapping("/logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok("已注销所有会话"); }3.3 会话共存原理
当is-share=true时,所有登录会话共享同一个Token,此时:
- 任一终端注销会导致全端下线
- Token更新会同步到所有终端
- 适合需要严格会话一致性的场景
4. 单地登录实现
4.1 关键配置
sa-token: is-concurrent: false # 禁止并发登录4.2 登录挤退机制
当新会话建立时,旧会话会收到"Token被顶下线"异常:
try { // 业务代码 } catch (NotLoginException e) { if(e.getType() == NotLoginException.BE_REPLACED) { // 处理被顶下线情况 return SaResult.error("您的账号已在其他地方登录"); } }4.3 适用场景分析
单地登录特别适合:
- 金融支付系统
- 敏感数据管理后台
- 需要严格单点控制的ERP系统
5. 同端互斥登录实现
5.1 完整配置方案
sa-token: is-concurrent: false # 启用互斥 is-share: false # 独立Token模式 token-prefix: "token_" # 建议设置前缀避免冲突5.2 设备级会话控制
// 指定设备类型登录 StpUtil.login(10001, "Mobile"); // 查询当前设备类型 String device = StpUtil.getLoginDevice(); // 精准注销特定设备 StpUtil.logout(10001, "PC");5.3 会话状态检测
// 获取所有登录设备 List<String> devices = StpUtil.getTokenSession().getDataMap() .keySet().stream() .map(key -> key.replace("token_", "")) .collect(Collectors.toList());6. 高级功能实现
6.1 动态策略切换
通过编程方式实时修改登录策略:
// 动态改为单地登录 SaManager.getConfig().setIsConcurrent(false); // 动态改为多地登录 SaManager.getConfig().setIsConcurrent(true);6.2 自定义设备类型
扩展支持更细粒度的设备识别:
// 根据UA识别设备 String userAgent = request.getHeader("User-Agent"); String deviceType = parseDeviceType(userAgent); StpUtil.login(userId, deviceType);6.3 登录事件监听
@EventListener public void onLogin(StpLoginEvent event) { String loginId = (String) event.getLoginId(); String device = event.getDevice(); log.info("用户{}在{}设备登录", loginId, device); }7. 性能优化建议
7.1 Token存储策略
sa-token: token-prefix: "satoken:" # Redis键前缀 timeout: 86400 # 默认有效期(秒) activity-timeout: -1 # 无操作续期时间7.2 会话数据压缩
对于大型用户系统:
// 自定义Token生成策略 @Bean public SaTokenTemplate saTokenTemplate() { return new SaTokenTemplate() { @Override public String createToken(Object loginId, String device) { // 实现压缩算法 return compressToken(super.createToken(loginId, device)); } }; }8. 安全防护措施
8.1 异常登录检测
// 记录登录设备指纹 String fingerprint = RequestUtil.getHeader("X-Device-Fingerprint"); SaSession session = StpUtil.getSession(); session.set("last_login_device", fingerprint); // 校验设备变更 if(!fingerprint.equals(session.get("last_login_device"))) { // 触发二次认证 }8.2 并发登录限制
// 限制最大登录设备数 long deviceCount = StpUtil.getTokenSession().getDataMap().size(); if(deviceCount >= MAX_DEVICES) { throw new RuntimeException("已达到最大登录设备限制"); }9. 实战问题排查
9.1 常见异常处理
| 异常场景 | 错误码 | 解决方案 |
|---|---|---|
| Token被顶下线 | -4 | 提示用户会话冲突 |
| Token被踢下线 | -5 | 记录安全日志 |
| 无效Token | -2 | 引导重新登录 |
| Token过期 | -3 | 自动续期或重新认证 |
9.2 日志监控建议
// 自定义日志拦截器 @Interceptor public class SaTokenLogInterceptor { @AroundInvoke public Object logTokenOps(InvocationContext ctx) { // 记录Token操作日志 return ctx.proceed(); } }10. 扩展应用场景
10.1 多租户系统适配
// 租户隔离的登录实现 StpUtil.login(10001, "Mobile", "TENANT_A");10.2 微服务架构集成
# 网关层配置 sa-token: is-read-cookie: false is-read-header: true token-name: "X-Auth-Token"在实际项目中,我们通过Sa-Token实现了办公系统的多端访问控制:允许员工在手机和电脑同时登录OA系统,但禁止同一账号在两台电脑上并行登录。这种策略既保证了使用便利性,又避免了账号共享风险。关键实现点在于登录时准确识别设备类型:
// 实际业务中的设备识别 private String resolveDeviceType(HttpServletRequest request) { String ua = request.getHeader("User-Agent").toLowerCase(); if(ua.contains("mobile")) { return "MOBILE"; } else if(ua.contains("pad")) { return "TABLET"; } else { return "PC"; } }
编程学习
技术分享
实战经验