
Sa-Token 数据结构全解析Token、Session 与插件存储 Key 的命名规范与字段格式【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token导读Sa-Token 作为一款开源、免费的 Java 权限认证框架其登录会话、SSO 单点登录、OAuth2 统一认证及各类插件的数据最终都持久化在 Redis、Memcached 等中间件中。理解这些数据的 Key 命名规则与 Value 字段格式是排查线上数据异常、实现跨服务共享会话、以及二次开发自定义存储层的必备基础。本文以仓库文档>// ttl 此 token 的 timeout 有效期 {tokenName}:{loginType}:token:{tokenValue} -- {loginId}该 Key 正是由splicingKeyTokenValue()方法拼接写入逻辑位于 StpLogic.java 的getSaTokenDao().set(splicingKeyTokenValue(tokenValue), String.valueOf(loginId), timeout)。读取、续期、删除则分别对应get、update、delete操作getTimeout()用于查询 token 剩余有效期。实际示例satoken:login:token:47ab0105-2be1-400c-b517-82f81a0cfcf8 -- 10001异常 value 格式当从请求中读取到的 token 不合法时Sa-Token 会向调用方返回特定的负数值这些值也是业务开发排查登录异常的关键依据-1 未能从请求中读取到有效 token -2 已读取到 token但是 token 无效 -3 已读取到 token但是 token 已经过期 (详) -4 已读取到 token但是 token 已被顶下线 -5 已读取到 token但是 token 已被踢下线 -6 已读取到 token但是 token 已被冻结 -7 未按照指定前缀提交 token2.2、active-timeout活跃超时当开启active-timeout配置后Sa-Token 会额外维护每个 token 的最后活跃时间// ttl 对应 token 的 timeout 有效期值 {tokenName}:{loginType}:last-active:{tokenValue} -- {13位时间戳}对应源码方法为splicingKeyLastActiveTime(tokenValue)返回tokenName : loginType :last-active: tokenValue读取与更新活跃时间的逻辑位于 StpLogic.java。示例satoken:login:last-active:06d1f12b-614e-4c00-8d8e-c07fef5f4aa9 -- 1722334954193value 格式分为两种1722334954193 // 单值时此 token 最后访问日期 1722334954193, 1200 // 双值时此 token 最后访问日期此 token 指定的动态 active-timeout 值需要特别注意的是判断一个 token 是否 active-timeout 过期与 ttl 无关而是利用 value 值计算当前时间 - token 最后访问时间 active-timeout truetoken 已冻结falsetoken 未冻结 这意味着即使 Key 本身的 ttl 尚未归零只要「最后访问时间 active-timeout」早于当前时间token 就会被判定为冻结对应上面异常值中的-6。2.3、SaSession 三种会话对象Sa-Token 提供三种会话类型对应三类 Key{tokenName}:{loginType}:session:{loginId} -- {SaSession 对象} // Account-Session {tokenName}:{loginType}:token-session:{loginId} -- {SaSession 对象} // Token-Session {tokenName}:custom:session:{sessionId} -- {SaSession 对象} // Custom-SessionAccount-Session账号会话与账号绑定登录后无论使用哪个 token 访问都是同一个会话Key 中为loginIdToken-Session令牌会话与单个 token 绑定不同 token 各自拥有独立会话Key 中为tokenValue由splicingKeyTokenSession()拼接注意其分段为token-sessionKey 示例中satoken:login:session:xxx为文档原文演示实际拼接形态以 StpLogic.java 为准Custom-Session自定义会话完全由业务方指定 sessionId 的会话不依赖账号或 token。Key 示例// Account-Session satoken:login:session:1000001 // Token-Session satoken:login:session:47ab0105-2be1-400c-b517-82f81a0cfcf8 // Custom-Session satoken:custom:session:role-1001value 格式Session 对象以 JSON 形式序列化存储其中class字段记录 Java 类信息用于反序列化还原典型结构如下{ class: cn.dev33.satoken.dao.SaSessionForJacksonCustomized, // java calss 信息 id: satoken:login:session:10001, // sessionId type: Account-Session, // session类型Account-Session / Token-Session / Custom-Session loginType: login, // 账号类型 loginId: [ // 对应登录id 值Account-Session才会有值 java.lang.Long, 10001 ], token: null, // 对应 token 值 Token-Session才会有值 createTime: 1722334954145, // 此 session 创建时间13位时间戳 dataMap: { // 此 session 挂载数据 class: java.util.concurrent.ConcurrentHashMap, name: 张三 // 此 session 挂载数据 详情 // 更多值 ... }, terminalList: [ // 已登录终端信息列表Account-Session才会有值 java.util.Vector, [ { class: cn.dev33.satoken.session.SaTerminalInfo, index: 1, tokenValue: 2551663f-bb98-47d7-9af3-e2e6a28dadce, // 客户端 token 值 deviceType: DEF, // 登录设备类型 deviceId: xxxxxxxxx, // 登录设备id extraData: { // 扩展信息列表 手动自定义值 class: java.util.LinkedHashMap, deviceSimpleTitle: XiaoMi 15 Ultra, loginAddress: 浙江省杭州市西湖区, loginIp: 127.0.0.1, loginTime: 2025-03-08 15:00:02 }, createTime: 1741406340845 // 登录时间 } ] ] }其中terminalList是 Account-Session 中的多端登录终端信息列表配合「同端互斥登录」等场景使用SaTerminalInfo类定义于 SaTerminalInfo.java 附近的核心 session 包下DEFAULT_LOGIN_DEVICE_TYPE DEF常量声明于 SaTokenConsts.java。2.4、二级认证业务二级认证如支付密码二次校验使用如下 Key{tokenName}:{loginType}:safe:{service}:{tokenValue} -- SAFE_AUTH_SAVE_VALUEvalue 为常量值SAFE_AUTH_SAVE_VALUE。对应拼接方法为splicingKeySafe(tokenValue, service)注释中给出的形态为satoken:login:safe:important:gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__即「Token名称:账号类型:safe:业务标识:Token值」。写入、校验、删除分别对应 StpLogic.java 中的openSafe、isSafe、closeSafe系列方法其中写入时通过getSaTokenDao().set(...)并设置safeTime作为 Key 的 ttl。2.5、账号服务封禁{tokenName}:{loginType}:disable:{service}:{loginId} -- {level}value 为封禁等级int 类型。对应方法splicingKeyDisable(loginId, service)返回tokenName : loginType :disable: service : loginId。封禁等级相关常量集中在 SaTokenConsts.java默认封禁服务DEFAULT_DISABLE_SERVICE login默认封禁等级DEFAULT_DISABLE_LEVEL 1最小封禁级别MIN_DISABLE_LEVEL 1未封禁级别NOT_DISABLE_LEVEL -2。写入封禁标记、查询封禁剩余时间、删除封禁的源码逻辑分别位于 StpLogic.java。2.6、其它存储SaApplication 全局变量应用级共享数据{tokenName}:var:{变量名}本次请求新创建 token 的 SaStorage 存储 key带前缀方式JUST_CREATED_ -- {token}本次请求新创建 token 的 SaStorage 存储 key无前缀方式JUST_CREATED_NOT_PREFIX_ -- {token}这两个常量JUST_CREATED与JUST_CREATED_NOT_PREFIX定义于 SaTokenConsts.java。在登录创建新 token 时StpLogic.java 会根据token-prefix配置决定是否带前缀存入 SaStorage后续同一次请求内可直接取出该 token 而无需二次解析。临时身份切换存储 keySWITCH_TO_SAVE_KEY_{loginType} -- {loginId}对应splicingKeySwitch()返回SaTokenConsts.SWITCH_TO_SAVE_KEY loginType临时切换身份switchTo的写入与清除逻辑位于 StpLogic.java。三、SSO 单点登录模块的数据结构SSO 服务端在「登录认证通过后签发 ticket、客户端携 ticket 换取登录凭证」的流程中共维护三组映射关系均以{tokenName}默认satoken开头3.1、ticket - loginId 映射// ttl 此 ticket 有效期下同理 {tokenName}:ticket:{ticket} -- {loginId}对应源码方法为 SaSsoServerTemplate.java 中的splicingTicketModelSaveKey(ticket)返回tokenName :ticket: ticket。ticket 签发setObjectticketTimeout与校验getObject分别位于该类的 第 70 行 与 第 142 行 附近。3.2、ticket - client 映射{tokenName}:ticket-client:{ticket} -- {client}该 Key 用于记录 ticket 签发给哪个接入应用client是 SSO 回调校验时核对来源的重要依据。3.3、loginId - ticket 映射client loginId 反查 ticket{tokenName}:ticket-index:{client}:{loginId} -- {ticket}对应splicingTicketIndexKey(client, id)SaSsoServerTemplate.java实现上会做一层兜底当client为空或等于通配符*时替换为CLIENT_ANON匿名标记最终返回tokenName :ticket-index: client : id。写索引setticketTimeout、删索引、查索引分别位于该类的 第 236 行、第 249 行 与 第 263 行 附近。借助这组反向索引服务端无需遍历即可实现「同一 client 同一账号的 ticket 幂等复用或强制下线」。四、OAuth2 统一认证模块的数据结构OAuth2 模块插件sa-token-oauth2的存储 Key 统一以{tokenName}:oauth2:为前缀全部拼接方法集中在 SaOAuth2Dao.java。4.1、Code 授权码{tokenName}:oauth2:code:{code} -- {CodeModel 对象}value 示例{ class: cn.dev33.satoken.oauth2.model.CodeModel, // java class 信息 code: AbRVp2HrgyklE0BXYWszskGJWAGY7xhGu6Zaco4zJECzGYagCCFWj0jOlHza, // code值 scope: , // 所申请权限列表多个用逗号隔开 loginId: 10001, // 对应的loginId redirectUri: , // 重定向地址 }clientId loginId 反查 code{tokenName}:oauth2:code-index:{clientId}:{loginId} -- {code 值}4.2、Access-Token 资源令牌{tokenName}:oauth2:access-token:{accessToken} -- {AccessTokenModel 对象}value 示例{ class: cn.dev33.satoken.oauth2.data.model.AccessTokenModel, // java class 信息 accessToken: Pu3t55dJIgvkmVoHz50FqaVQOJ6Flggjr2eHTiS74Ooai8e3nNyYPq78K80P, // 资源令牌值 refreshToken: baGyl6PHK304tPojnpxd1SpW12oJcOGv7gFaDAAkjLWbJG1J1WLUIGobsw7m, // 刷新令牌值 expiresTime: 1738280553695, // 资源令牌到期时间 refreshExpiresTime: 1740865353760, // 刷新令牌到期时间 clientId: 1001, // 对应的应用id loginId: 10001, // 对应的loginId scopes: [ // 所具有的权限列表 java.util.ArrayList, [ userinfo, userid, openid, unionid, oidc ] ], tokenType: Bearer, // tokenType grantType: authorization_code, // 授权方式 extraData: { // 扩展数据 class: java.util.LinkedHashMap, userid: 10001, openid: ded91dc189a437dd1bac2274be167d50, unionid: 11d48faa74c4e5f19355ccc53c1c5c7a, id_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzM4MjczOTUzLCJpYXQiOjE3MzgyNzMzNTMsImF1dGhfdGltZSI6MTczODI3MzM0Miwibm9uY2UiOiJZQTlPQjJzYkpGanZkUlFjN0E3V1pnTUFhTDFVRjE5OSIsImF6cCI6IjEwMDEifQ.pvoj6CR7tdhOblvYJoGUfvam9egSiL5Uw3tflLLMb5g }, createTime: 1738273353694, // 创建时间 expiresIn: 7199 // 资源令牌剩余有效时间单位秒 refreshExpiresIn: 2592000, // 刷新令牌剩余有效时间单位秒 }clientId loginId 反查 Access-Token{tokenName}:oauth2:access-token-index:{clientId}:{loginId} -- {access_token 值}4.3、Refresh-Token 刷新令牌{tokenName}:oauth2:refresh-token:{refreshToken} -- {RefreshTokenModel 对象}value 示例{ class: cn.dev33.satoken.oauth2.data.model.RefreshTokenModel, // java class 信息 refreshToken: baGyl6PHK304tPojnpxd1SpW12oJcOGv7gFaDAAkjLWbJG1J1WLUIGobsw7m, // 刷新令牌值 expiresTime: 1740865353760, // 刷新令牌到期时间 clientId: 1001, // 对应的应用id loginId: 10001, // 对应的loginId scopes: [ // 所具有的权限列表 java.util.ArrayList, [ userinfo, userid, openid, unionid, oidc ] ], extraData: { // 扩展数据 class: java.util.LinkedHashMap, userid: 10001, openid: ded91dc189a437dd1bac2274be167d50, unionid: 11d48faa74c4e5f19355ccc53c1c5c7a, id_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzM4MjczOTUzLCJpYXQiOjE3MzgyNzMzNTMsImF1dGhfdGltZSI6MTczODI3MzM0Miwibm9uY2UiOiJZQTlPQjJzYkpGanZkUlFjN0E3V1pnTUFhTDFVRjE5OSIsImF6cCI6IjEwMDEifQ.pvoj6CR7tdhOblvYJoGUfvam9egSiL5Uw3tflLLMb5g }, createTime: 1738273353760, // 创建时间 expiresIn: 2591999 // 刷新令牌剩余有效时间单位秒 }clientId loginId 反查 Refresh-Token{tokenName}:oauth2:refresh-token-index:{clientId}:{loginId} -- {refresh_token 值}4.4、Client-Token 应用令牌{tokenName}:oauth2:client-token:{clientToken} -- {ClientTokenModel 对象}value 示例{ class: cn.dev33.satoken.oauth2.data.model.ClientTokenModel, // java class 信息 clientToken: lIpS3fKEACKMFauEWVpR7Zmzh7SoFetPVuB9aDzISnqzHKu8R3OwpWFy5nLv, // 应用令牌值 expiresTime: 1738280930646, // 应用令牌到期时间 clientId: 1001, // 对应的应用id scopes: [ // 所具有的权限列表 java.util.ArrayList, [ userinfo, userid, openid, unionid, oidc ] ], tokenType: Bearer, // tokenType grantType: client_credentials, // 授权类型 extraData: { // 扩展数据 class: java.util.LinkedHashMap }, createTime: 1738273730646, // 创建时间 expiresIn: 7199 // 应用令牌剩余有效时间单位秒 }clientId 反查 Client-Token{tokenName}:oauth2:client-token-index:{clientId} -- {client_token 值}Lower-Client-Token 次级应用令牌索引{tokenName}:oauth2:lower-client-token-index:{clientId} -- {client_token 值}4.5、用户授权记录{tokenName}:oauth2:grant-scope:{clientId}:{loginId} -- {scope列表}值为 scope 列表多个用逗号隔开例如userinfo,openid,userid。该 Key 记录某 client 对某用户已授权的 scope 集合对应splicingKeyGrantScope(clientId, loginId)返回tokenName :oauth2:grant-scope: clientId : loginId见 SaOAuth2Dao.java是「免重复授权」流程的数据基础。五、插件模块的数据结构5.1、临时 token 会话temp-tokentemp-token 用于跨系统、跨场景的临时身份凭证存储结构如下// namespace 默认值为 temp-token {tokenName}:{namespace}:{temp-token} -- {value}value 反查 temp-token{tokenName}:raw-session:{namespace}:{value} -- {Raw#SaSession 对象}在 SaSession 以__HD_TEMP_TOKEN_MAP为 key 存储 temp-token 索引列表。值类型为 Map。其中Map 的 key temp-token 值Map 的 value 此 temp-token 到期时间戳。这种「正查 反查」的双索引设计使业务方既可以用 temp-token 反查数据也可以拿原始 value 反查它关联了哪些临时 token。5.2、Same-Token微服务同源校验Same-Token 与临时旧值存储如下{tokenName}:var:same-token -- {same-token 值}{tokenName}:var:past-same-token -- {same-token 值}两者均挂在{tokenName}:var:前缀下与全局变量共用同一命名空间段past-same-token用于同源校验刷新时的旧值留存窗口。5.3、Sign 签名API 参数签名// nonce 值 默认为 32位随机字符 {tokenName}:sign:nonce:{nonce} -- {nonce 值}用于防重放校验服务端收到带签名的请求后将随机串 nonce 以该 Key 写入存储并设置有效期相同 nonce 再次出现即判定为重放请求。5.4、API Key密钥授权// namespace 默认值为 apikey (全小写), ttl 此 API Key 剩余有效期 {tokenName}:{namespace}:{apikey} -- {ApiKeyModel 对象}namespace 用于多实例隔离由 SaApiKeyTemplate.java 的构造函数注入非空校验默认使用apikey。key 示例satoken:apikey:AK-XCoJLP2E7Q9GXyPiiZWMM8Sqi6Fm0JoFC41Rvalue 示例{ class: cn.dev33.satoken.apikey.model.ApiKeyModel, // java class 信息 title: test, // API Key 名称 intro: null, // 用途介绍 apiKey: AK-XCoJLP2E7Q9GXyPiiZWMM8Sqi6Fm0JoFC41R, // API Key 值 loginId: 10001, // 所属用户 id createTime: 1766509019137, // 创建时间戳 expiresTime: 1769101019136, // 到期时间戳 isValid: true, // 是否有效 scopes: [ // 含有权限 java.util.ArrayList, [ userinfo, user-update ] ], extraData: null // 扩展数据MapString, Object 类型 }value 反查 API Key{tokenName}:raw-session:{namespace}:{value} -- {Raw#SaSession 对象}在 SaSession 以__HD_API_KEY_LIST为 key 存储 API Key 索引列表。值类型为 List (API Key 列表)。与 temp-token 一样API Key 也通过{tokenName}:raw-session:{namespace}:{value}建立反向索引从而支持「根据用户/数据反查其名下全部 API Key」的运维与审计需求。六、总结数据结构的三大设计要点纵观全篇Sa-Token 的数据结构设计可以提炼出三个关键点理解它们有助于在排障与二次开发中举一反三前缀一致、语义分段所有 Key 都以{tokenName}为总前缀随后按「账号类型 → 数据类型 → 业务标识」逐段展开。这让 Redis 中同属一个登录业务的 Key 天然聚集便于使用KEYS satoken:login:*或SCAN前缀批量检索。正反索引成对出现SSO 的ticket-index、OAuth2 的code-index / access-token-index / refresh-token-index / client-token-index、插件的raw-session反查均是为了在持有某一侧业务主键时能 O(1) 反查另一侧避免全量遍历。value 即文档、模型即契约各模型的class字段记录 Java 类全限定名expiresTime / createTime / expiresIn三组时间字段完整刻画令牌生命周期直接对接 ttl 计算逻辑。官方文档 contenteditable="false">【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。