Sa-Token 踢人下线详解:强制注销、踢人下线与顶人下线的原理与实践
【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架,让鉴权变得简单、优雅!—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token
Sa-Token 是开源、免费的 Java 权限认证框架,其"踢人下线"能力用于后台治理会话:管理员或业务代码可以按账号、按设备端、按 Token 值,将指定用户的会话强制注销或踢下线。本文以 官方文档 为骨架,结合 StpLogic 核心实现 与 Demo 示例,讲清楚三种下线方式的 API 用法、场景值差异与底层调用链,读完你可以直接在业务代码中实现"后台踢人""按端注销""新登录顶掉旧设备"等常见会话治理功能。
一、什么是"踢人下线"
所谓踢人下线,核心操作就是找到指定loginId对应的Token,并设置其失效。
在 Sa-Token 的数据模型中,框架会在持久层(SaTokenDao)维护一张token-value -> loginId的映射表,会话是否有效,取决于这张映射表里的数据。因此,无论按账号踢、按设备端踢还是按 Token 踢,最终落点都是清理或篡改这条映射记录:
- 清理映射:删除
token-value -> loginId记录,对应"强制注销"; - 篡改映射:把
token-value -> loginId更新为一个特殊标记值,对应"踢人下线 / 顶人下线"。
映射的写入、更新、删除分别由 saveTokenToIdMapping / updateTokenToIdMapping / deleteTokenToIdMapping 完成,它们最终都委托给SaTokenDao(默认内存实现,可切换 Redis 等分布式存储),这就是三种下线方式共用的一套底层机制。
二、强制注销(logout)
强制注销的语义是:等价于对方主动调用了注销方法,让指定账号的会话立即失效。
2.1 三个核心 API
StpUtil.logout(10001); // 强制指定账号注销下线 StpUtil.logout(10001, "PC"); // 强制指定账号指定端注销下线 StpUtil.logoutByTokenValue("token"); // 强制指定 Token 注销下线三个方法的含义:
| API | 作用 | 定位粒度 |
|---|---|---|
StpUtil.logout(loginId) | 强制指定账号注销下线 | 账号(所有设备端) |
StpUtil.logout(loginId, deviceType) | 强制指定账号指定端注销下线 | 账号 + 设备端 |
StpUtil.logoutByTokenValue(tokenValue) | 强制指定 Token 注销下线 | 单条 Token |
其中deviceType参数(如"PC"、"APP"、"H5")是登录时通过StpUtil.login(loginId, deviceType)指定的设备类型标记。从 StpLogic.logout(loginId, deviceType) 的源码注释可以看出:deviceType填null代表注销该账号的所有设备类型。
2.2 注销后的行为
强制注销会彻底清除 Token 信息(包括 Token 映射、Token-Session 等),对方再次访问系统时,会抛出NotLoginException异常,提示语为token 无效。
三、踢人下线(kickout)
踢人下线是后台管理最常用的操作:管理员可以随时把某个可疑账号或某台设备踢下线,而不需要破坏它已经产生的数据。
3.1 三个核心 API
StpUtil.kickout(10001); // 将指定账号踢下线 StpUtil.kickout(10001, "PC"); // 将指定账号指定端踢下线 StpUtil.kickoutByTokenValue("token"); // 将指定 Token 踢下线用法与logout系列完全对应:
| API | 作用 | 定位粒度 |
|---|---|---|
StpUtil.kickout(loginId) | 将指定账号踢下线 | 账号(所有设备端) |
StpUtil.kickout(loginId, deviceType) | 将指定账号指定端踢下线 | 账号 + 设备端 |
StpUtil.kickoutByTokenValue(tokenValue) | 将指定 Token 踢下线 | 单条 Token |
同样,deviceType填null代表踢出该账号的所有设备类型(见 StpLogic.kickout(loginId, deviceType))。
3.2 强制注销 vs 踢人下线的区别
这是本主题最核心的辨析点:
- 强制注销等价于对方主动调用了注销方法,Token 映射被彻底删除,对方再次访问会提示:Token 无效;
- 踢人下线不会清除 Token 信息,而是将其打上特定标记,对方再次访问会提示:Token 已被踢下线。
"打上特定标记"在源码中体现得很直观。在 StpLogic._fireLogoutEvent 中:
// SaLogoutMode.LOGOUT:注销下线 —— 真正删除映射 deleteTokenToIdMapping(tokenValue); SaTokenEventCenter.doLogout(loginType, loginId, tokenValue); // SaLogoutMode.KICKOUT:踢人下线 —— 把映射更新为特殊标记 updateTokenToIdMapping(tokenValue, NotLoginException.KICK_OUT); SaTokenEventCenter.doKickout(loginType, loginId, tokenValue);其中NotLoginException.KICK_OUT的值是字符串"-5",定义于 NotLoginException:
/** 表示 token 已被踢下线 */ public static final String KICK_OUT = "-5"; public static final String KICK_OUT_MESSAGE = "token 已被踢下线";也就是说,踢人下线后,DAO 中原本token -> 10001的映射变成了token -> "-5"。下次该 Token 再访问时,框架取出的 loginId 是-5,被判定为"被踢下线"并抛出NotLoginException(场景值type = -5)。由于 Token 记录本身还保留在存储中,其 Token-Session 等关联数据也更便于后续排查或恢复。
四、顶人下线(replaced)
"顶人下线"操作发生在框架登录时顶退旧登录设备,属于框架内部操作,一般情形下你不会调用到此 API:
StpUtil.replaced(10001); // 将指定账号顶下线 StpUtil.replaced(10001, "PC"); // 将指定账号指定端顶下线 StpUtil.replacedByTokenValue("token"); // 将指定 Token 顶下线它与踢人下线的差异点:
- 触发场景不同:
replaced通常在"同账号新设备登录、旧设备被顶退"的互斥登录场景中被框架内部调用(配合isConcurrent、maxLoginCount等配置),业务代码一般用不到; - 场景值不同:顶人下线写入的标记是
NotLoginException.BE_REPLACED = "-4"(源码定义),对方再次访问会提示token 已被顶下线; - 保留 Account-Session:从 StpLogic._logout 的注释可以看到,调用顶替下线时通常新客户端正在登录,因此不会注销该账号的 Account-Session,避免"注销后又立刻创建"造成不必要的性能浪费;而注销/踢人下线后若账号已无任何在线终端,会通过
session.logoutByTerminalCountToZero()将 Account-Session 一并注销。
五、三种下线方式对比速查表
| 对比项 | 强制注销 logout | 踢人下线 kickout | 顶人下线 replaced |
|---|---|---|---|
| 典型使用方 | 用户主动退出 / 后台强退 | 后台管理踢出可疑会话 | 框架登录时顶退旧设备 |
| 对 Token 映射的处理 | 删除映射 | 更新为-5标记 | 更新为-4标记 |
| 对方再次访问的提示 | token 无效 | token 已被踢下线 | token 已被顶下线 |
| NotLoginException 场景值 | -2(INVALID_TOKEN) | -5(KICK_OUT) | -4(BE_REPLACED) |
| 是否保留 Account-Session | 终端清零时注销 | 终端清零时注销 | 保留(新客户端在登录) |
| 框架事件回调 | doLogout / doBeforeLogout | doKickout / doBeforeKickout | doReplaced / doBeforeReplaced |
场景值常量统一收敛在 NotLoginException.ABNORMAL_LIST(
-1 ~ -7),业务代码可用e.getType()精确判断会话失效原因,进而区分提示语或跳转逻辑。
六、底层原理:一条 Token 下线要经历什么
三种方式最终都汇入StpLogic._logoutByTokenValue(tokenValue, logoutParameter)(源码),logoutParameter中的mode(SaLogoutMode.LOGOUT / KICKOUT / REPLACED,见 SaLogoutMode 枚举)决定了后续走"删除"还是"打标记"。完整调用链如下:
- 前置校验:根据 token 解析 loginId(
getLoginIdByTokenNotThinkFreeze),若 loginId 为空则直接返回,避免写入意外数据;同时检查isFreeze冻结状态; - 发布"注销前"事件:
_fireBeforeLogoutEvent按 mode 触发doBeforeLogout/doBeforeKickout/doBeforeReplaced(源码),可用于在会话失效前做审计、通知等; - 清理最后活跃时间:开启活跃度校验(
isOpenCheckActiveTimeout)时调用clearLastActive; - 清理 Token-Session:未开启
isKeepTokenSession时调用deleteTokenSession; - 处理 Token 映射并发布事件:
_fireLogoutEvent中按 mode 删除或篡改映射,并触发doLogout/doKickout/doReplaced(源码); - 清理终端信息:从 Account-Session 移除该 Token 对应的
SaTerminalInfo,若终端数为 0 则尝试注销 Account-Session。
而按loginId下线的_logout(loginId, logoutParameter)(源码)则先取出该账号的 Account-Session,遍历其终端列表,按deviceType、deviceId过滤后,对每个命中的终端逐一执行_removeTerminal,最终走一遍上述"清理 → 改映射 → 发事件"的流程。这就是StpUtil.kickout(10001, "PC")只影响 PC 端、不影响 APP 端的原理所在。
另外,框架的门面类 StpUtil 只是把上述方法逐一定向委托给内部stpLogic(StpUtil.logout(...)转发到stpLogic.logout(...)等),真实的业务逻辑全部封装在StpLogic,这也意味着多账号体系下你可以为不同loginType创建独立的StpLogic实例,实现分账号类型的精准踢人。
七、实战示例:后台踢人接口
官方在 KickoutController.java 中提供了可直接运行的演示代码(所在模块 sa-token-demo-case,启动后访问http://localhost:8081):
@RestController @RequestMapping("/kickout/") public class KickoutController { // 将指定账号强制注销 ---- http://localhost:8081/kickout/logout?userId=10001 @RequestMapping("logout") public SaResult logout(long userId) { // 强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效。 StpUtil.logout(userId); return SaResult.ok(); } // 将指定账号踢下线 ---- http://localhost:8081/kickout/kickout?userId=10001 @RequestMapping("kickout") public SaResult kickout(long userId) { // 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线。 StpUtil.kickout(userId); return SaResult.ok(); } // 根据 Token 值踢人 ---- http://localhost:8081/kickout/kickoutByTokenValue?tokenValue=已登录账号的token值 @RequestMapping("kickoutByTokenValue") public SaResult kickoutByTokenValue(String tokenValue) { StpUtil.kickoutByTokenValue(tokenValue); return SaResult.ok(); } }操作步骤:
- 先调用登录接口登录一个账号:
http://localhost:8081/acc/doLogin?name=zhang&pwd=123456; - 分别访问上面的
logout/kickout接口,再访问登录校验接口http://localhost:8081/acc/checkLogin,对比两次返回的提示信息差异——强制注销后报"Token 无效",踢人下线后报"Token 已被踢下线",直观验证两种方式的区别; - 若想按 Token 精确踢人,可从登录响应中拿到 token 值,再调用
kickoutByTokenValue。
该行为同样有单元测试兜底:在 StpLogicLogoutTest 中,kickoutByTokenValue_marksTokenAsKicked验证了"kickoutByTokenValue 后再次 checkLogin 应抛出 NotLoginException",logoutByLoginId_clearsAccountSession验证了按 loginId 注销会清空 Token 映射与 Account-Session,可作为理解框架行为的参考依据。
八、进阶:通过注销参数精细控制
除上述基础 API 外,所有下线方法都支持传入SaLogoutParameter做精细控制(对应StpUtil.logout(loginId, SaLogoutParameter)、StpUtil.kickout(loginId, SaLogoutParameter)等重载)。常用参数项(定义见 SaLogoutParameter):
| 参数 | 说明 |
|---|---|
setDeviceType(String) | 仅注销/踢出指定设备端,null 代表全部 |
setDeviceId(String) | 仅注销/踢出指定设备 ID(更细粒度的设备定位) |
setRange(SaLogoutRange) | 注销范围:TOKEN仅处理当前 Token,ACCOUNT级联处理账号下全部终端 |
setMode(SaLogoutMode) | 注销模式:LOGOUT/KICKOUT/REPLACED |
setIsKeepTokenSession(boolean) | 是否保留 Token-Session 数据 |
setIsKeepFreezeOps(boolean) | 遇到冻结 Token 时是否跳过处理 |
例如只踢掉某账号在APP端且指定deviceId的会话,可组合deviceType与deviceId两个条件,实现比"按端踢人"更精准的单设备下线。
九、关联文档与进一步阅读
- 本文主题文档:踢人下线(kick.md)
- 登录认证与会话体系:login-auth.md、session.md
- 会话治理扩展:search-session.md(后台查询/管理会话)、mutex-login.md(互斥登录、单端登录)
- 全局事件监听(doKickout / doReplaced 等回调的落地方式):global-listener.md
综上所述,Sa-Token 的"踢人下线"能力虽然 API 只有几行,背后却是一套"账号 → 终端 → Token 映射"的完整会话治理模型。理解deleteTokenToIdMapping与updateTokenToIdMapping的分野,就能准确判断"注销 / 踢下线 / 顶下线"三种行为对既有会话数据的不同影响,进而在后台管理中做到按需、按端、按 Token 的精细化下线控制。
【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架,让鉴权变得简单、优雅!—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考