
我第一次接这块时,也以为就是一个 StpUtil.login() 的事。后来把 Sa-Token 真放进项目里,我才发现,真正决定系统稳不稳的,不是你能不能让用户登录,而是你有没有把登录策略设计清楚。
这篇文章,我就把 Sa-Token 里的登录参数、注销参数、终端控制拆开讲。不是照着文档翻译,而是站在真实项目的角度,看看这些参数到底解决什么问题,什么时候该用,什么时候别乱用。
说实话,如果只是写 Demo,StpUtil.login(loginId) 真的够用了。
但到了真实项目里,产品和业务很快就会把问题变复杂:
你会发现,登录从来都不是一个动作。
它更像是一整套策略:谁在登录、从哪登录、能活多久、跟谁互斥、退出时影响谁。
而 Sa-Token 的这套参数,恰恰就是把这些策略显式交给你。
先看最基础的两个例子:
// 指定`账号id`和`设备类型`进行登录
StpUtil.login(10001, "PC");
// 设置登录账号 id 为 10001,并指定是否为 “记住我” 模式
StpUtil.login(10001, false);很多人第一次看到这里,会觉得第二个参数只是个“补充信息”。
但我后来越看越觉得,它其实已经在表达策略了。
"PC",你是在声明这次登录属于哪个终端。false,你是在声明这次登录是不是持久化会话。如果你需要更细一点的控制,Sa-Token 直接给了一个完整的参数对象:
StpUtil.login(10001, new SaLoginParameter()
.setDeviceType("PC") // 此次登录的客户端设备类型, 一般用于完成 [同端互斥登录] 功能
.setDeviceId("xxxxxxxxx") // 此次登录的客户端设备ID, 登录成功后该设备将标记为可信任设备
.setIsLastingCookie(true) // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
.setTimeout(60 * 60 * 24 * 7) // 指定此次登录 token 的有效期, 单位:秒,-1=永久有效
.setActiveTimeout(60 * 60 * 24 * 7) // 指定此次登录 token 的最低活跃频率, 单位:秒,-1=不进行活跃检查
.setIsConcurrent(true) // 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
.setIsShare(false) // 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token)
.setMaxLoginCount(12) // 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
.setMaxTryTimes(12) // 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
.setExtra("key", "value") // 记录在 Token 上的扩展参数(只在 jwt 模式下生效)
.setToken("xxxx-xxxx-xxxx-xxxx") // 预定此次登录的生成的Token
.setIsWriteHeader(false) // 是否在登录后将 Token 写入到响应头
.setTerminalExtra("key", "value")// 本次登录挂载到 SaTerminalInfo 的自定义扩展数据
.setReplacedRange(SaReplacedRange.CURR_DEVICE_TYPE) // 顶人下线的范围: CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端
.setOverflowLogoutMode(SaLogoutMode.LOGOUT) // 溢出 maxLoginCount 的客户端,将以何种方式注销下线: LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线
.setRightNowCreateTokenSession(true) // 是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建)
.setupCookieConfig(cookie->{ // 设置 Cookie 配置项
cookie.setDomain("sa-token.cc"); // 设置:作用域
cookie.setPath("/shop"); // 设置:路径 (一般只有当你在一个域名下部署多个项目时才会用到此值。)
cookie.setSecure(true); // 设置:是否只在 https 协议下有效
cookie.setHttpOnly(true); // 设置:是否禁止 js 操作 Cookie
cookie.setSameSite("Lax"); // 设置:第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制)
cookie.addExtraAttr("aa", "bb"); // 设置:额外扩展属性
}
);第一次看到这串参数,很多人会下意识头大。
我当时也是。因为它看起来像一张“参数大全”,但真正在项目里用时,你不需要一次全记住。你只要先把它拆成 3 个问题就顺了。
这类参数我会优先看:
setDeviceType("PC"):先把设备边界定义清楚,不然后面的同端互斥、跨端共存都没法谈。setDeviceId("xxxxxxxxx"):如果你们有可信设备、风控、登录审计,这个字段会非常有用。setTerminalExtra("key", "value"):适合挂一些终端级别的信息,比如渠道、客户端版本、来源标记。这里我踩过一个很真实的坑。
很多团队一开始根本没有认真定义 deviceType,随手写成 pc、PC、admin-web、web 混着来。前期还能跑,后面一做“同端互斥”或者“指定端注销”,线上行为就开始变得不可解释。
设备类型不是一个字符串细节,它是登录策略的分组键。
这组参数最容易和“记住我”混在一起:
setIsLastingCookie(true):控制的是 Cookie 持不持久,重点在“浏览器关了之后还在不在”。setTimeout(...):控制的是 Token 过不过期,重点在“服务端还认不认”。setActiveTimeout(...):更适合做活跃校验,不是简单的总时长。setupCookieConfig(...):这块一旦上生产,Secure、HttpOnly、SameSite 基本都绕不过去。很多线上问题,其实不是代码写错了,而是这两个概念从一开始就没拆开:
这两个问题看起来像一件事,实际上完全不是一件事。
所以我现在和产品沟通“记住我”时,不会只问一句“要不要支持”。
我会直接追问两件事:
你把这两个问题问清楚,后面一半的坑都能提前避掉。
这组参数特别像业务规则,不像工具参数:
setIsConcurrent(true):允不允许同账号并发登录。setIsShare(false):并发登录时,是共用一个 token,还是每次新建。setMaxLoginCount(12):允许同时保留多少个登录终端。setReplacedRange(...):顶人下线影响当前设备类型,还是所有设备类型。setOverflowLogoutMode(...):超出数量时,用注销、踢下线,还是顶下线来处理。说实话,这块如果只是本地调通接口,怎么配都像能用。
但一旦到了真实生产环境,问题马上就来了。
比如:
isShare=true 和 isShare=false,后续审计成本差很多。我现在更倾向一个判断:
因为后台系统里,真正贵的不是让人重新登录,而是出了安全问题之后你解释不清。
很多人只盯着登录,忽略注销。
但我后面越来越觉得,注销参数直接决定了你的退出动作到底影响谁。
原始用法是这样的:
// 当前客户端注销
StpUtil.logout(new SaLogoutParameter()
// 注销范围: TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话
// 此参数只在调用 StpUtil.logout() 时有效
.setRange(SaLogoutRange.TOKEN)
);
// 指定 token 注销
StpUtil.logoutByTokenValue("xxxxxxxxxxxxxxxxxxxxxxx", new SaLogoutParameter()
// 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API)(默认 false)
// 此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue("token") 时有效
.setIsKeepFreezeOps(false)
// 是否保留此 token 的 Token-Session 对象(默认 false)
.setIsKeepTokenSession(true)
);
// 指定 loginId 注销
StpUtil.logout(10001, new SaLogoutParameter()
.setDeviceType("PC") // 设置注销的设备类型 (如果不指定,则默认注销所有客户端)
.setIsKeepTokenSession(true) // 是否保留对应 token 的 Token-Session 对象(默认 false)
.setMode(SaLogoutMode.REPLACED) // 设置注销模式:LOGOUT=注销登录、KICKOUT=踢人下线,REPLACED=顶人下线(默认LOGOUT)
);这块我自己的理解很简单,先别把它当成 API 细节,而是先按场景看:
setRange(SaLogoutRange.TOKEN)。logoutByTokenValue(...)。setDeviceType("PC")。setMode(...)。这里最容易被忽略的一点是,“退出登录”不一定都是同一种退出。
有的是用户主动退。
有的是管理员踢。
有的是新会话把旧会话顶掉。
如果前后端不把这个语义对齐,最后用户看到的就只是一句很模糊的“未登录”,排障会特别痛苦。
真实项目做到后面,经常会出现一种情况:
参数都有,但还是不够。
比如你想做更细的终端策略:
这时候别硬往配置里塞,Sa-Token 已经给了一个更稳的口子:
// 测试
@RequestMapping("logout")
public SaResult logout() {
// 遍历账号 10001 已登录终端列表,进行详细操作
StpUtil.forEachTerminalList(10001, (session, ter) -> {
// 根据登录顺序,奇数的保留,偶数的下线
if(ter.getIndex() % 2 == 0) {
StpUtil.removeTerminalByLogout(session, ter); // 注销下线方式 移除这个登录客户端
// StpUtil.removeTerminalByKickout(session, ter); // 踢人下线方式 移除这个登录客户端
// StpUtil.removeTerminalByReplaced(session, ter); // 顶人下线方式 移除这个登录客户端
}z
});
return SaResult.ok();
}我很喜欢这一段设计。
因为它意味着一件事:当通用参数组合不出你的业务时,你可以把策略下沉到终端级别自己控制。
这才更像真实工程,而不是死等配置项“刚好够用”。
这部分不是文档内容,但真的很实战。
别小看这些确认项。
很多“登录模块偶发异常”,最后都不是底层框架问题,而是这些边界在项目里从来没被讲清楚。
以前我会把登录参数看成 Sa-Token 文档里的进阶部分。
现在我更愿意把它理解成:你在给系统定义登录态生命周期和账号边界。
StpUtil.login(loginId) 只是把人放进来。
真正决定系统是不是稳、是不是可控、是不是好排障的,是后面这一整套参数。
登录不是一次调用,登录参数也不是附加信息,它们本质上是在定义一套账号在线策略。
你们项目里,登录这块最容易踩坑的是哪一类?
欢迎把你们踩过的坑留在评论区,我很想看看大家都是怎么把这块一步步磨稳的。
相关文章推荐:
如果这篇文章帮到了你,不妨点个分享给同样需要的朋友吧! 你的每一次支持,都是我持续创作的动力!💪