登录注册番外篇(一) - 2026 年最新 Sa-Token 由浅入深快速开始


第一章. 回头看:我们造了多少轮子

阶段式学习路径

本篇是番外篇系列的第一站。在基础篇中,我们花了六章的篇幅,从零手写了一套完整的认证内核——RSA 加密、JJWT 双令牌、Redis 黑名单、多设备管理。现在,我们暂停主线,换一个全新的视角:如果有一个框架,能用一行代码完成登录,我们还需要手写那么多东西吗?

若你对登录注册系列基础篇感兴趣,请跳转至:

本篇将带你认识 Sa-Token 框架,搭建全新的独立项目,并体验它的核心登录 API 与 Redis 集成能力。

在正式认识 Sa-Token 之前,我们先做一件事——回头看看基础篇六章走下来,我们到底写了多少代码。

这不是为了否定之前的努力。恰恰相反,正是因为我们亲手造过这些轮子,才能真正理解框架帮我们省掉了什么,以及它在背后做了哪些我们曾经手动完成的事情。

1.1. 基础篇的六大手写模块回顾

让我们快速盘点一下基础篇的产出。六章内容,我们在 auth 项目中构建了一个三模块的认证系统:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
auth/
├── auth-common/ # 公共基础模块
│ ├── Result.java # 统一响应封装
│ └── SnowflakeIdGenerator.java # 雪花算法 ID 生成器
├── auth-core/ # 认证核心模块
│ ├── RsaKeyManager.java # RSA 密钥加载与管理
│ ├── JwtUtil.java # JJWT 0.12 Token 生成与解析
│ ├── JwtProperties.java # JWT 配置属性绑定
│ ├── RedisKeyConstants.java # Redis Key 命名常量
│ ├── TokenRedisManager.java # Token 存储(ZSet 实现)
│ ├── BlacklistRedisManager.java # 黑名单管理(String + TTL)
│ ├── AuthToken.java # 双令牌模型
│ ├── DeviceInfo.java # 设备信息模型
│ ├── DeviceInfoExtractor.java # 设备信息提取工具
│ └── AuthService.java # 认证 Facade 服务
├── auth-web/ # Web 应用模块
│ ├── AuthApplication.java # 启动类
│ └── AuthController.java # 认证控制器
└── resources/
├── application.yml
└── certs/
├── private_key.pem # RSA 私钥
└── public_key.pem # RSA 公钥

14 个 Java 源文件,2 个密钥文件,1 个配置文件。这还只是一个"登录注册"功能——没有涉及权限校验,没有涉及角色管理,没有涉及路由拦截。


1.2. 手写轮子的三大代价

基础篇的每一行代码都有它的教学价值,但如果把这套代码直接搬进生产环境,我们会面临三个现实问题。

代价一:维护成本远超预期

JwtUtil 需要处理 Token 过期、签名验证、Claims 解析等逻辑。TokenRedisManager 需要管理 ZSet 的 score 计算、过期清理、Pipeline 批量操作。BlacklistRedisManager 需要精确计算每个 Token 的剩余 TTL。这些工具类一旦出现 Bug,排查成本很高——因为没有社区帮你验证,所有边界情况都需要自己覆盖。

代价二:安全性依赖个人经验

我们手写的 RSA 密钥加载逻辑、Token 生成规则、黑名单过期策略,都是基于个人对安全的理解来实现的。但安全领域有一条铁律:不要自己发明加密方案。一个成熟的框架背后有数千个 Issue 和 PR 的打磨,这种安全性是个人项目很难达到的。

代价三:功能扩展举步维艰

现在产品经理提了一个需求:“同一个账号在手机端和电脑端可以同时登录,但同一端只能有一个设备在线。”

回忆一下基础篇的实现——我们需要修改 TokenRedisManager 的 ZSet 逻辑,修改 AuthService 的登录方法,修改 DeviceInfoExtractor 的设备识别规则,还要在 AuthController 中新增接口。一个需求牵动四个类,这就是手写轮子的扩展代价。手写轮子的价值在于理解原理,但生产环境中,我们需要站在巨人的肩膀上。


第二章. 认识 Sa-Token:一行代码解决登录

