资讯详情 Sa-Token 集成 MongoDB:基于 SaTokenDao 实现会话与 Token 持久化(Spring Boot 实战)
📅 2026/11/13 11:12:29
Sa-Token 集成 MongoDB基于 SaTokenDao 实现会话与 Token 持久化Spring Boot 实战【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token导读Sa-Token 默认将 Token、Session 等数据保存在进程内内存中SaTokenDaoDefaultImpl.java基于ConcurrentHashMap应用重启后数据即丢失且无法在多实例集群间共享。本文以 integ-spring-mongod-2.md 为骨架讲解如何在 Spring Boot 下通过实现SaTokenDao接口把 Sa-Token 的数据持久化到 MongoDB并利用 MongoDB 的 TTL 索引实现数据自动过期删除。读完本文你将掌握自定义SaSession子类、重写SaStrategy.instance.createSession、设计基于MongoTemplate的SaTokenDao实现类等完整落地步骤可直接照搬到自己的项目中。本文配套的另一套实现思路见 integ-spring-mongod-1.md使用Document包装类 expireAt过期字段的方案两篇可对照阅读。一、集成思路一切围绕 SaTokenDao 展开Sa-Token 的所有缓存读写最终都收敛到一个接口上——SaTokenDao.java。该接口统一定义了四类操作操作分类方法字符串读写Token 值、登录标记等get/set/update/delete/getTimeout/updateTimeout对象读写SaSession、OAuth2 票据等getObject/setObject/updateObject/deleteObject/getObjectTimeout/updateObjectTimeoutSaSession 读写默认复用 Object 方法getSession/setSession/updateSession/deleteSession/getSessionTimeout/updateSessionTimeout会话管理searchData接口中定义了两个关键常量见 SaTokenDao.javaNEVER_EXPIRE -1表示永不过期NOT_VALUE_EXPIRE -2表示系统中不存在该缓存。所有timeout参数的单位均为秒语义为值大于 0 时限时存储值 -1 时永久存储值 0 或 ≤ -2 时不存储。我们的 MongoDB 实现必须严格遵循这套约定Sa-Token 框架才能正确判断数据的存活状态。需要特别说明的是SaSession的六个方法在接口中都有默认实现见 SaTokenDaoBySessionFollowObject.javagetSession通过getObject(sessionId, SaStrategy.instance.sessionClassType)读取它们最终会委托给 Object 读写方法。因此 MongoDB 集成时只要把SaSession作为 Object 存进 MongoDB即可复用默认实现无需为 Session 单独编写 CRUD。二、第一步引入 MongoDB 依赖在pom.xml或build.gradle中加入 Spring Data MongoDB 依赖!-- Maven 方式 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-mongodb/artifactId /dependency// Gradle 方式 implementation org.springframework.boot:spring-boot-starter-data-mongodb前置条件Spring Boot 3.x 环境 可用的 MongoDB 实例并在application.yml中配置好 MongoDB 连接信息spring.data.mongodb.uri或host/port/database。三、第二步自定义 MySaSession 子类解决反序列化难题直接使用框架的SaSession作为 MongoDB 文档对象时会遇到一个经典问题spring-data-mongodb在反序列化SaSession时若dataMap字段无法被赋值会抛出类似如下错误Cannot set property dataMap because no setter, no wither and its not part of the persistence constructor public cn.dev33.satoken.session.SaSession()因此需要先创建一个MySaSession继承SaSession为其补充一个可供持久化框架调用的数据写入入口public class MySaSession extends SaSession { public MySaSession(String id) { super(id); } public void setDataMap(MapString, Object dataMap) { refreshDataMap(dataMap); } }从当前仓库源码看SaSession.java 中已提供setDataMap替换整个 map 引用与refreshDataMap清空旧数据后putAll新数据并调用update()将数据同步回持久层。自定义子类通过refreshDataMap间接完成数据回流同时确保底层ConcurrentHashMap引用不被替换既兼容了持久化框架的写入需求又不会破坏 Sa-Session 内部的并发安全设计。四、第三步重写 SaStrategy.instance.createSession让自定义 Session 生效SaStrategy是 Sa-Token 的策略对象框架默认通过SaStrategy.instance.createSession创建SaSession实例默认实现见 SaStrategy.java。要让框架在创建 Session 时使用我们的MySaSession需在 Spring Boot 启动入口中、run方法执行之前重写该策略SpringBootApplication public class SpringApplication { public static void main(String[] args) { // 重写 SaStrategy.instance.createSession 方法 SaStrategy.instance.createSession (sessionId) - { return new MySaSession(sessionId); }; SpringApplication.run(SpringApplication.class, args); } }说明示例中主类名为SpringApplication仅用于演示实际项目中建议使用与业务相关的命名避免与 Spring 框架自身类名混淆。重写必须发生在框架初始化之前即放在main方法中SpringApplication.run(...)调用之前。另外SaStrategy.java 中还提供了sessionClassType字段它决定了 Session 反序列化时的目标类型getSession默认实现正是依据它调用getObject(sessionId, classType)完成读取的与自定义子类方案配合使用效果最佳。五、第四步设计 SaTokenWrap 数据模型与 TTL 自动过期机制在实现SaTokenDao之前先定义一个 MongoDB 文档模型SaTokenWrap用于统一承载三类数据字符串value、对象object以及它们的过期时间。// 定义一个类用于保存 SaSession Document public class SaTokenWrap { private String id; private String value; private Object object; // 利用 MongoDB 的 TTL 索引过期时 MongoDB 会自动删除过期的数据 // 同时如果 timeout 为 null 则视为永不删除 Indexed(expireAfterSeconds 1, background true) private Date timeout; public boolean live() { return getTimeout() null || getTimeout().after(new Date()); } }设计要点TTL 索引实现自动过期Indexed(expireAfterSeconds 1, background true)会在timeout字段上建立 TTL 索引。MongoDB 后台线程每隔约 60 秒执行一次清理删除timeout字段时间点已过去的文档。expireAfterSeconds 1表示在timeout时间点之后再过 1 秒才可被清理属于容错性设置。永不过期语义当timeout字段为null时文档不参与 TTL 清理对应 Sa-Token 的NEVER_EXPIRE-1语义。双保险校验live()方法在读取时再次判断timeout是否已过期。由于 TTL 清理存在延迟约 60 秒一次应用层读取时必须显式过滤避免读到已过期但尚未被物理删除的脏数据。一个集合承载多种数据value存字符串Tokenobject存对象SaSession两者可同时存在例如一条记录的value是 token 值、object是对应的 session 对象。六、第五步完整实现 SaTokenDaoMongo核心类是SaTokenDaoMongo它通过MongoTemplate完成所有 CRUD并实现SaTokenDao接口// SaTokenDao 实现 Component public class SaTokenDaoMongo implements SaTokenDao { private final MongoTemplate mongoTemplate; public SaTokenDaoMongo(MongoTemplate mongoTemplate) { this.mongoTemplate mongoTemplate; } OptionalSaTokenWrap getByKey(String key) { SaTokenWrap tokenWrap mongoTemplate.findById(key, SaTokenWrap.class); return Optional.ofNullable(tokenWrap).filter(SaTokenWrap::live); } Date timeoutToDate(long timeout) { return new Date(timeout * 1000 System.currentTimeMillis()); } void upsertByPath(String key, String path, Object value, long timeout) { if (timeout 0 || timeout SaTokenDao.NOT_VALUE_EXPIRE) { return; } Update update Update.update(path, value); if (timeout ! SaTokenDao.NEVER_EXPIRE) { update.set(timeout, timeoutToDate(timeout)); } else { update.unset(timeout); } mongoTemplate.upsert( Query.query(Criteria.where(id).is(key)), update, SaTokenWrap.class ); } void updateByPath(String key, String path, Object value) { mongoTemplate.updateFirst( Query.query(Criteria.where(id).is(key).and(timeout).gte(new Date())), Update.update(path, value), SaTokenWrap.class ); } // ------------------------ String 读写操作 Override public String get(String key) { return getByKey(key).map(SaTokenWrap::getValue).orElse(null); } Override public void set(String key, String value, long timeout) { upsertByPath(key, value, value, timeout); } Override public void update(String key, String value) { updateByPath(key, value, value); } Override public void delete(String key) { mongoTemplate.remove(Query.query(Criteria.where(id).is(key))); } Override public long getTimeout(String key) { SaTokenWrap tokenWrap mongoTemplate.findById(key, SaTokenWrap.class); if (tokenWrap null) { return SaTokenDao.NOT_VALUE_EXPIRE; } if (tokenWrap.getTimeout() null) { return SaTokenDao.NEVER_EXPIRE; } long expire tokenWrap.getTimeout().getTime(); long timeout (expire - System.currentTimeMillis()) / 1000; // 小于零时视为不存在 if (timeout 0) { mongoTemplate.remove(Query.query(Criteria.where(id).is(key))); return SaTokenDao.NOT_VALUE_EXPIRE; } return timeout; } Override public void updateTimeout(String key, long timeout) { Update update new Update(); if (timeout SaTokenDao.NEVER_EXPIRE) { update.unset(timeout); } else { update.set(timeout, timeoutToDate(timeout)); } mongoTemplate.upsert( Query.query(Criteria.where(id).is(key)), update, SaTokenWrap.class ); } // ------------------------ Object 读写操作 Override public Object getObject(String key) { return getByKey(key).map(SaTokenWrap::getObject).orElse(null); } Override public void setObject(String key, Object object, long timeout) { upsertByPath(key, object, object, timeout); } Override public void updateObject(String key, Object object) { updateByPath(key, object, object); } Override public void deleteObject(String key) { delete(key); } Override public long getObjectTimeout(String key) { return getTimeout(key); } Override public void updateObjectTimeout(String key, long timeout) { updateTimeout(key, timeout); } // ------------------------ Session 读写操作 // 使用接口默认实现 // --------- 会话管理 Override public ListString searchData(String prefix, String keyword, int start, int size, boolean sortType) { ListSaTokenWrap wrapList mongoTemplate.find( Query.query(Criteria.where(id).regex(prefix * keyword *).and(timeout).gte(new Date())), SaTokenWrap.class ); ListString list wrapList.stream().map(SaTokenWrap::getValue).filter(StringUtils::hasText).collect(Collectors.toList()); return SaFoxUtil.searchList(list, start, size, sortType); } }6.1 四个核心辅助方法getByKey(key)按主键查询并通过SaTokenWrap::live过滤已过期但尚未被 TTL 清理的文档保证读取层永远拿不到过期数据。timeoutToDate(timeout)把 Sa-Token 的秒级剩余存活时间换算成绝对的过期时间点Datetimeout * 1000 当前毫秒时间戳该 Date 即 TTL 索引的判定依据。upsertByPath(key, path, value, timeout)写入的核心方法。path指明要更新的字段value或object一条记录中两者可分别独立写入。timeout 0 || timeout NOT_VALUE_EXPIRE按接口约定直接放弃存储timeout ! NEVER_EXPIRE同时写入timeout过期时间点timeout NEVER_EXPIREunset(timeout)使文档不参与 TTL 删除实现永久保存。updateByPath(key, path, value)更新指定字段但通过timeout.gte(new Date())条件确保只更新仍未过期的记录过期记录留给 TTL 清理。6.2 String 读写Token 数据落地get、set、update、delete分别对应 token 查询、写入、更新、删除。其中set复用upsertByPath存在即更新、不存在即插入update只改值不动过期时间这与 Sa-Token 对更新数据的语义过期时间不变保持一致。getTimeout是状态判断的关键文档不存在返回NOT_VALUE_EXPIRE-2timeout字段为 null 返回NEVER_EXPIRE-1否则计算剩余秒数若已小于 0则顺带删除这条过期记录并返回 -2。这样框架层可以准确区分数据已过期与数据永不过期两种状态。6.3 Object 读写SaSession 持久化Object 相关方法在思路上与 String 完全一致只是字段从value换成了object。特别值得注意的是Sa-Session 的六个方法这里全部使用接口默认实现委托到 Object 方法因此只要object字段能正确存取MySaSessionAccount-Session、Token-Session、Custom-Session 三类会话就全部自动获得了 MongoDB 持久化能力三类会话的存储格式是一致的详见 SaSession.java 的注释说明。6.4 searchData会话管理查询searchData用于支撑根据关键字搜索在线会话类功能如后台踢人、会话列表。实现要点用正则prefix * keyword *匹配id字段并附加timeout now过滤过期数据从命中文档中取出valuetoken 值过滤掉空字符串最后调用工具类SaFoxUtil.searchList(list, start, size, sortType)完成分页与排序内部实现见 SaFoxUtil.javasortType为 false 时先反转列表start小于 0 时归零。注意该方法取的是value字段因此要求搜索范围内的记录是以 String 形式存储的 Token 数据纯 Session 对象记录仅写入object不会出现在结果中这与 Sa-Token 默认实现的行为对 key 集合搜索在数据口径上略有差异实际使用时可根据业务需要调整。七、集成后的关键行为验证完成上述四步引入依赖、定义MySaSession、重写createSession、注册SaTokenDaoMongo为 Spring Bean后Sa-Token 的整套数据读写都会自动切换到 MongoDB框架行为MongoDB 中的落点登录生成 Tokensa_token_wrap集合中写入value字段timeout为过期时间点创建/读取 Sessionobject字段存放序列化后的MySaSession会话续期updateTimeout重新set过期时间点注销登录 / 踢人下线delete删除对应文档后台会话搜索searchData按 id 正则检索数据自然过期TTL 索引后台自动删除 读取时live()双保险几个容易踩坑的点TTL 清理有延迟MongoDB 的 TTL 监控线程默认每 60 秒运行一次过期文档不会到点即删应用层必须依赖live()/timeout.gte(now)这类条件过滤而不是指望物理删除。时间单位一致性Sa-Token 的 timeout 单位是秒MongoDB 的Date是毫秒级绝对时间timeoutToDate中的* 1000换算不可省略。永不过期与不存储的分界写入时 -1 表示永久unset timeout、0 或 ≤ -2 表示不存储直接 return实现时必须精确对齐 SaTokenDao.java 的常量约定否则框架的过期判断会出现偏差。反序列化类型Session 读取依赖SaStrategy.instance.sessionClassType默认为SaSession.class若你希望反序列化时直接还原为MySaSession可结合自定义子类与getSession的默认实现自行控制目标类型。八、小结本文完整演示了在 Spring Boot 下把 Sa-Token 数据持久化到 MongoDB 的全部步骤引入spring-boot-starter-data-mongodb→ 创建MySaSession子类规避dataMap反序列化问题 → 在启动入口重写SaStrategy.instance.createSession→ 设计带 TTL 索引的SaTokenWrap文档模型 → 实现完整的SaTokenDaoMongo。整套方案无需修改框架源码仅靠实现一个接口、注册一个 Spring Bean 即可完成也让应用获得了数据可持久化、多实例可共享的能力。如果你想进一步了解基于expireAt字段 独立包装类的另一套 MongoDB 集成写法可继续阅读 integ-spring-mongod-1.md关于SaTokenDao接口的更多扩展思路Redis、Caffeine 等实现可参考 dao-extend.md 与 sa-token-dao.md。【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考