Article

Sa-Token 零基础入门与实现原理

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 登录,一般会写出这样的流程:

  1. 用户提交用户名和密码。
  2. 后端校验密码。
  3. 后端生成一个 Token。
  4. 把 Token 返回给前端。
  5. 前端每次请求把 Token 放到请求头。
  6. 后端解析 Token,判断用户是谁。
  7. 再去查这个用户有没有权限。

这个流程看起来不复杂,但真正做后台系统时,会马上遇到很多细节:

  • 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 SecuritySa-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 格式,而是一套认证鉴权框架。

它可以做三件事:

  1. 生成和管理 Token:登录、注销、续期、踢人下线。
  2. 维护登录态:知道哪个 Token 对应哪个用户,用户是否在线,是否被禁用。
  3. 做权限判断:判断当前用户是否拥有某个角色或权限标识。

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
timeoutToken 总有效期,单位秒
active-timeoutToken 最低活跃时间,长时间不访问会失效
is-concurrent是否允许同一账号多端同时登录
is-share同账号多次登录是否共用同一个 Token
token-styleToken 生成风格,例如 UUID、随机字符串、JWT 风格
is-log是否打印 Sa-Token 日志

在供应链 SaaS 项目中,常见设计是:

  • AccessToken 有效期较短,例如 2 小时。
  • RefreshToken 有效期较长,例如 7 天。
  • 普通接口使用 AccessToken。
  • AccessToken 过期后,用 RefreshToken 换取新的 AccessToken。
  • 用户退出、修改密码、账号禁用时,让登录态和 RefreshToken 一起失效。

Sa-Token 自身负责核心登录态和权限校验,RefreshToken 可以由业务系统单独设计一张表或 Redis Key 来管理。

3.3 登录接口

登录接口的业务流程:

  1. 根据 tenantCode 查询租户。
  2. 根据 tenantId + username 查询用户。
  3. 判断账号是否启用、是否锁定。
  4. 使用 BCrypt 校验密码。
  5. 调用 StpUtil.login(userId) 完成 Sa-Token 登录。
  6. 获取 Token 返回给前端。
  7. 查询角色权限,缓存到 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();
}

执行过程是:

  1. 请求进入 Controller 前,Sa-Token 拦截器先判断是否登录。
  2. 发现方法上有 @SaCheckPermission 注解。
  3. 读取注解里的权限标识:srm:supplier:audit
  4. 调用 StpInterface#getPermissionList() 获取当前用户权限列表。
  5. 判断权限列表里是否包含 srm:supplier:audit
  6. 包含则放行,不包含则抛出无权限异常。

流程图:

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 详细对比

对比项手写 JWTSa-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"]

这里有三层安全边界:

  1. Gateway 做基础 Token 校验和路由。
  2. 业务服务用 Sa-Token 注解做功能权限。
  3. 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

但美国站运营只能看美国店铺订单,德国站运营只能看德国店铺订单。

这种要靠数据权限实现:

  1. Sa-Token 判断用户是否有 oms:order:list
  2. AOP 根据当前用户角色查询允许访问的店铺 ID。
  3. Mapper 查询追加 shop_id in (...)
  4. 多租户插件继续追加 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 或数据库读取,并支持缓存失效。

传统 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 管理、权限校验、多端控制和踢人下线这些通用能力。