基础篇让我们深刻体会到了手写认证系统的复杂度。那么 Java 生态中,有没有一个框架能大幅降低这种复杂度,同时又不像 Spring Security 那样需要理解一整套过滤器链才能上手?

答案是 Sa-Token。

2.1. Sa-Token 是什么

Sa-Token轻量级 Java 权限认证框架,是 dromara国内知名开源社区 开源社区下的一个项目,目前在 GitHub 上拥有超过 18,000 个 Star。它的官方定位是:

一个轻量级 Java 权限认证框架,让鉴权变得简单、优雅。

这句话的关键词是"轻量级"和"简单"。我们可以通过一个最直观的例子来感受——在 Sa-Token 中,完成用户登录只需要一行代码:

1
2
// 参数是用户 ID,可以是 long、int、String 类型
StpUtil.login(10001);

没有 JwtUtil,没有 RsaKeyManager,没有 TokenRedisManager。一行代码,框架自动完成了 Token 生成、Session 创建、Cookie 写入(或响应头返回)等全部工作。

让我们看看 Sa-Token 的五大核心模块,对它的能力范围建立一个全局认知:

image-20260208211535983

模块解决的问题本系列是否覆盖
登录认证用户登录、注销、Token 管理、多设备登录✅ 本篇开始
权限认证角色校验、权限码校验、注解鉴权、路由拦截✅ 后续篇章
单点登录(SSO)多系统共享登录态❌ 不在本系列范围
OAuth2.0第三方授权登录❌ 进阶篇单独讲解
微服务网关鉴权Gateway 层统一鉴权❌ 不在本系列范围

Sa-Token 的设计哲学是"一个方法解决一个问题",API 命名直白到几乎不需要查文档。


2.2. 为什么选 Sa-Token 而不是 Spring Security

你可能会想:Java 安全领域的"正统"不是 Spring Security 吗?为什么我们先讲 Sa-Token?

这个问题很好,我们从三个维度来回答。

维度一:学习曲线

Spring Security 的核心是一条 过滤器链由多个安全过滤器串联组成的请求处理链。要理解它,你需要先搞清楚 SecurityFilterChainAuthenticationManagerAuthenticationProviderUserDetailsServiceSecurityContextHolder 这一整套概念,然后才能写出第一个自定义登录逻辑。

Sa-Token 的学习路径则完全不同。它没有过滤器链的概念,所有操作都通过一个静态工具类 StpUtil 完成。你不需要理解任何底层架构,打开 API 文档,找到你需要的方法,直接调用就行。

官方文档链接请跳转至:https://sa-token.cc/doc.html#/

学习曲线对比
2026-01-01 10:00
S

老师,Spring Security 和 Sa-Token 上手难度差多少?

T
teacher

打个比方,Spring Security 像是一辆手动挡赛车,性能强悍但你得先学会换挡。Sa-Token 像是一辆自动挡家用车,上车就能开。

S

那 Sa-Token 是不是功能比较弱?

T
teacher

不是。它覆盖了登录认证、权限校验、多设备管理、踢人下线等绝大多数场景。只是在 OAuth2 和微服务网关这种重度企业场景下,Spring Security 的生态更成熟。

维度二:代码量

我们在基础篇中用了 14 个 Java 文件来实现登录认证。使用 Sa-Token 之后,同样的功能大约只需要 3-4 个文件。这不是因为 Sa-Token “偷工减料”,而是因为它把 Token 生成、Session 管理、Redis 存储这些通用逻辑全部内置了,我们只需要关注业务本身。

维度三:适用场景

Sa-Token 最适合的场景是:

  • 中小型项目的快速开发
  • 前后端分离架构
  • 需要快速原型验证的团队
  • 不想被框架"绑架"的开发者(Sa-Token 的侵入性极低)

而 Spring Security 更适合:

  • 需要深度定制安全策略的企业级项目
  • 已经深度使用 Spring 生态的团队
  • 需要 OAuth2 Authorization Server 的场景

番外篇的后续章节会专门讲解 Spring Security,届时我们会做更深入的对比。


第三章. 环境搭建与项目初始化

