Sa-Token 缓存层扩展实战:实现 SaTokenDao 接口对接 Redis、MongoDB 等持久化中间件
【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架,让鉴权变得简单、优雅!—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token
对于权限认证框架而言,最容易碰到的扩展点就是数据存储方式:默认实现把 Token、Session 都放在 JVM 内存中,重启即丢、无法多节点共享。Sa-Token 将所有数据持久化操作抽象到了SaTokenDao接口,开发者只需实现该接口(或基于框架提供的次级接口做少量实现),即可将会话数据对接到 Redis、MongoDB、Caffeine 等任意存储介质上。读完本文,你将掌握SaTokenDao的完整方法契约、timeout 取值语义、框架"跟随式"次级接口的设计,以及官方 Redis 集成包与自定义 MongoDB 实现的源码级细节,并知道如何选型官方已提供的 9 类集成方案。
一、为什么需要缓存层扩展:默认内存方案的边界
Sa-Token 的登录态由两类数据构成:Token 对应的字符串数据,以及SaSession会话对象。默认情况下它们存储在内存缓存中——读写速度最快,且避免了序列化与反序列化的性能消耗,但存在两个固有限制:
- 系统重启后数据丢失:所有登录态、Session 数据清零,用户被迫重新登录;
- 无法在分布式环境中共享数据:多节点部署时,A 节点登录的会话 B 节点无法识别。
为此,Sa-Token 把全部持久化操作收敛到一个接口中。对接新的存储介质,只需要提供该接口的一个实现,无需改动任何业务代码。接口定义见 SaTokenDao.java,其 Javadoc 明确指出:"此接口的不同实现类可将数据存储至不同位置,如:内存Map、Redis 等等。如果你要自定义数据存储策略,也需通过实现此接口来完成。"
二、SaTokenDao 接口全解:方法契约与超时语义
SaTokenDao定义了四组方法,覆盖了会话数据的全生命周期操作:
2.1 字符串读写(最底层的六件套)
| 方法 | 语义 |
|---|---|
String get(String key) | 获取 value,如无返空 |
void set(String key, String value, long timeout) | 写入 value 并设定存活时间(单位:秒) |
void update(String key, String value) | 更新 value,过期时间不变 |
void delete(String key) | 删除 value |
long getTimeout(String key) | 获取 value 的剩余存活时间(单位:秒) |
void updateTimeout(String key, long timeout) | 修改剩余存活时间(单位:秒) |
2.2 对象读写
getObject(key)、getObject(key, classType)、setObject(key, object, timeout)、updateObject、deleteObject、getObjectTimeout、updateObjectTimeout,方法与字符串组一一对应,但操作的是可序列化的对象(例如SaSession)。
2.3 SaSession 读写
getSession、setSession、updateSession、deleteSession、getSessionTimeout、updateSessionTimeout。这组方法在接口层面有默认实现,复用对象读写(详见下文"跟随式"次级接口)。
2.4 会话管理与生命周期
List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType):按前缀+关键字搜索 key 集合,用于踢人下线、按条件查询会话等场景。size传 -1 表示从start处一直取到末尾;default void init()/default void destroy():实例被装载/卸载时的生命周期钩子,内存型实现用它来启停过期数据的定时清理线程。
2.5 timeout 参数的取值语义
接口的 Javadoc 与 Redis 官方实现的注释共同定义了 timeout 的完整取值规则,实现时务必严格遵守:
| 取值 | 含义 |
|---|---|
timeout > 0 | 限时存储,单位为秒 |
timeout == SaTokenDao.NEVER_EXPIRE(即-1) | 永久存储 |
timeout == 0或<= SaTokenDao.NOT_VALUE_EXPIRE(即≤ -2) | 不存储(直接 return) |
另外,getTimeout对不存在的 key 应返回NOT_VALUE_EXPIRE(-2),实现方可据此区分"key 不存在"与"永不过期"(-1)。这两个常量定义于 SaTokenDao.java。
三、"跟随式"次级接口:实现时到底要写几个方法
完整实现SaTokenDao需要写 20 个方法,工作量大且容易出错。为此框架在dao.auto包中提供了三层继承结构的次级接口,全部采用 default 方法"向下折叠",让实现方只写真正需要持久化的那一层:
- SaTokenDaoByObjectFollowString:Object 读写跟随 String 读写。接口注释明确标注"推荐中间件型缓存实现 implements 此接口"。它借助
SaManager.getSaSerializerTemplate()完成 String↔Object 的 JSON 序列化转换,实现方只需实现字符串组 6 个方法 +searchData; - SaTokenDaoByStringFollowObject:String 读写跟随 Object 读写,注释标注"推荐内存型缓存实现 implements 此接口"。内存缓存直接存对象即可,省去序列化开销,实现方只需实现对象组;
- SaTokenDaoBySessionFollowObject:SaSession 读写跟随 Object 读写。
getSession默认委托给getObject(sessionId, SaStrategy.instance.sessionClassType)——会话类的类型是可被策略(Strategy)替换的,这是 Sa-Token 可定制性设计的体现。
从源码结构看,继承链为SaTokenDao → SaTokenDaoBySessionFollowObject → (ByStringFollowObject | ByObjectFollowString),两者最终都实现了完整的SaTokenDao契约。选择原则很简单:往中间件(Redis、MongoDB 等)里写的数据天然是字符串,就实现SaTokenDaoByObjectFollowString;纯内存缓存直接存 Java 对象,就实现SaTokenDaoByStringFollowObject。
四、默认内存实现与官方集成包全家福
默认实现 SaTokenDaoDefaultImpl 位于 core 核心包中,基于SaTimedCache(底层为两个ConcurrentHashMap包装)存储字符串与对象,实现了SaTokenDaoByStringFollowObject;init()时启动定时刷新线程清理过期数据,destroy()时结束该线程。正如类注释所述,它是"内存缓存,系统重启后数据丢失"。
框架已提供的集成包包括(对应源码均位于sa-token-plugin/目录):
- 默认方式:储存在内存中,位于 core 核心包(SaTokenDaoDefaultImpl.java);
- sa-token-redis-template:Redis Template 集成包,可用环境为 SpringBoot2、SpringBoot3、SpringBoot4(SaTokenDaoForRedisTemplate.java);
- sa-token-redis-template-jdk-serializer:Redis 集成包,使用 JDK 默认序列化方式;
- sa-token-hutool-timed-cache:集成 hutool 框架的 Timed-Cache 缓存方案(基于内存);
- sa-token-caffeine:集成 Caffeine 缓存方案(基于内存,SaTokenDaoForCaffeine.java);
- sa-token-redisson:集成 Redisson 客户端;
- sa-token-redisson-spring-boot-starter:集成 Redisson 客户端的 SpringBoot 自动配置包;
- sa-token-alone-redisson:独立 Redisson 连接,权限缓存与业务缓存分离;
- sa-token-redisx:Redisx 集成包。
以 Caffeine 方案为例,SaTokenDaoForCaffeine 与默认实现结构一致:仍实现SaTokenDaoByStringFollowObject,只是把底层的 Map 包装替换为SaMapPackageForCaffeine,可见内存型方案之间"换底不换形"的扩展模式。
注意(Redisson 升级兼容):
SaTokenDaoForRedisson默认使用StringCodec。从旧版本升级时,请先清空 Redis 中的 Sa-Token 缓存,或构造时传入Kryo5Codec以兼容旧数据,详见 集成 Redis - 集成 Redisson。
五、源码深读:Redis 实现的关键细节
SaTokenDaoForRedisTemplate 是理解"如何实现一个中间件型 SaTokenDao"的最佳范本,其实现细节值得逐一拆解:
1. 只实现字符串层。类签名为implements SaTokenDaoByObjectFollowString, SaTokenDao——全部对象/Session 读写由 default 方法折叠到get/set/update/delete/getTimeout/updateTimeout+searchData这 7 个方法上。
2. 惰性初始化。init(RedisConnectionFactory)通过@Autowired注入,内部用isInit标记防止重复初始化,构建StringRedisTemplate后调用afterPropertiesSet()。
3. timeout 语义的严格落地。set方法开头即判断if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) return;,命中NEVER_EXPIRE(-1)时执行无过期时间的set,否则按秒设置 TTL——与接口契约完全一致。
4. update 的 KEEPTTL 实现。update要求"值更新但过期时间不变",官方实现采用 Redis 6.0+ 的SET key value XX KEEPTTL(见 setStringAndKeepTTL 方法),且XX选项保证仅当 key 存在时才覆写。前提:Redis 版本必须 ≥ 6.0。类尾部注释还贴心地给出了低于 6.0 的兼容写法(先读 TTL 再带 TTL 重写)。
5. updateTimeout 的"永久化"处理。传入 -1 时,若当前已为永久则不作处理;否则读取现值后重新set一次以去除 TTL。这一"先查后写"的逻辑在 MongoDB 参考实现中被原样复用,是各实现必须处理的边界。
6. 基于 SCAN 的 searchData。searchData使用ScanOptions(count=1000)游标遍历代替阻塞式的KEYS,先对prefix + "*" + keyword + "*"整体做wrapKey包装再执行 scan,最后交给SaFoxUtil.searchList完成分页与排序——这是生产环境可用的扫描式搜索范本。
7. wrapKey 扩展点。wrapKey(String key)默认原样返回,注释说明"需要给 Redis 键加统一前缀时,可重写此方法",方便多系统共用一个 Redis 时做键隔离。
该实现的正确性由单元测试覆盖:SaTokenDaoForRedisTemplateTest 与 SaTokenDaoForRedisTemplateSearchDataTest;Caffeine 方案同样配有 SaTokenDaoForCaffeineTest。
六、自定义实现实战:将 Session 持久化到 MongoDB
对于官方未提供集成包的介质(如 MongoDB),可以完全自定义SaTokenDao实现。仓库文档 集成 MongoDB 参考一(另见 集成 MongoDB 参考二)给出了完整示例,先决条件为 Spring Boot 3 + Spring Data MongoDB,引入spring-boot-starter-data-mongodb依赖即可。
第一步:定义数据包装类,利用 MongoDB TTL 索引实现自动清理过期数据:
@Document("saTokenMongo") // 你也可以自定义集合名称 public class SaTokenMongoData { @Id private String id; // token @Indexed(unique = true) private String key; // sa-token 的 session private SaSession session; // sa-token 的 token string private String string; // 给 expireAt 添加 @Indexed(expireAfterSeconds = 0),过期时 MongoDB 自动删除 @SuppressWarnings("removal") @Indexed(expireAfterSeconds = 0) private LocalDateTime expireAt; // 忽略 getter setter }第二步:实现 SaTokenDao。示例类SaTokenMongoDao仿照官方 Redis 集成实现,核心方法要点如下(完整代码见上述文档):
@Component public class SaTokenMongoDao implements SaTokenDao { private final MongoTemplate mongoTemplate; public SaTokenMongoDao(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } private Query keyQuery(String key) { return Query.query(Criteria.where("key").is(key)); } @Override public String get(String key) { return Optional.ofNullable(mongoTemplate.findOne(keyQuery(key), SaTokenMongoData.class)) .map(SaTokenMongoData::getString).orElse(null); } // timeout == NEVER_EXPIRE 时 expireAt 置 null,MongoDB 不会删除该记录 LocalDateTime getExpireAtFromTimeout(long timeout) { return timeout == SaTokenDao.NEVER_EXPIRE ? null : LocalDateTime.now().plusSeconds(timeout); } @Override public void set(String key, String value, long timeout) { if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } mongoTemplate.upsert( keyQuery(key), Update.update("string", value).set("expireAt", getExpireAtFromTimeout(timeout)), SaTokenMongoData.class ); } @Override public void updateTimeout(String key, long timeout) { // 与 Redis 实现相同的"永久化"边界处理: // 已永久则不处理;否则读取现值重新 set 一次 if (timeout == SaTokenDao.NEVER_EXPIRE) { long expire = getTimeout(key); if (expire == SaTokenDao.NEVER_EXPIRE) { // 已经被设置为永久,不作任何处理 } else { this.set(key, this.get(key), timeout); } return; } mongoTemplate.upsert( keyQuery(key), Update.update("expireAt", getExpireAtFromTimeout(timeout)), SaTokenMongoData.class ); } // 其余 getTimeout / searchData(正则匹配 + skip/limit 分页)等方法见完整文档 }该示例验证了SaTokenDao契约的可移植性:timeout 三值语义(>0 限时 / -1 永久 / 0 或 ≤-2 不存储)、update的"不改过期时间"约定、updateTimeout(-1)的永久化边界,在 Redis、MongoDB 等不同介质上的落地方式高度一致——这正是接口抽象带来的工程红利。
七、选型建议与版本注意事项
结合仓库实际情况,给出选型决策路径:
- 想省心:直接使用
sa-token-redis-template或sa-token-redisson-spring-boot-starter,依赖 + 自动装配即可,详细步骤见 集成 Redis; - 权限缓存与业务缓存要隔离:选
sa-token-alone-redisson(独立 Redisson 连接); - 单机/演示环境、不想依赖外部中间件:默认内存实现或
sa-token-caffeine、sa-token-hutool-timed-cache(均基于内存,重启丢数据,适合非多节点场景); - 非 Redis 介质:参照第五、六节的模式自定义实现,优先选择正确的次级接口(中间件型选
SaTokenDaoByObjectFollowString,内存型选SaTokenDaoByStringFollowObject)。
版本与环境的适用前提需注意:
sa-token-redis-template/sa-token-redisson自 v1.46.0 起使用了 Redis 6.0+ 的SET KEEPTTL特性,Redis 服务低于 6.0 会报ERR syntax error(兼容写法见 SaTokenDaoForRedisTemplate 尾部注释);- Redisson 方案升级时的 Codec 兼容问题(
StringCodecvsKryo5Codec)见第一节"注意"; - 往 Session 存自定义实体类后从 Redis 读回若报"无法反序列化的类型",需要先配置 JSON 全局类型白名单。
八、小结
SaTokenDao是 Sa-Token 面向数据存储的核心扩展点:接口以"字符串 + 对象 + Session + 搜索 + 生命周期"五组方法定义了完整契约,timeout 采用>0 / -1 / 0或≤-2三值语义;三层"跟随式"次级接口让中间件型实现只需编写 7 个方法;官方已提供内存、RedisTemplate、Redisson、Caffeine、Hutool Timed-Cache 等 9 类集成方案,自定义 MongoDB 等介质的实现路径也可由 Redis 范本与官方参考文档直接推导。理解并掌握这一扩展点,即可让 Sa-Token 的会话存储适配任意基础设施,兼顾重启不丢失与分布式一致性。
【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架,让鉴权变得简单、优雅!—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考