Sa-Token 零基础入门与实现原理
Sa-Token 是一个 Java 权限认证框架,主要用来解决登录认证、权限认证、Session 会话、单点登录、OAuth2、微服务鉴权等问题。它不是一种新的 Token 格式,也不是 JWT 的替代名称,而是把“登录、鉴权、会话、踢人下线、权限注解、多端登录、SSO”等常见功能封装成一套更容易使用的框架。
如果已经学过 Token、JWT、单点登录,可以先用一句话理解:
JWT 是一种 Token 格式,Sa-Token 是一套权限认证框架。Sa-Token 可以生成普通随机 Token,也可以集成 JWT 风格 Token,还可以把登录状态、角色权限、Session、踢人下线、多端登录统一管理起来。
第一节 为什么需要 Sa-Token
1.1 原生 Token 登录会遇到什么问题
假设没有权限框架,只用自己写的 Token 登录,一般会写出这样的流程:
- 用户提交用户名和密码。
- 后端校验密码。
- 后端生成一个 Token。
- 把 Token 返回给前端。
- 前端每次请求把 Token 放到请求头。
- 后端解析 Token,判断用户是谁。
- 再去查这个用户有没有权限。
这个流程看起来不复杂,但真正做后台系统时,会马上遇到很多细节:
- Token 放请求头还是 Cookie?
- Token 过期时间怎么控制?
- 用户退出登录后 Token 如何失效?
- 管理员禁用用户后,旧 Token 是否还能用?
- 同一个账号能不能多端登录?
- 同一个账号在新设备登录时,要不要挤掉旧设备?
- 用户角色变更后,权限如何立即生效?
- Controller 接口上如何优雅地做权限校验?
- 微服务下多个服务如何共享登录态?
- 网关层和业务服务层如何协同鉴权?
如果这些都自己写,代码会分散在拦截器、Controller、Service、Redis 工具类、权限工具类里,后期维护成本很高。
Sa-Token 的价值就是把这些通用能力封装起来,让业务代码更关注“登录成功后做什么”“这个接口需要什么权限”,而不是每个项目都重复造一套登录鉴权轮子。
1.2 Sa-Token 能解决哪些问题
在本项目中,Sa-Token 主要承担以下职责:
| 能力 | 说明 | 项目中的例子 |
|---|---|---|
| 登录认证 | 判断用户是否已经登录 | 未登录访问供应商审核接口,返回 401 |
| Token 管理 | 生成、读取、续期、注销 Token | 用户登录后生成 AccessToken |
| 权限认证 | 判断用户是否拥有某个权限标识 | @SaCheckPermission("srm:supplier:audit") |
| 角色认证 | 判断用户是否拥有某个角色 | 只有租户管理员能管理员工 |
| Session 会话 | 保存当前登录账号的会话信息 | 保存登录设备、登录 IP、用户上下文 |
| 踢人下线 | 主动让某个账号的 Token 失效 | 管理员禁用账号后强制下线 |
| 多端登录 | 控制同账号是否允许多设备在线 | PC 端和移动端是否互斥 |
| SSO 单点登录 | 多个系统共享登录态 | 管理后台、供应商 Portal、BI 看板统一登录 |
| 微服务鉴权 | 多个服务共享登录状态 | Gateway 和业务服务都能识别当前用户 |
1.3 Sa-Token 和 Spring Security 的区别
Spring Security 是 Spring 生态中非常强大的安全框架,能力完整,但配置链路比较复杂。Sa-Token 更轻量,API 更直观,更适合中后台项目快速落地。
| 对比项 | Spring Security | Sa-Token |
|---|---|---|
| 学习成本 | 较高,过滤器链和配置较复杂 | 较低,API 和注解直观 |
| 登录认证 | 功能强,但配置多 | StpUtil.login(id) 即可登录 |
| 权限校验 | 表达能力强 | 注解和工具类简单易懂 |
| 多端登录 | 需要额外配置 | 内置支持 |
| 踢人下线 | 能实现,但配置较多 | API 直接支持 |
| 适合场景 | 复杂企业安全体系 | 中后台、SaaS、管理系统、微服务鉴权 |
选择 Sa-Token 不代表系统自动安全。密码加密、接口限流、数据权限、租户隔离、权限缓存失效、敏感字段脱敏、审计日志仍然需要业务系统自己设计。
第二节 先建立正确概念
2.1 Token 是什么
Token 本质上是一段令牌字符串。用户登录成功后,服务器把 Token 返回给前端。前端后续请求带上 Token,服务器根据 Token 判断“这个请求来自哪个用户”。
Token 本身只是一种“凭证”。它可以是随机字符串,也可以是 JWT,也可以是其他格式。
例如普通随机 Token:
3b7d3bdb-21bd-45ac-8b82-6b6e705e3f3a
例如 JWT:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEwMDEsImV4cCI6MTcxNjAwMDAwMH0.xxxxx
2.2 JWT 是什么
JWT,全称 JSON Web Token,是一种自包含 Token 格式。它通常由三部分组成:
Header.Payload.Signature
- Header:声明签名算法,例如 HS256。
- Payload:保存用户 ID、租户 ID、过期时间等信息。
- Signature:用密钥对 Header 和 Payload 签名,防止篡改。
JWT 的特点是服务端可以通过签名验证判断 Token 是否可信,并从 Payload 中取出用户信息。它的优点是服务端不一定要存储 Token;缺点是 Token 一旦签发,在过期前很难主动失效,除非额外引入黑名单、版本号或 Redis 状态校验。
2.3 Sa-Token 是什么
Sa-Token 不是一种 Token 格式,而是一套认证鉴权框架。
它可以做三件事:
- 生成和管理 Token:登录、注销、续期、踢人下线。
- 维护登录态:知道哪个 Token 对应哪个用户,用户是否在线,是否被禁用。
- 做权限判断:判断当前用户是否拥有某个角色或权限标识。
Sa-Token 可以使用多种 Token 风格。默认可以是 UUID、随机字符串等,也可以通过集成 JWT 让 Token 长得像 JWT。
2.4 最容易讲错的一句话
错误说法:
Sa-Token 就是 JWT。
正确说法:
Sa-Token 是权限认证框架,JWT 是 Token 格式。Sa-Token 可以使用 JWT 风格 Token,也可以使用随机字符串 Token。使用 JWT 后,是否完全无状态,还要看系统是否需要踢人下线、权限实时失效、单点登录、多端管理等能力。
第三节 Sa-Token 快速入门
3.1 引入依赖
Spring Boot 3 项目可以引入 Sa-Token 的 Spring Boot 3 Starter。版本号建议在父工程统一管理。
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
如果需要把登录态保存到 Redis,还需要引入对应 Redis 集成依赖。项目中为了支持多服务共享登录态,通常会让 Sa-Token 使用 Redis 作为持久层。
如果使用 JWT 风格 Token,需要引入 Sa-Token 的 JWT 集成包,并配置 token-style 或 JWT 相关参数。具体依赖名称可以根据项目使用的 Sa-Token 版本查官方文档确认。
3.2 基础配置
一个典型配置如下:
sa-token:
token-name: Authorization
timeout: 7200
active-timeout: 1800
is-concurrent: true
is-share: false
token-style: random-64
is-log: false
字段含义:
| 配置 | 含义 |
|---|---|
token-name | 前端传 Token 的名称,常见是 Authorization |
timeout | Token 总有效期,单位秒 |
active-timeout | Token 最低活跃时间,长时间不访问会失效 |
is-concurrent | 是否允许同一账号多端同时登录 |
is-share | 同账号多次登录是否共用同一个 Token |
token-style | Token 生成风格,例如 UUID、随机字符串、JWT 风格 |
is-log | 是否打印 Sa-Token 日志 |
在供应链 SaaS 项目中,常见设计是:
- AccessToken 有效期较短,例如 2 小时。
- RefreshToken 有效期较长,例如 7 天。
- 普通接口使用 AccessToken。
- AccessToken 过期后,用 RefreshToken 换取新的 AccessToken。
- 用户退出、修改密码、账号禁用时,让登录态和 RefreshToken 一起失效。
Sa-Token 自身负责核心登录态和权限校验,RefreshToken 可以由业务系统单独设计一张表或 Redis Key 来管理。
3.3 登录接口
登录接口的业务流程:
- 根据
tenantCode查询租户。 - 根据
tenantId + username查询用户。 - 判断账号是否启用、是否锁定。
- 使用 BCrypt 校验密码。
- 调用
StpUtil.login(userId)完成 Sa-Token 登录。 - 获取 Token 返回给前端。
- 查询角色权限,缓存到 Redis。
示例代码:
@RestController
@RequestMapping("/auth")
public class LoginController {
private final UserService userService;
public LoginController(UserService userService) {
this.userService = userService;
}
@PostMapping("/login")
public Result<LoginVO> login(@RequestBody LoginRequest request) {
LoginUser loginUser = userService.checkLogin(request);
StpUtil.login(loginUser.getUserId());
String token = StpUtil.getTokenValue();
LoginVO vo = new LoginVO();
vo.setAccessToken(token);
vo.setUserId(loginUser.getUserId());
vo.setTenantId(loginUser.getTenantId());
vo.setUsername(loginUser.getUsername());
vo.setPermissions(loginUser.getPermissions());
return Result.ok(vo);
}
}
这里要注意:StpUtil.login(userId) 不负责校验用户名密码。用户名密码校验是业务系统自己的事情。Sa-Token 只负责在校验通过后,把这个用户标记为“已登录”。
3.4 获取当前登录用户
业务接口中可以通过 Sa-Token 获取当前登录用户 ID:
Long userId = StpUtil.getLoginIdAsLong();
如果当前请求没有登录,Sa-Token 会抛出未登录异常。项目里通常通过全局异常处理器把它转换成统一响应:
{
"code": 401,
"message": "登录已过期,请重新登录"
}
3.5 退出登录
退出登录时调用:
StpUtil.logout();
它会让当前 Token 对应的登录态失效。前端也应该删除本地保存的 Token。
如果是管理员强制某个用户下线,可以按用户 ID 踢下线:
StpUtil.logout(userId);
3.6 接口权限注解
Sa-Token 常用注解:
@SaCheckLogin
表示必须登录才能访问。
@SaCheckPermission("srm:supplier:audit")
表示必须拥有供应商审核权限。
@SaCheckRole("tenant_admin")
表示必须拥有租户管理员角色。
供应商审核接口可以这样写:
@PostMapping("/{id}/approve")
@SaCheckPermission("srm:supplier:audit")
public Result<Void> approve(@PathVariable Long id) {
supplierService.approve(id);
return Result.ok();
}
这个注解的意思是:当前用户必须已经登录,并且拥有 srm:supplier:audit 权限,否则 Sa-Token 会拦截请求并返回 403。
3.7 告诉 Sa-Token 用户有哪些权限
Sa-Token 本身不知道用户有哪些角色和权限。它需要业务系统实现一个接口,告诉框架如何查询权限。
典型写法是实现 StpInterface:
@Component
public class StpInterfaceImpl implements StpInterface {
private final PermissionService permissionService;
public StpInterfaceImpl(PermissionService permissionService) {
this.permissionService = permissionService;
}
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 先查询 Redis 如果Redis有 就直接Redis中的集合 如果没有 再查询数据库
Long userId = Long.valueOf(loginId.toString());
return permissionService.getPermissionCodes(userId);
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
Long userId = Long.valueOf(loginId.toString());
return permissionService.getRoleCodes(userId);
}
}
权限查询可以先查 Redis 缓存,缓存没有再查数据库:
权限缓存 Key:auth:permission:{tenantId}:{userId}
角色缓存 Key:auth:role:{tenantId}:{userId}
管理员修改角色权限、用户角色、禁用用户、调整套餐时,要删除对应缓存,让权限及时生效。
第四节 Sa-Token 的实现原理
这一节是重点。学习 Sa-Token 不能只会写 StpUtil.login(),还要理解它背后到底做了什么。
4.1 Sa-Token 的核心模型
Sa-Token 可以抽象成四个核心对象:
| 核心对象 | 作用 |
|---|---|
| LoginId | 当前登录账号的唯一标识,例如用户 ID |
| TokenValue | 发给前端的令牌字符串 |
| Token 存储 | 保存 Token 和 LoginId 的映射关系 |
| Session | 保存当前账号的会话数据,例如登录设备、权限缓存、临时数据 |
最核心的关系是:
TokenValue -> LoginId
LoginId -> 当前账号的登录状态、Session、Token 列表
用户请求时,Sa-Token 先拿到 TokenValue,再根据 TokenValue 找到 LoginId,最后判断这个 LoginId 是否处于有效登录状态。
4.2 默认模式下的登录原理
默认服务端登录态模式可以理解为“随机 Token + 服务端存储”。
登录时:
sequenceDiagram
participant FE as 前端
participant BE as 后端服务
participant SAT as Sa-Token
participant DAO as SaTokenDao
participant Redis as Redis
FE->>BE: 提交用户名和密码
BE->>BE: 校验租户 用户 密码 状态
BE->>SAT: StpUtil.login(userId)
SAT->>SAT: 生成 TokenValue
SAT->>DAO: 保存 TokenValue 与 userId 映射
DAO->>Redis: 写入登录态和 Session 数据
SAT-->>BE: 返回当前 Token
BE-->>FE: 返回 Token 和用户信息
可以把它理解成:
Redis:
satoken:token:{tokenValue} -> userId
satoken:session:{userId} -> 用户 Session
satoken:account:{userId}:device -> 当前账号的 Token 列表
不同版本和不同配置下,实际 Key 名称可能不同,但思想类似:Token 本身只是钥匙,真正的登录态保存在服务端存储中。
4.3 请求鉴权原理
用户带 Token 访问接口时:
sequenceDiagram
participant FE as 前端
participant INT as Sa-Token 拦截器
participant DAO as SaTokenDao
participant Redis as Redis
participant API as Controller
FE->>INT: 请求头携带 Token
INT->>INT: 从 Header/Cookie/参数中读取 Token
INT->>DAO: 根据 Token 查询 LoginId
DAO->>Redis: 查询登录态
Redis-->>DAO: 返回 LoginId 和会话信息
DAO-->>INT: Token 有效
INT->>INT: 检查是否过期 是否被踢下线 是否被封禁
INT->>API: 放行请求
API->>API: 执行业务逻辑
如果 Token 不存在、过期、被踢下线、账号被封禁,Sa-Token 会抛出异常。全局异常处理器把异常转换成 401 或 403。
4.4 权限注解的底层原理
以这个接口为例:
@SaCheckPermission("srm:supplier:audit")
@PostMapping("/supplier/{id}/approve")
public Result<Void> approve(@PathVariable Long id) {
supplierService.approve(id);
return Result.ok();
}
执行过程是:
- 请求进入 Controller 前,Sa-Token 拦截器先判断是否登录。
- 发现方法上有
@SaCheckPermission注解。 - 读取注解里的权限标识:
srm:supplier:audit。 - 调用
StpInterface#getPermissionList()获取当前用户权限列表。 - 判断权限列表里是否包含
srm:supplier:audit。 - 包含则放行,不包含则抛出无权限异常。
流程图:
flowchart TD
A["请求进入供应商审核接口"] --> B["Sa-Token 判断是否登录"]
B --> C{"Token 有效?"}
C -- "否" --> D["返回 401 未登录"]
C -- "是" --> E["读取 @SaCheckPermission 注解"]
E --> F["获取所需权限<br>srm:supplier:audit"]
F --> G["调用 StpInterface<br>查询当前用户权限列表"]
G --> H{"权限列表是否包含该权限?"}
H -- "否" --> I["返回 403 无权限"]
H -- "是" --> J["放行业务方法"]
4.5 Sa-Token 如何获取权限列表
Sa-Token 不关心权限表怎么设计。权限表可以是:
sys_user
sys_role
sys_menu
sys_user_role
sys_role_menu
也可以是其他结构。Sa-Token 只要求业务系统最终返回一个字符串列表。
例如:
[
"srm:supplier:list",
"srm:supplier:add",
"srm:supplier:audit",
"pms:order:list"
]
Sa-Token 做权限判断时,只看这个列表里有没有对应字符串。
所以权限系统可以分成两层:
| 层次 | 谁负责 |
|---|---|
| 用户、角色、权限数据怎么存 | 业务系统负责 |
| 当前用户是否拥有某个权限 | Sa-Token 负责调用接口判断 |
4.6 Sa-Token 和 Redis 的关系
在单体项目里,Sa-Token 可以使用内存保存登录态。但在微服务项目里,不能用本地内存保存登录态。
原因很简单:
用户第一次请求打到 OMS 服务,登录态存在 OMS 内存。
用户第二次请求打到 WMS 服务,WMS 内存里没有登录态。
结果 WMS 认为用户未登录。
所以微服务系统通常把登录态放到 Redis:
Gateway、OMS、WMS、TMS、FMS 都连接同一个 Redis。
任意服务拿到 Token 后,都可以从 Redis 查到当前用户是谁。
Redis 在这里不是简单缓存,而是分布式登录态存储。
4.7 Sa-Token 的 JWT 模式怎么理解
Sa-Token 可以集成 JWT,让 Token 变成 JWT 风格。JWT Token 可以携带用户 ID、租户 ID、过期时间等信息,并用密钥签名防篡改。
JWT 模式下,请求鉴权可以变成:
读取 Token -> 校验签名 -> 解析 Payload -> 获取 userId/tenantId -> 判断过期时间
这看起来不依赖 Redis,但要注意一个现实问题:
只靠 JWT 自身,很难实现主动失效。
例如:
- 用户退出登录后,旧 JWT 是否还能用?
- 管理员禁用账号后,旧 JWT 是否还能用?
- 用户修改密码后,旧 JWT 是否还能用?
- 角色权限变更后,旧 JWT 是否还能访问接口?
- 要踢掉某个设备的登录态,怎么踢?
如果要解决这些问题,就仍然需要引入服务端状态,例如:
- Redis 黑名单:记录已经失效的 JWT ID。
- Token 版本号:JWT 中带 tokenVersion,服务端校验当前版本。
- 用户状态检查:每次请求查用户是否被禁用。
- 权限缓存:权限从 Redis 或数据库读取,而不是完全信任 JWT。
所以可以这样总结:
JWT 可以让 Token 自包含,但业务系统一旦需要退出登录立即失效、踢人下线、权限实时变更、多端管理,就不能完全无状态。Sa-Token 的价值在于把这些登录态和权限管理能力封装好。
4.8 本项目如何应用 Sa-Token + JWT + Redis
本项目使用 Sa-Token 做认证鉴权框架,Token 可以配置为 JWT 风格,方便在网关和微服务之间传递用户 ID、租户 ID 等基础信息。同时,权限列表、租户状态、账号禁用状态、RefreshToken、数据权限范围仍然通过 Redis 或数据库管理,保证用户禁用、角色变更、套餐到期后能够及时生效。
也就是说:
- JWT 负责“Token 可验证、可携带基础身份信息”。
- Sa-Token 负责“登录态管理、权限注解、会话控制、多端管理”。
- Redis 负责“分布式登录态、权限缓存、主动失效、限流和幂等”。
- 业务数据库负责“用户、角色、权限、租户、套餐等真实数据”。
第五节 Sa-Token 和 JWT 的对比
5.1 一句话对比
JWT 是一种令牌格式,Sa-Token 是一套认证鉴权框架。
5.2 详细对比
| 对比项 | 手写 JWT | Sa-Token |
|---|---|---|
| 本质 | Token 格式和签名规范 | Java 权限认证框架 |
| Token 生成 | 自己调用 JWT 工具类生成 | StpUtil.login() 后框架生成 |
| 登录态 | 通常依赖 JWT Payload | 可使用 Redis/Session/JWT 等方式 |
| 权限校验 | 自己写拦截器或注解 | 内置权限注解和工具类 |
| 退出登录 | JWT 天然不好主动失效 | 内置 logout 和踢人能力 |
| 踢人下线 | 需要黑名单或版本号 | 框架提供 API |
| 多端登录 | 需要自己设计设备和 Token 表 | 内置多端登录控制 |
| 权限变更生效 | 需要自己做缓存失效 | 可结合权限接口和缓存失效 |
| SSO | 需要自己设计认证中心 | Sa-Token 提供 SSO 方案 |
| 微服务共享登录态 | 自己约定 Token 和解析逻辑 | 可结合 Redis/Gateway 统一处理 |
5.3 手写 JWT 的典型流程
flowchart TD
A["用户登录"] --> B["校验用户名密码"]
B --> C["生成 JWT<br>写入 userId tenantId exp"]
C --> D["返回前端"]
D --> E["前端携带 JWT 请求接口"]
E --> F["后端校验签名和过期时间"]
F --> G["解析 userId"]
G --> H["执行业务"]
这个流程简单,但权限、退出、踢人、续期、多端、SSO 都要自己补。
5.4 Sa-Token 的典型流程
flowchart TD
A["用户登录"] --> B["业务系统校验用户名密码"]
B --> C["StpUtil.login(userId)"]
C --> D["Sa-Token 生成 Token"]
D --> E["保存登录态和 Session"]
E --> F["返回 Token"]
F --> G["前端携带 Token 请求接口"]
G --> H["Sa-Token 校验登录态"]
H --> I["权限注解校验角色和权限"]
I --> J["执行业务"]
Sa-Token 的重点不是“Token 长什么样”,而是“登录态和权限怎么统一管理”。
5.5 为什么学过 JWT 还要学 Sa-Token
因为真实后台项目需要的不只是 Token 字符串,还需要完整认证体系。
JWT 解决的是:
这个 Token 是否可信?
这个 Token 里携带了什么用户信息?
Sa-Token 解决的是:
这个用户是否登录?
这个登录是否过期?
这个账号是否被踢下线?
这个账号能否多端登录?
这个用户是否有某个角色?
这个用户是否有某个权限?
权限变化后如何生效?
多个系统如何共享登录态?
第六节 在供应链 SaaS 项目中的落地方案
6.1 登录认证流程
sequenceDiagram
participant FE as 前端
participant GW as Gateway
participant AUTH as Auth服务
participant DB as MySQL
participant Redis as Redis
FE->>GW: 提交 tenantCode username password
GW->>AUTH: 转发登录请求
AUTH->>DB: 查询租户和用户
DB-->>AUTH: 返回用户信息
AUTH->>AUTH: BCrypt 校验密码
AUTH->>AUTH: StpUtil.login(userId)
AUTH->>Redis: 保存登录态 权限缓存 RefreshToken
AUTH-->>FE: 返回 AccessToken RefreshToken 用户信息 权限列表
关键点:
- 必须按
tenantCode + username查询用户。 - 密码用 BCrypt,不保存明文。
- 登录失败要记录次数,连续失败锁定账号。
- 登录成功后生成 AccessToken 和 RefreshToken。
- 权限列表可以返回给前端渲染菜单,但后端仍要做权限校验。
6.2 请求鉴权流程
flowchart TD
A["请求到达 Gateway"] --> B["读取 Authorization Token"]
B --> C{"Token 是否有效?"}
C -- "否" --> D["返回 401"]
C -- "是" --> E["解析 userId tenantId"]
E --> F["写入 UserContext 和 TenantContext"]
F --> G["路由到业务服务"]
G --> H["Sa-Token 注解校验功能权限"]
H --> I["AOP 追加数据权限"]
I --> J["MyBatis 自动追加 tenant_id"]
J --> K["执行业务 SQL"]
这里有三层安全边界:
- Gateway 做基础 Token 校验和路由。
- 业务服务用 Sa-Token 注解做功能权限。
- Mapper 查询通过租户插件和数据权限条件做数据隔离。
6.3 权限缓存设计
用户登录后,系统可以把权限缓存到 Redis:
auth:permission:{tenantId}:{userId}
auth:role:{tenantId}:{userId}
缓存内容:
[
"srm:supplier:list",
"srm:supplier:add",
"srm:supplier:audit",
"pms:order:list",
"wms:inventory:view"
]
缓存失效时机:
- 管理员修改角色权限。
- 管理员给用户换角色。
- 用户被禁用。
- 租户套餐变更。
- 用户退出登录。
- 用户修改密码。
6.4 多租户隔离
Sa-Token 负责识别当前登录用户,但租户隔离不是 Sa-Token 自动完成的。项目需要把租户 ID 放入登录上下文。
登录成功后 Token 或 Session 中要有:
userId
tenantId
username
roleCodes
planType
请求进入业务服务后,把 tenantId 放入 TenantContext:
TenantContext.setTenantId(loginUser.getTenantId());
MyBatis-Plus 多租户插件再给 SQL 自动追加:
where tenant_id = 当前租户ID
请求结束后必须清理 ThreadLocal:
TenantContext.clear();
否则 Tomcat 线程复用时,可能把上一个请求的租户 ID 带到下一个请求,引发严重串租户问题。
6.5 数据权限
Sa-Token 的权限注解解决“能不能访问接口”,但不能直接解决“能看到哪些数据”。
例如两个用户都有订单列表权限:
oms:order:list
但美国站运营只能看美国店铺订单,德国站运营只能看德国店铺订单。
这种要靠数据权限实现:
- Sa-Token 判断用户是否有
oms:order:list。 - AOP 根据当前用户角色查询允许访问的店铺 ID。
- Mapper 查询追加
shop_id in (...)。 - 多租户插件继续追加
tenant_id = 当前租户。
第七节 单点登录怎么理解
7.1 什么是单点登录
单点登录 SSO 的意思是:用户只登录一次,就能访问多个相互信任的系统。
例如:
- SaaS 管理后台
- 供应商 Portal
- BI 数据看板
- 运维管理后台
这些系统如果都接入统一认证中心,用户在认证中心登录一次,访问其他系统时就不需要重复输入账号密码。
7.2 Sa-Token 做 SSO 的基本思想
SSO 一般有一个认证中心:
SSO Server:负责登录、签发 Token、校验 Token。
SSO Client:业务系统,负责跳转登录、接收 Token、访问资源。
同域名下可以共享 Cookie。跨域名时,一般通过重定向、ticket、Token 校验接口完成登录态传递。
简化流程:
sequenceDiagram
participant User as 用户
participant App as 业务系统
participant SSO as 认证中心
User->>App: 访问业务系统
App->>App: 发现未登录
App-->>User: 重定向到 SSO 登录页
User->>SSO: 输入账号密码
SSO->>SSO: 登录成功 生成登录态
SSO-->>User: 携带 ticket 返回业务系统
User->>App: 带 ticket 访问回调地址
App->>SSO: 校验 ticket
SSO-->>App: 返回用户身份
App->>App: 建立本系统登录态
7.3 单点登录和普通 Token 登录的区别
普通 Token 登录是一个系统自己登录自己。
SSO 是多个系统信任同一个认证中心。业务系统不直接处理账号密码,而是把用户引导到认证中心,由认证中心完成登录,再把身份结果返回给业务系统。
在供应链项目中,SSO 可以用于统一管理后台、供应商 Portal、BI 看板、运营后台的登录体验。
第八节 常见问题和易错点
8.1 Sa-Token 会自动查数据库吗
不会。
Sa-Token 不知道你的用户表、角色表、菜单表怎么设计。登录前的账号密码校验、权限列表查询、角色列表查询,都需要业务系统自己实现。Sa-Token 负责调用你提供的接口,并根据返回结果做判断。
8.2 使用 Sa-Token 后还要不要 JWT
看项目需求。
如果希望 Token 自包含、网关可以解析基础用户信息,可以集成 JWT。
如果希望强控制登录态、踢人下线、多端管理、权限实时失效,仍然要结合 Redis 或数据库状态。
Sa-Token 和 JWT 不是二选一。可以用 Sa-Token 做框架,用 JWT 做 Token 格式。
8.3 使用 JWT 后还要不要 Redis
很多项目仍然需要 Redis。
原因是 JWT 自身很难主动失效。退出登录、踢人下线、账号禁用、权限变化、多端管理、RefreshToken、限流、幂等都需要服务端状态。Redis 可以保存黑名单、权限缓存、登录设备、RefreshToken 和限流计数。
8.4 Sa-Token 能不能替代数据权限
不能完全替代。
Sa-Token 可以判断用户有没有某个权限,例如 oms:order:list。但用户有订单列表权限,不代表能看所有订单。具体能看哪些店铺、仓库、供应商的数据,还需要业务系统通过数据权限 AOP 和 SQL 条件控制。
8.5 前端隐藏按钮后,后端还要校验权限吗
必须校验。
前端隐藏按钮只能提升用户体验,不能作为安全边界。用户可以通过浏览器控制台、Postman、Apifox 直接调用接口。如果后端不校验权限,就会出现越权操作。
第九节 面试高频问题
Q1:Sa-Token 是什么
Sa-Token 是一个 Java 权限认证框架,主要解决登录认证、权限认证、Session 会话、单点登录、OAuth2、微服务鉴权等问题。它不是某一种 Token 格式,而是围绕 Token 登录态提供完整管理能力的框架。
Q2:Sa-Token 和 JWT 是什么关系
JWT 是一种 Token 格式,Sa-Token 是权限认证框架。Sa-Token 可以生成普通随机 Token,也可以集成 JWT 风格 Token。JWT 解决 Token 自包含和签名验证问题,Sa-Token 解决登录态、权限、角色、多端、踢人、SSO 等完整认证问题。
Q3:StpUtil.login(userId) 底层做了什么
它会根据配置生成一个 Token,把 Token 和 loginId 的关系保存到 Sa-Token 的持久层中,例如 Redis。同时会创建或更新当前账号的登录 Session,记录 Token 有效期、多端登录状态等信息。最后当前请求就处于已登录状态,可以通过 StpUtil.getTokenValue() 获取 Token 返回给前端。
Q4:Sa-Token 如何判断用户是否登录
请求进来后,Sa-Token 会从请求头、Cookie 或参数中读取 Token。然后根据 Token 查询登录态,判断 Token 是否存在、是否过期、是否被踢下线、账号是否被封禁。如果都通过,就认为当前用户已登录。
Q5:Sa-Token 的权限注解是怎么生效的
例如接口上有 @SaCheckPermission("srm:supplier:audit"),Sa-Token 在方法执行前会读取注解中的权限标识,然后调用业务系统实现的 StpInterface#getPermissionList() 获取当前用户权限列表。如果列表中包含该权限,就放行;否则返回 403。
Q6:Sa-Token 会自动知道用户有哪些权限吗
不会。用户、角色、权限表由业务系统设计。业务系统需要实现 StpInterface,在里面查询当前用户的角色和权限。Sa-Token 只负责调用这个接口,并根据返回的权限字符串做判断。
Q7:为什么权限要缓存到 Redis
如果每个接口都查询用户、角色、权限表,数据库压力会很大。权限列表读多写少,适合缓存到 Redis。用户登录后或首次鉴权时缓存权限,后续请求直接读取缓存。角色权限变更、用户角色变更、用户禁用时删除缓存,让权限及时生效。
Q8:管理员修改了角色权限,用户需要重新登录吗
不一定。可以在角色权限变更后删除受影响用户的权限缓存。用户下一次请求时缓存未命中,系统重新从数据库加载权限,新权限就生效了。如果是高风险权限变更,也可以同时强制用户下线。
Q9:Sa-Token 如何实现踢人下线
Sa-Token 保存了账号和 Token 的关系。管理员可以根据用户 ID 找到该用户的登录 Token,并让这些 Token 失效。后续这些 Token 再访问接口时,Sa-Token 校验登录态失败,就会返回未登录或被踢下线。
Q10:Sa-Token 如何支持多端登录
Sa-Token 可以记录同一个账号在不同设备端的登录 Token。通过配置可以控制是否允许并发登录、是否共享 Token、新登录是否挤掉旧登录。业务上还可以区分 PC、APP、供应商 Portal 等不同登录端。
Q11:Sa-Token 使用 JWT 后是不是完全无状态
不一定。JWT 本身可以自包含用户信息和过期时间,但如果系统需要退出立即失效、管理员踢人、账号禁用实时生效、权限变更实时生效、多端登录管理,就仍然需要 Redis 或数据库保存服务端状态。因此不能简单说用了 JWT 就完全无状态。
Q12:为什么不能把所有权限都放到 JWT 里
权限放到 JWT 里会导致 Token 过大,而且权限变更后旧 JWT 里的权限不会自动变化。比如管理员刚取消某个用户的审核权限,但旧 JWT 还没过期,用户可能继续访问审核接口。更合理的做法是 JWT 放基础身份信息,权限从 Redis 或数据库读取,并支持缓存失效。
Q13:Sa-Token 和 Session-Cookie 登录有什么区别
传统 Session-Cookie 登录通常由服务器创建 Session,并通过 Cookie 保存 SessionId。Sa-Token 可以理解为更灵活的 Token 化登录态框架,Token 可以放 Header,也可以放 Cookie,可以支持前后端分离、多端登录、权限注解、踢人下线、SSO 和微服务共享登录态。
Q14:Sa-Token 在微服务中怎么共享登录态
多个服务连接同一个 Redis,Sa-Token 登录态保存到 Redis。用户请求任意服务时,只要携带 Token,该服务就能从 Redis 查到登录态。也可以在 Gateway 做统一 Token 解析,把 userId、tenantId 传给下游服务,下游服务继续做接口权限和数据权限校验。
Q15:Gateway 校验了 Token,业务服务还要校验吗
要。Gateway 可以做基础认证和路由拦截,但业务服务仍然要做接口级权限和数据权限。因为不同接口需要不同权限,业务服务最清楚自己的权限边界。内部服务也要防止绕过网关直接访问。
Q16:Sa-Token 如何和多租户结合
多租户的功能 体现在业务上 Sa-Token 我们需要在当前业务执行的上下文上(ThreadLocal) 可以随时获取当前的租户ID 在登录的时候 就把用户的ID 租户的ID 都存储起来
登录时根据 tenantCode + username 查询用户,登录成功后把 tenantId 放入 Token、Session 或登录上下文。请求进入业务服务后,把 tenantId 放入 TenantContext,MyBatis-Plus 多租户插件自动给 SQL 追加 tenant_id 条件。
Q17:Sa-Token 能不能防止 SQL 注入和 XSS
不能直接防止。Sa-Token 主要负责认证和鉴权。SQL 注入要靠 MyBatis 参数绑定、字段白名单、禁止拼接危险 SQL;XSS 要靠输入过滤、输出转义、富文本白名单、Cookie HttpOnly 等手段。
Q18:AccessToken 和 RefreshToken 为什么要分开
AccessToken 用于访问业务接口,生命周期短,泄露后的风险较低。RefreshToken 生命周期长,只用于换取新的 AccessToken,不能直接访问业务接口。这样既能提高安全性,又能减少用户频繁登录。
Q19:用户退出登录后,RefreshToken 怎么处理
退出登录时不仅要调用 Sa-Token 让 AccessToken 失效,还要删除或标记 RefreshToken 失效。否则用户可能用旧 RefreshToken 换取新的 AccessToken,导致退出登录不彻底。
Q20:如果 Redis 挂了,Sa-Token 还能用吗
如果项目采用 Redis 保存登录态,Redis 不可用会影响登录鉴权。生产环境需要 Redis 高可用,例如主从、哨兵或集群。也可以针对网关和认证服务设计降级策略,但权限系统属于安全核心,一般不建议在 Redis 不可用时随意放行请求。
Q21:Sa-Token 的核心优点是什么
核心优点是简单、轻量、功能完整。它用少量 API 和注解就能完成登录认证、权限校验、角色校验、踢人下线、多端登录、Session、SSO 等功能,适合中后台和 SaaS 系统快速落地。
Q22:Sa-Token 的核心缺点是什么
它不是万能安全框架。复杂企业安全场景、细粒度 OAuth2 授权、非常复杂的安全策略可能仍然需要更完整的安全体系。并且 Sa-Token 只负责认证鉴权,数据权限、租户隔离、密码策略、接口安全、审计日志仍然需要业务系统设计。
Q23:为什么项目里选择 Sa-Token
因为本项目是跨境供应链 SaaS 中后台系统,需要快速实现登录认证、RBAC 权限、接口权限注解、多端登录、踢人下线和微服务共享登录态。Sa-Token API 简洁,学习成本低,和 Redis、JWT、Spring Boot 集成方便,适合这个项目的权限安全需求。
Q24:如何向面试官讲 Sa-Token 的实现原理
可以这样讲:
Sa-Token 的核心原理是维护 Token 和登录账号之间的映射关系。用户登录成功后,业务系统调用 StpUtil.login(userId),Sa-Token 生成 Token,并把 Token 与 userId 的关系保存到持久层,例如 Redis。用户后续请求携带 Token,Sa-Token 从请求中读取 Token,再从 Redis 或 JWT 中解析出 userId,判断 Token 是否过期、是否被踢下线、账号是否有效。接口上如果有权限注解,Sa-Token 会调用业务系统实现的权限查询接口,拿到当前用户权限列表,再判断是否包含目标权限。它本质上帮我们封装了登录态管理、Token 管理、权限校验、多端控制和踢人下线这些通用能力。