认识了 Sa-Token 的定位和能力之后,是时候动手了。本章我们将从零创建一个全新的独立项目,引入 Sa-Token 的核心依赖,并完成基础配置。

和基础篇不同,这次我们不再使用多模块架构。Sa-Token 的一大优势就是轻量,我们用一个单模块项目就能承载所有功能,让你把注意力完全放在框架本身。

3.1. 新建 Maven 项目

我们创建一个名为 auth-satoken 的全新项目,与基础篇的 auth 项目完全独立。

步骤 1:通过 IDEA 创建项目

打开 IntelliJ IDEA,选择 File → New → Project,在左侧选择 Spring Initializr,填写以下信息:

  • Nameauth-satoken
  • Groupcom.example
  • Artifactauth-satoken
  • Type:Maven
  • Language:Java
  • JDK:17
  • Java:17
  • Packaging:Jar

在依赖选择页面,勾选以下两项:

  • Spring Web
  • Spring Data Redis

点击 Create 完成创建。

如果你更习惯手动创建,也可以访问 https://start.spring.io 生成项目骨架后导入 IDEA。

步骤 2:确认项目结构

创建完成后,项目的初始结构如下:

1
2
3
4
5
6
7
8
9
auth-satoken/
├── pom.xml
└── src/
└── main/
├── java/
│ └── com/example/authsatoken/
│ └── AuthSatokenApplication.java
└── resources/
└── application.yml

步骤 3:引入 Sa-Token 和 Hutool 依赖

打开 pom.xml,在 <dependencies> 节点中追加以下依赖:

📄 文件:pom.xml(修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- Sa-Token 权限认证(SpringBoot3 专用 Starter) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.44.0</version>
</dependency>

<!-- Sa-Token 整合 Redis(使用 Jackson 序列化) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.44.0</version>
</dependency>

<!-- Redis 连接池(Sa-Token Redis 插件的底层依赖) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

<!-- Hutool 工具库(提供加密、JSON、字符串等常用工具) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.34</version>
</dependency>

这里有几个要点需要说明:

sa-token-spring-boot3-starter 是 Sa-Token 为 SpringBoot 3.x 提供的专用启动器。如果你使用的是 SpringBoot 2.x,需要将 boot3 改为 boot,即 sa-token-spring-boot-starter。两者的 API 完全一致,只是底层适配的 Servlet 版本不同。

sa-token-redis-jackson 是 Sa-Token 的 Redis 集成插件。引入这个依赖后,Sa-Token 会自动将会话数据存储到 Redis 中,不需要我们写任何配置代码。它依赖 commons-pool2 作为 Redis 连接池,所以需要一并引入。

hutool-all 是 Hutool 的全量包。在后续章节中,我们会用到它的加密工具、JSON 处理等能力。如果你之前没有接触过 Hutool,不用担心——我们不会专门讲解它的 API,用到的时候会直接说明用途。

追加完成后,点击 IDEA 右侧的 Maven 面板,点击刷新图标(Reload All Maven Projects)。等待底部进度条走完,确认 pom.xml 中没有红色报错线,说明依赖下载成功。


3.2. 配置 application.yml

依赖引入完成后,我们需要在 application.yml 中完成两件事:配置 Sa-Token 的核心参数,以及配置 Redis 连接信息。

📄 文件:src/main/resources/application.yml(新建)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
server:
port: 8081 # 使用 8081 端口,避免与基础篇的 8080 冲突

# Sa-Token 核心配置
sa-token:
# Token 名称(同时也是 Cookie 名称和请求头名称)
token-name: satoken
# Token 有效期,单位:秒。-1 表示永不过期
timeout: 2592000
# Token 最低活跃频率,单位:秒。-1 表示不限制
active-timeout: -1
# 是否允许同一账号多端同时登录(true 允许,false 新登录会挤掉旧登录)
is-concurrent: true
# 在多端登录下,是否共用同一个 Token(true 共用,false 每次登录生成新 Token)
is-share: true
# Token 风格:uuid(默认)、simple-uuid(不带横线)、random-32、random-64、random-128、tik
token-style: uuid
# 是否在初始化配置时在控制台打印版本字符画
is-print: true

# Redis 连接配置
spring:
data:
redis:
host: 127.0.0.1
port: 6379
password: # 如果 Redis 没有设置密码,留空即可
database: 0 # 使用 0 号数据库

每一项配置都有它存在的理由,让我们逐一理解它们的因果关系:

token-name: satoken 决定了三件事:前端通过 Cookie 传递 Token 时的 Cookie 名称、前端通过请求头传递 Token 时的 Header 名称、以及后端从请求中读取 Token 时的参数名。如果不配置,默认值也是 satoken。在前后端分离的项目中,前端需要在每次请求的 Header 中携带 satoken: <token值>

timeout: 2592000 是 Token 的最大存活时间,2592000 秒等于 30 天。超过这个时间,即使用户没有主动注销,Token 也会自动失效。如果设置为 -1,Token 将永不过期——这在生产环境中通常不推荐,但在开发调试阶段很方便。

is-concurrent: true 控制是否允许同一个账号在多个设备上同时登录。设置为 true 时,用户在手机和电脑上可以同时保持登录状态。设置为 false 时,新设备登录会自动将旧设备踢下线。回忆一下基础篇——我们为了实现这个功能,手写了整个 TokenRedisManager 的 ZSet 逻辑和顶号机制。而在 Sa-Token 中,只需要改一个配置项。

is-share: true 在多端登录的前提下,控制多个设备是否共用同一个 Token。设置为 true 时,同一账号的多次登录会返回同一个 Token 值。设置为 false 时,每次登录都会生成一个全新的 Token。

token-style: uuid 决定了 Token 的生成格式。Sa-Token 内置了多种风格,uuid 是默认值,生成类似 a]1b2c3d4-e5f6-7890-abcd-ef1234567890 的字符串。如果你觉得横线碍眼,可以用 simple-uuid

