资讯动态

Sa-Token 集成 MongoDB(二):Spring Data MongoDB 反序列化 SaSession 与 SaTokenDao 完整实现

发布时间:2026/9/13 18:37:04 来源:尧图企业网站定制
Sa-Token 集成 MongoDB二Spring Data MongoDB 反序列化 SaSession 与 SaTokenDao 完整实现【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token导读本文基于 Sa-Token 官方文档「集成 MongoDB 参考二」展开深入讲解在 Spring Boot 环境下通过Spring Data MongoDB将 Sa-Token 的 Token、Session 等数据持久化到 MongoDB 的完整方案。与「参考一」直接存储SaSession对象的思路不同本方案以 String / Object 两种基础类型存储为骨架专门解决 Spring Data MongoDB 反序列化SaSession时因缺少dataMapsetter 而抛出的异常并利用 MongoDB 原生TTL 索引实现数据的自动过期清理。读完本文你将掌握自定义 SaSession 子类规避反序列化陷阱、重写SaStrategy.instance.createSession让自定义 Session 生效、以及完整实现SaTokenDao接口接入 MongoDB 的工程化写法。一、集成前的先决条件与依赖引入本方案以Spring Boot Spring Data MongoDB为运行前提数据持久化基于MongoTemplate完成。首先需要引入官方 Starter 依赖:::tabs Maven 方式!-- 提供MongoDB依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-mongodb/artifactId /dependency Gradle 方式// 提供MongoDB依赖 implementation org.springframework.boot:spring-boot-starter-data-mongodb:::引入依赖后Spring Boot 会自动装配MongoTemplate后续的SaTokenDao实现将通过构造器注入使用它。二、问题背景SaSession 反序列化为什么会失败Spring Data MongoDB 在读取文档并映射回 Java 对象时使用的是基于属性的持久化Property-based Persistence目标类型必须提供对应的 setter、wither或者该属性属于持久化构造器的参数否则会抛出类似下面的异常Cannot set property dataMap because no setter, no wither and its not part of the persistence constructor public cn.dev33.satoken.session.SaSession()查看SaSession的源码sa-token-core/src/main/java/cn/dev33/satoken/session/SaSession.javaprivate MapString, Object dataMap new ConcurrentHashMap();dataMap字段在类中既没有对应名称的 setter只有setDataMap(Map)和refreshDataMap(Map)两个方法见该文件 L568-L580也没有被包含进任何持久化构造器参数因此当 MongoDB 需要将dataMap反序列化回对象时就会失败。文档给出的解决思路是创建一个SaSession的子类在其中补充一个严格匹配dataMap属性名的 setter从而满足 Spring Data MongoDB 的属性映射要求。三、步骤一创建自定义 MySaSession 子类继承SaSession并新增一个与字段名严格对应的 setterpublic class MySaSession extends SaSession { public MySaSession(String id) { super(id); } public void setDataMap(MapString, Object dataMap) { refreshDataMap(dataMap); } }这里有一个容易踩坑的细节refreshDataMap与setDataMap的行为并不相同。从源码看SaSession.javasetDataMap(Map)改变底层对象引用将this.dataMap整体替换为新 MaprefreshDataMap(Map)不改变底层对象引用先clear()再putAll()原地替换所有数据。自定义 setter 内部调用refreshDataMap而非直接赋值可以保持内部ConcurrentHashMap实例不变避免并发环境下引用被整体替换带来的读写一致性问题这也是文档示例的推荐写法。四、步骤二重写 SaStrategy.instance.createSession仅有自定义子类还不够Sa-Token 框架默认创建 Session 时使用的是内置的SaSession。框架的创建策略定义在SaStrategy中sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.javapublic SaCreateSessionFunction createSession (sessionId) - { return new SaSession(sessionId); };因此需要在 Spring Boot 启动入口处将createSession策略重写为返回我们的MySaSessionSpringBootApplication 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); } }这样框架在新建 Account-Session / Token-Session 时都会经由该策略工厂产出MySaSession实例。需要说明的是示例中主类名为SpringApplication是为了简洁演示实际项目中请使用你自己的启动类类名。五、步骤三实现 SaTokenDao 接入 MongoDBSaTokenDao是 Sa-Token 定义的数据持久层接口sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDao.java不同实现类可将数据存储至不同位置如内存 Map、Redis 等自定义存储策略例如本文的 MongoDB也需要通过实现此接口完成。5.1 理解 SaTokenDao 的常量与三类读写能力在动手实现前先掌握接口中两个关键常量常量值语义NEVER_EXPIRE-1永不过期NOT_VALUE_EXPIRE-2系统中不存在该缓存对不存在的 key 获取剩余存活时间时返回接口同时定义了字符串读写get/set/update/delete/getTimeout/updateTimeout、对象读写getObject/setObject/updateObject/deleteObject/getObjectTimeout/updateObjectTimeout、Session 读写getSession/setSession/updateSession/deleteSession/getSessionTimeout/updateSessionTimeout默认复用对象读写方法以及搜索searchData和生命周期init/destroy共四大类能力。timeout参数统一语义为值大于 0 时限时存储值 -1 时永久存储值 0 或小于等于 -2 时不存储。5.2 定义 MongoDB 文档模型 SaTokenWrap首先定义一个Document注解的实体类用于在 MongoDB 中保存 Sa-Token 的数据// 定义一个类用于保存 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()); } }设计要点用id存储 Sa-Token 的 keyvalue存储 String 类型数据如 token 值object存储对象类型数据如 SaSession核心是timeout字段上的TTL 索引Indexed(expireAfterSeconds 1)MongoDB 的 TTL 后台线程会定期扫描并删除过期文档从而天然实现数据自动清理无需应用层定时任务timeout为null时表示该记录永不删除对应 Sa-Token 的「永久存储」语义live()方法用于读取时判断记录是否仍存活永不过期null或未到过期时间即为有效。注意expireAfterSeconds 1表示 TTL 线程实际清理存在最长约 1 秒的延迟因此读取时必须配合live()做逻辑过滤避免读到「物理上尚未被 TTL 删除、但逻辑上已过期」的数据。5.3 完整实现 SaTokenDaoMongo// 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 基础读写upsert 与 update 的分工实现将 String 和 Object 两类数据统一映射到同一个SaTokenWrap文档通过path参数区分写入value还是object字段upsertByPath用于「写入 / 覆盖」set、setObject不存在则插入存在则更新。写入时根据 timeout 语义决定设置还是移除timeout字段updateByPath用于「仅更新值、不改变过期时间」update、updateObject查询条件额外追加and(timeout).gte(new Date())即只更新尚未过期的记录过期记录自然跳过。timeout字段的写入规则值得单独强调timeout 取值对timeout字段的操作含义timeout 0 或 timeout NOT_VALUE_EXPIRE(-2)直接 return不执行任何写入不存储该数据timeout NEVER_EXPIRE(-1)update.unset(timeout)永久存储TTL 索引不删除timeout 0update.set(timeout, timeoutToDate(timeout))限时存储到期由 TTL 清理6.2 剩余存活时间getTimeout 的三种返回分支if (tokenWrap null) { return SaTokenDao.NOT_VALUE_EXPIRE; // 记录不存在-2 } if (tokenWrap.getTimeout() null) { return SaTokenDao.NEVER_EXPIRE; // 永不过期-1 } // 计算剩余秒数小于零视为不存在并顺手物理删除这种分支设计完整对齐了SaTokenDao接口的常量契约SaTokenDao.java是框架内部判断「是否需要续期、是否已过期」的依据。6.3 Session 读写直接复用接口默认实现注意代码注释「// 使用接口默认实现」——SaTokenDaoMongo没有单独重写getSession/setSession/updateSession/deleteSession/getSessionTimeout/updateSessionTimeout六个方法而是依赖SaTokenDao接口中的默认实现SaTokenDao.java它们内部默认复用 Object 读写方法。这意味着 Session 数据会以Object类型落到SaTokenWrap.object字段中。也正因为 Session 要经过 Object 反序列化才有了前文自定义MySaSession的必要性——两处设计环环相扣。6.4 searchData模糊搜索与分页排序searchData是框架用于「根据关键字搜索 Token 列表」等会话管理场景的方法使用 MongoDB 正则prefix * keyword *对id字段做模糊匹配并限定只查未过期记录从结果中取出value即 token 值过滤空串最后交给SaFoxUtil.searchList(list, start, size, sortType)完成分页与排序start为开始索引size为获取数量-1代表取到末尾sortType为true正序、false反序。七、两种 MongoDB 集成方案的取舍本仓库文档目录下同时存在两篇 MongoDB 集成参考integ-spring-mongod-1.md 与 integ-spring-mongod-2.md二者的核心差异在于对比项参考一参考二本文存储粒度单个SaTokenMongoData文档同时持有string与session字段String 存value字段、Object 存object字段读写路径分开SaSession 处理文档字段直接声明为SaSession类型通过自定义MySaSession 重写createSession策略解决反序列化过期机制Indexed(expireAfterSeconds 0)的LocalDateTime expireAtIndexed(expireAfterSeconds 1)的Date timeout并用live()过滤适合场景少量改造即可完成集成代码更少结构更清晰、读写路径更聚焦适合对 Session 反序列化有强控制需求的场景八、总结与注意事项至此一个基于 MongoDB 的 Sa-Token 数据持久层就完整落地了。工程落地时还需注意以下几点数据模型必须使用Document并显式声明IndexedTTL 索引由 MongoDB 首次启动时自动创建若集合已存在且索引变更需要人工调整索引或换集合名TTL 清理存在延迟业务读取务必走getByKey内部filter(SaTokenWrap::live)做逻辑过期判断getTimeout中也要对「剩余时间为负」做兜底删除Session 反序列化问题是集成 MongoDB 的核心坑点如果项目实际并未把 Session 存入 MongoDB例如仅持久化 token 字符串则可以跳过MySaSession与createSession重写两步接口未强制要求的默认方法Session 读写、init/destroy可按需覆盖示例中采用「使用接口默认实现」的写法已能满足基本功能本文代码示例可直接对照 SaTokenDao 接口定义 与 SaStrategy 策略类 阅读理解每个方法在框架内部的调用时机便于后续按需增强如支持getObject(key, Class)指定反序列化类型等接口新能力。【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价