Sa-Token Token 有效期详解:timeout 与 active-timeout 双过期策略及自动续签机制
【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架,让鉴权变得简单、优雅!—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token
本文基于 Sa-Token 官方文档 Token有效期详解 展开,系统讲解 Sa-Token 提供的两种 Token 自动过期策略——timeout(长久有效期)与active-timeout(最低活跃频率)的配置方式、冻结与过期语义差异、手动续签 API 以及自动续签的底层实现链路。读完后你可以准确配置 Token 有效期、理解"冻结但删除"的两种状态区别,并能在业务中自定义续签时机。
两种过期策略总览
Sa-Token 提供两种 Token 自动过期策略,分别是timeout与active-timeout,配置方法如下:
sa-token: # token 有效期(单位:秒),默认30天,-1代表永不过期 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1# token 有效期(单位:秒),默认30天,-1代表永不过期 sa-token.timeout=2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 sa-token.active-timeout=-1两者的区别,可以通过下面的例子体现:
- 假设你到银行要存钱,首先就要办理一张卡(要访问系统接口先登录)。
- 银行为你颁发一张储蓄卡(系统为你颁发一个 Token),以后每次存取钱都要带上这张卡(后续每次访问系统都要提交 Token)。
- 银行为这张卡设定两个过期时间:
- 第一个是
timeout,代表这张卡的长久有效期,就是指这张卡最长能用多久。假设timeout=3年,那么 3 年后此卡将被银行删除,想要继续来银行办理业务必须重新办卡(Token 过期后想要访问系统必须重新登录)。 - 第二个就是
active-timeout,代表这张卡的最低活跃频率限制,就是指这张卡必须每隔多久来银行一次。假设active-timeout=1月,如果你超过 1 月不来办一次业务,银行就将你的卡冻结,列为长期不动户(Token 长期不访问系统,被冻结,但不会被删除)。
- 第一个是
- 两个过期策略可以单独配置,也可以同时配置,只要有其中一个有效期超出了范围,这张卡就会变得不可用(两个有效期只要有一个过期了,Token 就无法成功访问系统了)。
从源码结构看,两个配置项默认值定义在 SaTokenConfig 中:
/** token 有效期(单位:秒) 默认30天,-1 代表永久有效 */ private long timeout = 60 * 60 * 24 * 30;timeout默认为 30 天(2592000 秒),与文档描述一致;active-timeout默认 -1 代表永不冻结。此外 SaTokenConfig 中保留了@Deprecated的activity-timeout旧配置项,若误用会输出"配置项已过期,请更换:sa-token.activity-timeout -> sa-token.active-timeout"的告警,升级时需注意区分拼写。
timeout:长久有效期
timeout代表 Token 的长久有效期,单位/秒,例如将其配置为 2592000(30 天),代表在 30 天后,Token 必定过期,无法继续使用。timeout无法续签,想要继续使用必须重新登录。v1.29.0+ 版本新增续期方法:StpUtil.renewTimeout(100)。timeout的值配置为 -1 后,代表永久有效,不会过期。
源码印证:renewTimeout 到底续了什么
续期能力的实现位于 StpLogic#renewTimeout,其处理流程可以拆分为七步:
- 校验 token 指向的 loginId 是否存在,为空则直接返回;
- 校验 token 合法性:查不到对应 Access-Session 会话或终端信息时抛出
SaTokenException("未能查询到对应 Access-Session 会话,无法续期"); - 续期 token 本身的有效期(改 dao 中 key 的 ttl);
- 续期该 token 的 Token-Session 有效期;
- 续期该 token 指向账号的 Account-Session 有效期(
session.updateMinTimeout(timeout)); - 若开启了 active-timeout 检查,同步更新"最后活跃时间"记录的有效期;
- 发布
SaTokenEventCenter.doRenewTimeout续期事件,供全局监听器感知。
值得注意的细节在 StpLogic#renewTimeout(long) 中:续期缓存数据之外,如果开启读 Cookie(isReadCookie()),还会同步续期客户端 Cookie 的有效期;当timeout = -1(永久)或超过Integer.MAX_VALUE时,由于浏览器一般不支持永久 Cookie,代码会将其收敛为Integer.MAX_VALUE,避免数据溢出。
active-timeout:最低活跃频率
active-timeout代表最低活跃频率,单位/秒,例如将其配置为 1800(30 分钟),代表用户如果 30 分钟无操作,则此 Token 会立即过期(被冻结,但不会删除掉)。- 如果在 30 分钟内用户有操作,则会再次续签 30 分钟,用户如果一直操作则会一直续签,直到连续 30 分钟无操作,Token 才会过期。
active-timeout的值配置为 -1 后,代表永久有效,不会过期,此时也无需频繁续签。
源码印证:冻结是如何判定的
"冻结"的判定核心是 StpLogic#getTokenActiveTimeoutByToken 中的计算公式:
// 实际时间差 long timeDiff = (System.currentTimeMillis() - lastActiveTime) / 1000; // 该 token 允许的时间差 long allowTimeDiff = getTokenUseActiveTimeoutOrGlobalConfig(tokenValue); if(allowTimeDiff == SaTokenDao.NEVER_EXPIRE) { // 如果允许的时间差为 -1 ,则代表永不冻结,此处需要立即返回 -1 return SaTokenDao.NEVER_EXPIRE; } // 校验这个时间差是否超过了允许的值 // 计算公式为: 允许的最大时间差 - 实际时间差,判断是否 < 0, 如果是则代表已经被冻结 ,返回-2 long activeTimeout = allowTimeDiff - timeDiff; if(activeTimeout < 0) { return SaTokenDao.NOT_VALUE_EXPIRE; // -2,代表已被冻结 } else { return activeTimeout; // 否则返回剩余活跃有效时间 }返回值的语义约定为:-1(NEVER_EXPIRE)代表永不冻结,-2(NOT_VALUE_EXPIRE)代表已被冻结,其余为剩余活跃秒数。StpLogic#isFreeze 正是基于这套返回值判断 token 是否处于冻结状态。
这与"冻结不删除"的语义完全对应:被冻结的 Token 在缓存中依然存在(最后活跃时间记录还在),只是检查时会被拒绝;而timeout过期后 key 的 ttl 到期由缓存自然删除。
当冻结态 Token 发起请求时,StpLogic#checkActiveTimeout 会抛出NotLoginException:
public void checkActiveTimeout(String tokenValue) { if (isFreeze(tokenValue)) { throw NotLoginException.newInstance(loginType, TOKEN_FREEZE, TOKEN_FREEZE_MESSAGE, tokenValue) .setCode(SaErrorCode.CODE_11016); } }其中场景值TOKEN_FREEZE与异常码定义在 NotLoginException:
/** 表示 token 已被冻结 */ public static final String TOKEN_FREEZE = "-6"; public static final String TOKEN_FREEZE_MESSAGE = "token 已被冻结";也就是说,前端捕获到"未登录"异常时,可以通过场景值-6与11016错误码区分出"token 已被冻结"这一特定情形,与 token 无效(-2)、超时(-4)、被顶下线(-5)等异常场景互不混淆。
单元测试印证
仓库内置的 StpLogicActiveTimeoutTest 覆盖了上述关键行为,例如:
- 登录 10001 后调用
updateLastActiveToNow(),getTokenActiveTimeout()返回值应落在175 ~ 180秒区间(配置active-timeout=180),且checkActiveTimeout()不抛异常; - 将
active-timeout配置为 -1 时,getTokenActiveTimeoutByToken()应返回SaTokenDao.NEVER_EXPIRE(永不冻结); - 配置
active-timeout=10秒并等待超时后,对应方法返回SaTokenDao.NOT_VALUE_EXPIRE(冻结态)。
这些用例与本文"计算公式 + 返回值约定"的源码分析一一对应。
关于 active-timeout 的续签
如果active-timeout配置了大于零的值,Sa-Token 会在登录时开始计时,在每次直接或间接调用getLoginId()、getTokenSession()时进行一次冻结检查与续签操作。此时会有两种情况:
- 一种是会话无操作时间太长,Token 已经被冻结,此时框架会抛出
NotLoginException异常(场景值 =-6,异常信息"token 已被冻结"); - 另一种则是会话在
active-timeout有效期内通过检查,此时 Token 可以成功续签。
自动续签的实现入口是 StpLogic#checkActiveTimeoutByConfig:
public void checkActiveTimeoutByConfig(String tokenValue) { if(isOpenCheckActiveTimeout()) { // storage.get(key, () -> {}) 可以避免一次请求多次校验,造成不必要的性能消耗 SaHolder.getStorage().get(SaTokenConsts.TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY, () -> { // 1、检查此 token 的最后活跃时间是否已经超过了 active-timeout 的限制, // 如果是则代表其已被冻结,需要抛出:token 已被冻结 checkActiveTimeout(tokenValue); // 2、如果配置了自动续签功能, 则: 更新这个 token 的最后活跃时间 // (注意此处的续签是在续 active-timeout,而非 timeout) if(SaStrategy.instance.autoRenew.apply(this)) { updateLastActiveToNow(tokenValue); } return true; }); } }这段代码体现了两个工程细节:
- 单次请求内去重:借助
SaHolder.getStorage().get(key, supplier),同一请求中多次调用getLoginId()等触发续签的方法时,冻结检查与续签只执行一次,避免不必要的缓存读写; - 续签策略可插拔:是否续签由 SaStrategy#autoRenew 这个策略函数决定,默认为 true,可通过配置项
autoRenew=false或自定义策略覆盖。
而续签动作本身 StpLogic#updateLastActiveToNow 的实现非常轻量——把"最后活跃时间"key 的 value 更新为当前时间戳 + 本次允许的 active-timeout,并刷新该 key 的过期时间:
public void updateLastActiveToNow(String tokenValue) { String key = splicingKeyLastActiveTime(tokenValue); String value = new SaValue2Box(System.currentTimeMillis(), getTokenUseActiveTimeout(tokenValue)).toString(); getSaTokenDao().update(key, value); }这正是"滑动窗口"语义的底层实现:每次有效访问都从当前时刻重新起算活跃窗口,只要用户持续活跃,窗口就不断顺延。
手动续签 active-timeout
可以!如果框架的自动续签算法无法满足您的业务需求,你可以进行手动续签,Sa-Token 提供两个 API 供你操作:
StpUtil.checkActiveTimeout():检查当前 Token 是否已经被冻结,如果是则抛出异常StpUtil.updateLastActiveToNow():续签当前 Token(将 [最后操作时间] 更新为当前时间戳)
注意:在手动续签时,即使 Token 已经被冻结也可续签成功(解冻),如果此场景下需要提示续签失败,可采用先检查再续签的形式保证 Token 有效性。
例如以下代码:
// 先检查是否已被冻结 StpUtil.checkActiveTimeout(); // 检查通过后继续续签 StpUtil.updateLastActiveToNow();这一注意事项与 StpLogic#updateLastActiveToNow 的 Javadoc 完全一致:"请注意: 即使 token 已被冻结 也可续签成功,如果此场景下需要提示续签失败,可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可"——因为续签只是覆写"最后活跃时间"记录,并不会校验当前是否冻结。
同时,你还可以关闭框架的自动续签(在配置文件中配置autoRenew=false),此时续签操作完全由开发者控制,框架不再自动进行任何续签操作。对应源码中 SaTokenConfig#autoRenew 默认值为true:
private Boolean autoRenew = true; // 是否打开自动续签 activeTimeout // (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)如果你需要给其它 Token 续签:
// 为指定 Token 续签 StpUtil.stpLogic.updateLastActiveToNow(tokenValue);timeout 与 active-timeout 可以同时使用吗?
可以同时使用!两者的认证逻辑彼此独立,互不干扰,可以同时使用。
从源码结构看,timeout走的是 token key 自身的缓存 ttl 到期机制,而active-timeout依赖独立的"最后活跃时间"key 做滑动检查,两者互不感知;同时生效时,任何一个到期(过期或冻结)都会导致该 Token 无法通过认证。
StpUtil 类中哪些方法支持自动续签 active-timeout?
直接或间接调用过getLoginId()、getTokenSession()的方法,包括但不限于:
| 包括但不限于这些 |
|---|
StpUtil.checkLogin() |
StpUtil.getLoginId() |
StpUtil.getLoginIdAsInt() |
StpUtil.getLoginIdAsString() |
StpUtil.getLoginIdAsLong() |
| --- |
StpUtil.getSession() |
StpUtil.getTokenSession() |
| --- |
StpUtil.getRoleList() |
StpUtil.hasRole() |
StpUtil.hasRoleAnd() |
StpUtil.hasRoleOr() |
StpUtil.checkRole() |
StpUtil.checkRoleAnd() |
StpUtil.checkRoleOr() |
| --- |
StpUtil.getPermissionList() |
StpUtil.hasPermission() |
StpUtil.hasPermissionAnd() |
StpUtil.hasPermissionOr() |
StpUtil.checkPermission() |
StpUtil.checkPermissionAnd() |
StpUtil.checkPermissionOr() |
| --- |
StpUtil.openSafe() |
StpUtil.isSafe() |
StpUtil.checkSafe() |
StpUtil.getSafeTime() |
StpUtil.closeSafe() |
以下注解都间接调用过getLoginId()方法:
| 支持自动续签的注解 |
|---|
@SaCheckLogin |
@SaCheckRole |
@SaCheckPermission |
@SaCheckSafe |
实践建议小结
结合本文的机制说明,给出几条可直接落地的配置建议:
| 场景 | 推荐配置 | 说明 |
|---|---|---|
| 管理后台/敏感业务 | timeout: 86400+active-timeout: 1800 | 最长 1 天,30 分钟无操作即冻结,兼顾安全与体验 |
| 普通 Web 应用 | timeout: 2592000(默认)+active-timeout: -1(默认) | 30 天长久有效,不限制活跃 |
| 移动端长连接 | timeout: -1+active-timeout: 604800 | 永不自然过期,但 7 天不活跃即冻结 |
| 需要业务自定义续签时机 | autoRenew: false+ 手动调用updateLastActiveToNow() | 框架不再自动续签,完全由业务代码控制 |
关键语义再强调一次:timeout到期是删除(必须重新登录),active-timeout到期是冻结(记录仍在,检查即被拒,场景值-6);renewTimeout()只能续timeout,updateLastActiveToNow()只能续active-timeout,两者不可互相替代。
参考资料(仓库内路径)
- 文档原文:sa-token-doc/fun/token-timeout.md
- 配置项定义:SaTokenConfig
- 冻结判定与续签核心实现:StpLogic
- 冻结异常定义:NotLoginException
- 续签策略函数:SaStrategy
- 行为验证用例:StpLogicActiveTimeoutTest
【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架,让鉴权变得简单、优雅!—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考