确保本地 Redis 服务已启动,否则项目启动时会报连接失败的错误。


第四章. 一行代码登录:StpUtil 核心 API

项目搭建完成,配置也就绪了。现在我们终于可以写代码了。

在基础篇中,实现一个登录功能需要经过这样的链路:Controller 接收请求 → AuthService 校验密码 → JwtUtil 生成 Token → TokenRedisManager 存储到 Redis → 封装 AuthToken 返回。五个类协作,才能完成一次登录。

在 Sa-Token 中,这条链路被压缩成了一行代码。让我们亲手体验一下。

4.1. 登录与注销

我们先创建一个最简单的登录控制器,感受 Sa-Token 的 API 风格。

📄 文件:src/main/java/com/example/authsatoken/controller/LoginController.java(新建)

为了让代码结构清晰,我们先创建 controller 包。在 com.example.authsatoken 包下新建 controller 目录,然后创建 LoginController.java

我们先从最核心的三个操作开始:登录、注销、查询登录状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.example.authsatoken.controller;

import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.*;


@RestController
@RequestMapping("/auth")
public class LoginController {
/**
* 登录接口
* 实际项目中,这里应该查询数据库校验用户名和密码
* 为了聚焦 Sa-Token 本身,我们先用硬编码模拟
*/
@PostMapping("/login")
public SaResult login(String username, String password) {
// 模拟登录成功
if ("admin".equals(username) && "123456".equals(password)) {
// 核心:一行代码完成登录
// 参数 10001 是用户 ID,Sa-Token 会围绕这个 ID 创建会话
StpUtil.login(10001);
return SaResult.ok("登录成功");
}
return SaResult.error("用户名或密码错误");
}

/**
* 注销接口
*/
@PostMapping("/logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok("注销成功");
}

/**
* 查询当前是否已登录
*/
@GetMapping("/isLogin")
public SaResult isLogin() {
return SaResult.ok("是否已登录:" + StpUtil.isLogin());
}
}

这段代码的核心就是 StpUtil.login(10001) 这一行。当它被执行时,Sa-Token 在背后完成了以下工作:

  1. 根据配置的 token-style 生成一个 Token 字符串(默认是 UUID 格式)
  2. 以用户 ID 10001 为键,在存储层(内存或 Redis)中创建一个 Session 会话
  3. 将 Token 与 Session 建立映射关系
  4. 将 Token 写入响应(通过 Cookie 或响应头返回给前端)

