1. 多端登录的核心挑战与解决方案现代应用开发中多终端同时登录已成为标配需求。想象这样一个场景用户在办公室用电脑处理工作通勤路上用手机查看数据回家后用平板电脑继续操作——三个设备需要保持独立的会话状态同时又要共享相同的账户身份。这种场景下传统的单会话管理机制会直接崩溃。Sa-Token通过loginType概念完美解决了这个问题。每个设备类型可以声明自己的登录类型比如// PC端登录 StpUtil.login(10001, pc); // 移动端登录 StpUtil.login(10001, mobile);这相当于给同一个用户分配了不同的身份通道。实际项目中我常用这些类型划分admin: 管理后台会话app: 移动端应用会话wechat: 微信小程序会话web: 网页端会话每个通道完全独立维护自己的Token有效期、会话数据。通过StpUtil.getLoginIdByToken(tokenValue, mobile)可以精准获取指定端的登录状态。这种设计带来两个显著优势隔离性某终端修改密码不会导致其他端被迫下线可控性可以单独设置某类终端的会话超时时间2. 会话管理的高级玩法2.1 分布式会话存储实战在Ruoyi-vue-plus项目中我推荐采用Redis作为会话存储介质。配置方式极其简单sa-token: # 启用Redis集成 is-share: true # Redis数据库索引 db: 0 # Redis主机 host: 127.0.0.1 # Redis端口 port: 6379实际使用时会遇到几个典型问题会话碰撞多个请求同时修改会话数据时采用session.update()方法保证原子性大对象存储超过1MB的数据建议先压缩我常用Snappy算法处理跨服务共享通过token-sign配置相同的密钥串实现集群间会话互通2.2 会话监听器的妙用通过实现SaTokenListener接口我们可以监控会话生命周期的关键事件Component public class MySaTokenListener implements SaTokenListener { // 每次登录时触发 Override public void doLogin(String loginType, Object loginId, String tokenValue) { log.info(设备[{}]登录成功,ID:{},Token:{}, loginType, loginId, tokenValue); } // 每次注销时触发 Override public void doLogout(String loginType, Object loginId, String tokenValue) { log.info(设备[{}]注销,ID:{}, loginType, loginId); // 清理相关缓存数据 RedisUtil.del(user: loginId : loginType); } }这个特性在以下场景特别有用记录用户登录日志实时统计在线人数资源释放的兜底保障3. 并发登录控制策略3.1 互斥登录实现金融类系统通常要求同一账户只能在一个设备登录。配置方式sa-token: # 是否允许并发登录 is-concurrent: false # 新登录是否踢掉旧登录 is-share: false代码层面需要额外处理踢出提示PostMapping(/login) public R login(RequestBody LoginDto dto) { // 如果已有登录会话 if(StpUtil.isLogin()) { // 强制注销现有会话 StpUtil.logout(); // 也可以选择返回提示信息 // return R.fail(您的账号已在其他设备登录); } // 正常登录流程... }3.2 登录人数限制限制单个账户的最大在线设备数// 在登录时校验 if(StpUtil.getLoginCount(mobile) 3) { throw new ApiException(移动端最多允许3个设备同时在线); }更精细的控制可以通过自定义策略实现public class LoginLimitStrategy { // 按设备类型限制 private static final MapString, Integer LIMITS ImmutableMap.of( pc, 1, mobile, 3, web, 2 ); public static void check(Object userId, String loginType) { int max LIMITS.getOrDefault(loginType, 1); if(StpUtil.getLoginCountByLoginType(userId, loginType) max) { throw new ApiException(loginType 端最多允许 max 个设备); } } }4. 实战Ruoyi-vue-plus中的增强改造4.1 多端登录适配在原有登录接口基础上改造PostMapping(/login/{clientType}) public R login( PathVariable String clientType, RequestBody LoginDto dto) { // 验证客户端类型 if(!Arrays.asList(admin,app,web).contains(clientType)) { return R.fail(不支持的客户端类型); } // 验证账号密码... SysUser user userService.selectUserByUserName(dto.getUsername()); // 执行登录 StpUtil.login(user.getUserId(), clientType); // 返回客户端特定数据 LoginVo vo new LoginVo(); if(admin.equals(clientType)) { vo.setMenus(menuService.getAdminMenus()); } else if(app.equals(clientType)) { vo.setAppConfig(appConfigService.getCurrent()); } return R.ok(vo); }4.2 会话管理面板为后台管理系统增加会话监控功能template div el-table :datasessions el-table-column proploginType label终端类型/ el-table-column proploginTime label登录时间/ el-table-column propip labelIP地址/ el-table-column label操作 template #defaultscope el-button clickforceLogout(scope.row.tokenValue) 强制下线 /el-button /template /el-table-column /el-table /div /template script export default { data() { return { sessions: [] } }, methods: { loadSessions() { this.$http.get(/admin/sessions).then(res { this.sessions res.data }) }, forceLogout(token) { this.$http.post(/admin/force-logout, {token}).then(() { this.loadSessions() }) } }, mounted() { this.loadSessions() } } /script后端接口实现示例GetMapping(/admin/sessions) public RListSessionVo getSessions() { ListSessionVo list new ArrayList(); // 获取所有登录会话 ListString tokens StpUtil.searchTokenValue(, 0, -1); for(String token : tokens) { SessionVo vo new SessionVo(); // 解析token信息 SaTokenInfo info StpUtil.getTokenInfoByToken(token); vo.setTokenValue(token); vo.setLoginType(info.getLoginType()); vo.setLoginTime(info.getLoginTime()); // 获取会话扩展信息 SaSession session StpUtil.getSessionByToken(token); vo.setIp(session.get(loginIp)); vo.setDevice(session.get(userAgent)); list.add(vo); } return R.ok(list); } PostMapping(/admin/force-logout) public R forceLogout(RequestBody MapString,String param) { StpUtil.kickoutByTokenValue(param.get(token)); return R.ok(); }