登录注册番外篇(一) - 2026 年最新 Sa-Token 由浅入深快速开始
登录注册番外篇(一) - 2026 年最新 Sa-Token 由浅入深快速开始
Prorise第一章. 回头看:我们造了多少轮子
阶段式学习路径
本篇是番外篇系列的第一站。在基础篇中,我们花了六章的篇幅,从零手写了一套完整的认证内核——RSA 加密、JJWT 双令牌、Redis 黑名单、多设备管理。现在,我们暂停主线,换一个全新的视角:如果有一个框架,能用一行代码完成登录,我们还需要手写那么多东西吗?
若你对登录注册系列基础篇感兴趣,请跳转至:
本篇将带你认识 Sa-Token 框架,搭建全新的独立项目,并体验它的核心登录 API 与 Redis 集成能力。
在正式认识 Sa-Token 之前,我们先做一件事——回头看看基础篇六章走下来,我们到底写了多少代码。
这不是为了否定之前的努力。恰恰相反,正是因为我们亲手造过这些轮子,才能真正理解框架帮我们省掉了什么,以及它在背后做了哪些我们曾经手动完成的事情。
1.1. 基础篇的六大手写模块回顾
让我们快速盘点一下基础篇的产出。六章内容,我们在 auth 项目中构建了一个三模块的认证系统:
1 | auth/ |
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 | // 参数是用户 ID,可以是 long、int、String 类型 |
没有 JwtUtil,没有 RsaKeyManager,没有 TokenRedisManager。一行代码,框架自动完成了 Token 生成、Session 创建、Cookie 写入(或响应头返回)等全部工作。
让我们看看 Sa-Token 的五大核心模块,对它的能力范围建立一个全局认知:
| 模块 | 解决的问题 | 本系列是否覆盖 |
|---|---|---|
| 登录认证 | 用户登录、注销、Token 管理、多设备登录 | ✅ 本篇开始 |
| 权限认证 | 角色校验、权限码校验、注解鉴权、路由拦截 | ✅ 后续篇章 |
| 单点登录(SSO) | 多系统共享登录态 | ❌ 不在本系列范围 |
| OAuth2.0 | 第三方授权登录 | ❌ 进阶篇单独讲解 |
| 微服务网关鉴权 | Gateway 层统一鉴权 | ❌ 不在本系列范围 |
Sa-Token 的设计哲学是"一个方法解决一个问题",API 命名直白到几乎不需要查文档。
2.2. 为什么选 Sa-Token 而不是 Spring Security
你可能会想:Java 安全领域的"正统"不是 Spring Security 吗?为什么我们先讲 Sa-Token?
这个问题很好,我们从三个维度来回答。
维度一:学习曲线
Spring Security 的核心是一条 过滤器链由多个安全过滤器串联组成的请求处理链。要理解它,你需要先搞清楚 SecurityFilterChain、AuthenticationManager、AuthenticationProvider、UserDetailsService、SecurityContextHolder 这一整套概念,然后才能写出第一个自定义登录逻辑。
Sa-Token 的学习路径则完全不同。它没有过滤器链的概念,所有操作都通过一个静态工具类 StpUtil 完成。你不需要理解任何底层架构,打开 API 文档,找到你需要的方法,直接调用就行。
官方文档链接请跳转至:https://sa-token.cc/doc.html#/
老师,Spring Security 和 Sa-Token 上手难度差多少?
打个比方,Spring Security 像是一辆手动挡赛车,性能强悍但你得先学会换挡。Sa-Token 像是一辆自动挡家用车,上车就能开。
那 Sa-Token 是不是功能比较弱?
不是。它覆盖了登录认证、权限校验、多设备管理、踢人下线等绝大多数场景。只是在 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,填写以下信息:
- Name:
auth-satoken - Group:
com.example - Artifact:
auth-satoken - Type:Maven
- Language:Java
- JDK:17
- Java:17
- Packaging:Jar
在依赖选择页面,勾选以下两项:
Spring WebSpring Data Redis
点击 Create 完成创建。
如果你更习惯手动创建,也可以访问 https://start.spring.io 生成项目骨架后导入 IDEA。
步骤 2:确认项目结构
创建完成后,项目的初始结构如下:
1 | auth-satoken/ |
步骤 3:引入 Sa-Token 和 Hutool 依赖
打开 pom.xml,在 <dependencies> 节点中追加以下依赖:
📄 文件:pom.xml(修改)
1 | <!-- Sa-Token 权限认证(SpringBoot3 专用 Starter) --> |
这里有几个要点需要说明:
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 | server: |
每一项配置都有它存在的理由,让我们逐一理解它们的因果关系:
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 | package com.example.authsatoken.controller; |
这段代码的核心就是 StpUtil.login(10001) 这一行。当它被执行时,Sa-Token 在背后完成了以下工作:
- 根据配置的
token-style生成一个 Token 字符串(默认是 UUID 格式) - 以用户 ID
10001为键,在存储层(内存或 Redis)中创建一个 Session 会话 - 将 Token 与 Session 建立映射关系
- 将 Token 写入响应(通过 Cookie 或响应头返回给前端)
回忆一下基础篇——这四步分别对应了我们手写的 JwtUtil.generateToken()、TokenRedisManager.storeToken()、RedisKeyConstants 的 Key 映射、以及 AuthController 中的响应封装。四个类的工作,被一行代码替代了。
你可能注意到了返回值使用的是 SaResult 而不是我们基础篇的 Result。SaResult 是 Sa-Token 内置的统一响应类,结构和我们手写的 Result 非常相似,包含 code、msg、data 三个字段。在实际项目中,你完全可以继续使用自己的 Result 类,这里为了减少额外代码,直接使用 Sa-Token 提供的。
4.2. Token 信息获取
登录成功后,我们通常需要获取当前用户的 Token 信息和登录 ID。Sa-Token 为此提供了一组简洁的 API。
让我们在 LoginController 中追加两个接口:
📄 文件:src/main/java/com/example/authsatoken/controller/LoginController.java(修改)
在 isLogin() 方法下方,追加以下两个方法:
1 | /** |
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 中运行 AuthSatokenApplication 的 main 方法。观察控制台输出,如果看到以下字符画,说明 Sa-Token 已成功加载:
1 | ____ ____ ______ ____ __ __ ____ _ __ |
同时确认控制台没有红色报错信息,并且出现了 Started AuthSatokenApplication in x.xx seconds 的启动成功提示。
步骤 3:测试登录
打开浏览器或 Postman,访问以下地址:
1 | http://localhost:8081/auth/login?username=admin&password=123456 |
预期响应:
1 | { |
步骤 4:测试查询登录状态
继续访问:
1 | http://localhost:8081/auth/isLogin |
预期响应:
1 | { |
步骤 5:测试获取 Token 信息
访问:
1 | http://localhost:8081/auth/tokenInfo |
预期响应(Token 值每次不同):
1 | { |
这个响应中有几个关键字段值得关注:
tokenName 就是我们在 application.yml 中配置的 satoken,前端在请求头中需要用这个名称传递 Token。
tokenTimeout 是 Token 的剩余有效期(秒),初始值就是我们配置的 2592000(30 天),随着时间推移会递减。
loginDevice 显示为 default-device,这是因为我们调用 StpUtil.login(10001) 时没有指定设备类型。在后续的多设备管理章节中,我们会学习如何指定设备。
步骤 6:测试注销
访问:
1 | http://localhost:8081/auth/logout |
预期响应:
1 | { |
注销后再次访问 /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。
回忆一下基础篇——我们为了解决这两个问题,手写了 TokenRedisManager 和 BlacklistRedisManager,用了将近 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 | redis-cli |
FLUSHDB 会清空当前数据库的所有数据,仅在开发环境中使用,切勿在生产环境执行。
步骤 2:执行一次登录
确保应用已启动,然后访问:
1 | http://localhost:8081/auth/login?username=admin&password=123456 |
步骤 3:查看 Redis 中的 Key
回到 Redis 客户端,执行:
1 | > KEYS * |
你会看到类似以下的输出(Token 值每次不同):
1 | 1) "satoken:login:token:a1b2c3d4-e5f6-7890-abcd-ef1234567890" |
三个 Key,每一个都有明确的职责。让我们逐一解读。
Key 1:satoken:login:token:<token值>
这是 Token 到用户 ID 的映射。执行 GET 查看它的值:
1 | > GET "satoken:login:token:a1b2c3d4-e5f6-7890-abcd-ef1234567890" |
当前端携带 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 做一个对比:
| 功能 | 基础篇手写 Key | Sa-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:<功能>:<标识> 模式 |