回忆一下基础篇——这四步分别对应了我们手写的 JwtUtil.generateToken()TokenRedisManager.storeToken()RedisKeyConstants 的 Key 映射、以及 AuthController 中的响应封装。四个类的工作,被一行代码替代了。

你可能注意到了返回值使用的是 SaResult 而不是我们基础篇的 ResultSaResult 是 Sa-Token 内置的统一响应类,结构和我们手写的 Result 非常相似,包含 codemsgdata 三个字段。在实际项目中,你完全可以继续使用自己的 Result 类,这里为了减少额外代码,直接使用 Sa-Token 提供的。


4.2. Token 信息获取

登录成功后,我们通常需要获取当前用户的 Token 信息和登录 ID。Sa-Token 为此提供了一组简洁的 API。

让我们在 LoginController 中追加两个接口:

📄 文件:src/main/java/com/example/authsatoken/controller/LoginController.java(修改)

isLogin() 方法下方,追加以下两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 获取当前登录用户的 Token 详细信息
*/
@GetMapping("/tokenInfo")
public SaResult tokenInfo() {
return SaResult.data(StpUtil.getTokenInfo());
}

/**
* 获取当前登录用户的 ID
*/
@GetMapping("/loginId")
public SaResult loginId() {
return SaResult.ok("当前登录用户 ID:" + StpUtil.getLoginId());
}

StpUtil.getTokenInfo() 返回一个 SaTokenInfo 对象,包含了当前会话的完整信息:Token 名称、Token 值、登录 ID、登录设备、Token 剩余有效期等。这个方法在调试阶段非常有用,可以帮助我们快速确认登录状态是否正确。

StpUtil.getLoginId() 返回当前登录用户的 ID。如果当前未登录,这个方法会直接抛出 NotLoginException 异常。在实际项目中,如果你只是想"查一下"而不想触发异常,可以使用 StpUtil.getLoginIdDefaultNull(),未登录时返回 null 而不是抛异常。

下面是 StpUtil 中与登录状态相关的常用 API 速查表:

方法作用未登录时的行为
StpUtil.login(id)执行登录,创建会话
StpUtil.logout()注销当前会话不报错,静默执行
StpUtil.isLogin()判断是否已登录返回 false
StpUtil.getLoginId()获取登录 ID抛出 NotLoginException
StpUtil.getLoginIdDefaultNull()获取登录 ID(安全版)返回 null
StpUtil.getTokenValue()获取当前 Token 值返回 null
StpUtil.getTokenInfo()获取 Token 完整信息返回信息中 isLogin=false

4.3. 启动项目并测试

代码写完了,让我们启动项目,亲手验证一下效果。

步骤 1:确认 Redis 已启动

在终端中执行以下命令,确认 Redis 服务正在运行:

1
redis-cli ping

如果返回 PONG,说明 Redis 正常运行。如果提示连接失败,请先启动 Redis 服务。

步骤 2:启动 Spring Boot 应用

在 IDEA 中运行 AuthSatokenApplicationmain 方法。观察控制台输出,如果看到以下字符画,说明 Sa-Token 已成功加载:

1
2
3
4
5
 ____    ____    ______   ____    __  __   ____    _   __
/ ___| / _ | |_ _| / __ \ | |/ / | ___| | \ | |
\___ \ | |_| | | | | | | | | / | |___ | \| |
___) || _ | | | | |__| | | _ \ | |___ | |\ |
|____/ |_| |_| |_| \____/ |_| \_\ |_____| |_| \_|

同时确认控制台没有红色报错信息,并且出现了 Started AuthSatokenApplication in x.xx seconds 的启动成功提示。

步骤 3:测试登录

打开浏览器或 Postman,访问以下地址:

1
http://localhost:8081/auth/login?username=admin&password=123456

预期响应:

1
2
3
4
5
{
"code": 200,
"msg": "登录成功",
"data": null
}

步骤 4:测试查询登录状态

继续访问:

1
http://localhost:8081/auth/isLogin

预期响应:

