首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >登录不是接口调用,而是一套账号生命周期策略

登录不是接口调用,而是一套账号生命周期策略

作者头像
程序员NEO
发布2026-04-29 19:37:59
发布2026-04-29 19:37:59
870
举报
你有没有遇到过这种情况:登录功能明明已经跑通了,一上生产却全是边角问题。有人被莫名顶下线,有人点了退出别的设备还在,有人没勾“记住我”第二天却还在线。

我第一次接这块时,也以为就是一个 StpUtil.login() 的事。后来把 Sa-Token 真放进项目里,我才发现,真正决定系统稳不稳的,不是你能不能让用户登录,而是你有没有把登录策略设计清楚。

这篇文章,我就把 Sa-Token 里的登录参数、注销参数、终端控制拆开讲。不是照着文档翻译,而是站在真实项目的角度,看看这些参数到底解决什么问题,什么时候该用,什么时候别乱用。

为什么很多人把登录做出来了,却没把登录策略做出来

说实话,如果只是写 Demo,StpUtil.login(loginId) 真的够用了。

但到了真实项目里,产品和业务很快就会把问题变复杂:

  • 要不要记住我。
  • 同一账号允不允许多端同时在线。
  • PC 和 APP 能不能共存,两个 PC 能不能共存。
  • 退出登录时,到底退当前设备,还是整个账号。
  • 后台账号被共享时,要不要强制顶掉旧会话。
  • 出了问题,运维能不能看清到底是哪台设备在线。

你会发现,登录从来都不是一个动作。

它更像是一整套策略:谁在登录、从哪登录、能活多久、跟谁互斥、退出时影响谁。

Sa-Token 的这套参数,恰恰就是把这些策略显式交给你。

SaLoginParameter 不是参数表,它其实是在回答 3 个问题

先看最基础的两个例子:

代码语言:javascript
复制
// 指定`账号id`和`设备类型`进行登录
StpUtil.login(10001, "PC");    

// 设置登录账号 id 为 10001,并指定是否为 “记住我” 模式
StpUtil.login(10001, false);

很多人第一次看到这里,会觉得第二个参数只是个“补充信息”。

但我后来越看越觉得,它其实已经在表达策略了。

  • • 传 "PC",你是在声明这次登录属于哪个终端。
  • • 传 false,你是在声明这次登录是不是持久化会话。

如果你需要更细一点的控制,Sa-Token 直接给了一个完整的参数对象:

代码语言:javascript
复制
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 个问题就顺了。

1. 这次登录是谁、从哪来

这类参数我会优先看:

  • setDeviceType("PC"):先把设备边界定义清楚,不然后面的同端互斥、跨端共存都没法谈。
  • setDeviceId("xxxxxxxxx"):如果你们有可信设备、风控、登录审计,这个字段会非常有用。
  • setTerminalExtra("key", "value"):适合挂一些终端级别的信息,比如渠道、客户端版本、来源标记。

这里我踩过一个很真实的坑。

很多团队一开始根本没有认真定义 deviceType,随手写成 pcPCadmin-webweb 混着来。前期还能跑,后面一做“同端互斥”或者“指定端注销”,线上行为就开始变得不可解释。

设备类型不是一个字符串细节,它是登录策略的分组键。

2. 这次登录能活多久,怎么活

这组参数最容易和“记住我”混在一起:

  • setIsLastingCookie(true):控制的是 Cookie 持不持久,重点在“浏览器关了之后还在不在”。
  • setTimeout(...):控制的是 Token 过不过期,重点在“服务端还认不认”。
  • setActiveTimeout(...):更适合做活跃校验,不是简单的总时长。
  • setupCookieConfig(...):这块一旦上生产,SecureHttpOnlySameSite 基本都绕不过去。

很多线上问题,其实不是代码写错了,而是这两个概念从一开始就没拆开:

  • 本地凭证还在不在。
  • 服务端会话还认不认。

这两个问题看起来像一件事,实际上完全不是一件事。

所以我现在和产品沟通“记住我”时,不会只问一句“要不要支持”。

我会直接追问两件事:

  • 浏览器关闭后,下次打开还要不要自动带登录态。
  • 这个登录态最多允许持续多久。

你把这两个问题问清楚,后面一半的坑都能提前避掉。

3. 同一账号允许怎么共存,超了以后怎么处理

这组参数特别像业务规则,不像工具参数:

  • setIsConcurrent(true):允不允许同账号并发登录。
  • setIsShare(false):并发登录时,是共用一个 token,还是每次新建。
  • setMaxLoginCount(12):允许同时保留多少个登录终端。
  • setReplacedRange(...):顶人下线影响当前设备类型,还是所有设备类型。
  • setOverflowLogoutMode(...):超出数量时,用注销、踢下线,还是顶下线来处理。