1
2
3
4
5
{
"code": 200,
"msg": "是否已登录:true",
"data": null
}

步骤 5:测试获取 Token 信息

访问:

1
http://localhost:8081/auth/tokenInfo

预期响应(Token 值每次不同):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"code": 200,
"msg": "ok",
"data": {
"tokenName": "satoken",
"tokenValue": "b876adf9737f4f65bb33ab83bb023688",
"isLogin": true,
"loginId": "10001",
"loginType": "login",
"tokenTimeout": 2591874,
"sessionTimeout": 2591874,
"tokenSessionTimeout": -2,
"tokenActiveTimeout": -1,
"loginDeviceType": "DEF",
"tag": null
}
}

这个响应中有几个关键字段值得关注:

tokenName 就是我们在 application.yml 中配置的 satoken,前端在请求头中需要用这个名称传递 Token。

tokenTimeout 是 Token 的剩余有效期(秒),初始值就是我们配置的 2592000(30 天),随着时间推移会递减。

loginDevice 显示为 default-device,这是因为我们调用 StpUtil.login(10001) 时没有指定设备类型。在后续的多设备管理章节中,我们会学习如何指定设备。

步骤 6:测试注销

访问:

1
http://localhost:8081/auth/logout

预期响应:

1
2
3
4
5
{
"code": 200,
"msg": "注销成功",
"data": null
}

注销后再次访问 /auth/isLogin,应该返回 是否已登录:false


4.4. 本章小结

本章创建了 LoginController,使用 StpUtil 的核心 API 实现了登录、注销、状态查询和 Token 信息获取四个接口,并通过完整的测试流程验证了每个接口的行为。整个过程只用了一个 Controller 文件,没有编写任何工具类、配置类或 Redis 操作代码。

要点何时使用关键动作
StpUtil.login(id)用户身份校验通过后传入用户 ID,一行代码完成登录
StpUtil.getTokenInfo()调试或返回 Token 给前端时获取包含有效期、设备等完整信息
StpUtil.getLoginIdDefaultNull()不确定是否已登录时安全获取登录 ID,避免异常

第五章. 深入 Redis 集成:会话存储的幕后真相

在第四章的测试中,我们的登录、注销、Token 查询都已经正常工作了。但你可能会好奇:Sa-Token 把会话数据存到哪里去了?Token 和用户 ID 的映射关系是怎么维护的?

这一章,我们打开 Redis,看看 Sa-Token 在背后做了什么。

5.1. 为什么需要 Redis

在回答"Sa-Token 存了什么"之前,我们先回顾一个更基础的问题:为什么需要 Redis?

Sa-Token 默认使用 JVM 内存来存储会话数据。这意味着如果你不引入 Redis,所有的 Token、Session 信息都保存在应用进程的内存中。这种模式在开发调试时没有问题,但在生产环境中会遇到两个致命缺陷:

缺陷一:重启即失效。 应用一旦重启,内存中的所有会话数据全部丢失,所有用户都需要重新登录。

缺陷二:多实例不共享。 如果你的应用部署了多个实例(比如通过 Nginx 做负载均衡),用户在实例 A 上登录后,请求被转发到实例 B 时会被判定为"未登录",因为实例 B 的内存中没有这个 Token。

回忆一下基础篇——我们为了解决这两个问题,手写了 TokenRedisManagerBlacklistRedisManager,用了将近 200 行代码来管理 Redis 中的 Key。而 Sa-Token 的做法是:你只需要引入 sa-token-redis-jackson 这一个依赖,框架会自动检测到 Redis 的存在,并将所有会话数据的存储层从内存切换到 Redis。

没有任何配置代码,没有任何 @Bean 声明。这就是 Sa-Token 所说的"零配置集成"。


5.2. 打开 Redis:看看 Sa-Token 存了什么

现在让我们用 Redis 客户端工具,看看登录之后 Redis 中到底多了哪些 Key。

步骤 1:清空测试数据

为了观察更清晰,我们先清空 Redis 的 0 号数据库:

1
2
3
redis-cli
> SELECT 0
> FLUSHDB

FLUSHDB 会清空当前数据库的所有数据,仅在开发环境中使用,切勿在生产环境执行。

步骤 2:执行一次登录

确保应用已启动,然后访问:

1
http://localhost:8081/auth/login?username=admin&password=123456

步骤 3:查看 Redis 中的 Key

回到 Redis 客户端,执行:

1
> KEYS *

你会看到类似以下的输出(Token 值每次不同):

1
2
1) "satoken:login:token:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
2) "satoken:login:session:10001"

三个 Key,每一个都有明确的职责。让我们逐一解读。

Key 1:satoken:login:token:<token值>

这是 Token 到用户 ID 的映射。执行 GET 查看它的值:

1
2
> GET "satoken:login:token:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
"10001"

当前端携带 Token 发起请求时,Sa-Token 就是通过这个 Key 反查出用户 ID 的。这和我们基础篇中 TokenRedisManager 存储的 auth:token:{jti} 逻辑完全一致,只是 Sa-Token 帮我们自动完成了。

Key 2:satoken:login:session:<用户ID>

这是用户的 Session 会话对象。它的值是一个 JSON 结构,包含了该用户所有的登录信息:

1
> GET "satoken:login:session:10001"

返回的是一个序列化后的 JSON 字符串,里面记录了该用户当前持有的所有 Token 列表、登录设备信息等。这个 Session 对象是 Sa-Token 多设备管理的核心——一个用户可以有多个 Token(多设备登录),但只有一个 Session。

Key 3:satoken:login:last-active:<token值>

这是 Token 的最后活跃时间戳。Sa-Token 用它来实现"Token 最低活跃频率"功能(对应配置项 active-timeout)。如果一个 Token 超过指定时间没有发起任何请求,即使它没有过期,也会被判定为"不活跃"而失效。

我们在配置中将 active-timeout 设置为 -1(不限制),所以这个 Key 目前不会影响登录状态。但在生产环境中,这是一个非常实用的安全特性——比如设置为 1800(30 分钟),用户如果 30 分钟内没有任何操作,就需要重新登录。


5.3. Sa-Token 的 Key 命名规范

观察上面三个 Key,你会发现它们都遵循一个统一的命名模式:

1
satoken:login:<功能>:<标识>

让我们和基础篇中手写的 RedisKeyConstants 做一个对比:

功能基础篇手写 KeySa-Token 自动 Key
Token → 用户 ID 映射auth:token:{jti}satoken:login:token:{tokenValue}
用户会话信息auth:user:{userId}:tokens(ZSet)satoken:login:session:{userId}
Token 黑名单auth:blacklist:{jti}框架内部管理,无需手动操作
Token 活跃时间无(未实现)satoken:login:last-active:{tokenValue}

对比之后可以发现:Sa-Token 的 Key 设计比我们手写的更加规范和完整。特别是"最后活跃时间"这个维度,我们在基础篇中完全没有考虑到,而 Sa-Token 默认就支持了。

Sa-Token 的 Key 前缀可以通过配置 sa-token.token-prefix 自定义,避免与其他业务 Key 冲突。

你可能还注意到一个重要的差异:基础篇中我们用 ZSet 来存储用户的 Token 列表(为了支持按时间排序和设备数量限制),而 Sa-Token 使用的是一个 Session 对象(JSON 序列化后存储为 String 类型)。这两种方案各有优劣——ZSet 在查询和排序上更灵活,而 Session 对象在数据聚合和原子性上更有优势。Sa-Token 选择了后者,因为它需要在一个 Session 中同时维护 Token 列表、设备信息、自定义属性等多种数据。


5.4. 本章小结

本章通过 Redis 客户端直接观察了 Sa-Token 登录后在 Redis 中创建的三个核心 Key,理解了 Token 映射、Session 会话、活跃时间三层数据结构的设计意图,并与基础篇手写的 Redis Key 方案进行了对比。

要点何时使用关键动作
Redis 自动集成引入 sa-token-redis-jackson零配置,框架自动切换存储层
三层 Key 结构排查登录问题时通过 KEYS satoken:* 查看会话状态
Key 命名规范需要自定义前缀或排查冲突时理解 satoken:login:<功能>:<标识> 模式