说实话,这块如果只是本地调通接口,怎么配都像能用。

但一旦到了真实生产环境,问题马上就来了。

比如:

  • 运营后台 往往不希望同端无限并发。
  • C 端产品 往往又不希望用户因为换个手机就被强踢。
  • 共享账号场景 里,isShare=trueisShare=false,后续审计成本差很多。

我现在更倾向一个判断:

  • C 端高频产品,体验优先,策略可以更宽松。
  • 后台、财务、运营系统,边界优先,策略最好更保守。

因为后台系统里,真正贵的不是让人重新登录,而是出了安全问题之后你解释不清。

登录做完不算完,logout 才是很多团队后面补债的地方

很多人只盯着登录,忽略注销。

但我后面越来越觉得,注销参数直接决定了你的退出动作到底影响谁。

原始用法是这样的:

代码语言:javascript
复制
// 当前客户端注销 
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)
  • 管理员指定某个 token 下线:重点是 logoutByTokenValue(...)
  • 按账号、按设备类型清理会话:重点是 setDeviceType("PC")
  • 不同下线原因要不要区分:重点是 setMode(...)

这里最容易被忽略的一点是,“退出登录”不一定都是同一种退出。

有的是用户主动退。

有的是管理员踢。

有的是新会话把旧会话顶掉。

如果前后端不把这个语义对齐,最后用户看到的就只是一句很模糊的“未登录”,排障会特别痛苦。

配置组合不出业务场景时,别硬拧,直接按终端精细化处理

真实项目做到后面,经常会出现一种情况:

参数都有,但还是不够。

比如你想做更细的终端策略:

  • • 某类设备只保留最近两台。
  • • 特定终端满足条件才允许继续在线。
  • • 某个账号只清理偶数序号终端做测试验证。

这时候别硬往配置里塞,Sa-Token 已经给了一个更稳的口子:

代码语言:javascript
复制
// 测试 
@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();
}

我很喜欢这一段设计。

因为它意味着一件事:当通用参数组合不出你的业务时,你可以把策略下沉到终端级别自己控制。

这才更像真实工程,而不是死等配置项“刚好够用”。

我自己上线前,一般会再确认这 6 件事

这部分不是文档内容,但真的很实战。

  • 设备类型枚举有没有统一。 不统一,后面所有同端策略都会漂。
  • “记住我”到底影响 Cookie、Token,还是两者都影响。 这件事一定要提前写清楚。
  • 前端存储位置和后端参数语义是否一致。 页面写“临时登录”,结果 token 却长期落本地,这种坑我见过不止一次。
  • 退出登录时,服务端和前端本地数据有没有一起清。 只清一边,迟早出鬼故事。
  • 后台和 C 端是不是用了同一套默认策略。 这件事通常不合理。
  • 被踢下线、被顶下线、主动退出,前端文案是否能区分。 这会直接影响排障效率。

别小看这些确认项。

很多“登录模块偶发异常”,最后都不是底层框架问题,而是这些边界在项目里从来没被讲清楚。

最后一句我现在特别认同

以前我会把登录参数看成 Sa-Token 文档里的进阶部分。

现在我更愿意把它理解成:你在给系统定义登录态生命周期和账号边界。

StpUtil.login(loginId) 只是把人放进来。

真正决定系统是不是稳、是不是可控、是不是好排障的,是后面这一整套参数。

工程金句

登录不是一次调用,登录参数也不是附加信息,它们本质上是在定义一套账号在线策略。

你们项目里,登录这块最容易踩坑的是哪一类?

  • • 是“记住我”和 Token 生命周期老是混在一起?
  • • 是多端登录策略一上生产就开始互相打架?
  • • 还是退出登录做得太粗,最后根本查不清谁把谁顶下线了?

欢迎把你们踩过的坑留在评论区,我很想看看大家都是怎么把这块一步步磨稳的。

相关文章推荐

如果这篇文章帮到了你,不妨点个分享给同样需要的朋友吧! 你的每一次支持,都是我持续创作的动力!💪

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-03-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员NEO 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么很多人把登录做出来了,却没把登录策略做出来
  • SaLoginParameter 不是参数表,它其实是在回答 3 个问题
    • 1. 这次登录是谁、从哪来
    • 2. 这次登录能活多久,怎么活
    • 3. 同一账号允许怎么共存,超了以后怎么处理
  • 登录做完不算完,logout 才是很多团队后面补债的地方
  • 配置组合不出业务场景时,别硬拧,直接按终端精细化处理
  • 我自己上线前,一般会再确认这 6 件事
  • 最后一句我现在特别认同
  • 工程金句
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档