一行代码登录,微秒级鉴权:Sa-Token + Redis Session 实践
本文从我本人参与的一个 ERP 项目的认证模块出发,由浅入深地讲解 Sa-Token 如何实现登录校验、权限鉴权、Session 管理,以及如何用 Redis 做 Session 共享来优化鉴权性能。读完本文,你将理解 Sa-Token 的完整流转过程,而不仅仅是"怎么用"。
一、Sa-Token 是什么?
在 Java 后端开发中,登录认证和权限校验几乎是每个项目的刚需。传统方案要么自己手写拦截器 + JWT,要么引入 Spring Security 这种"重量级"框架——前者重复造轮子,后者学习曲线陡峭。
Sa-Token 是一个轻量级的 Java 权限认证框架,它的核心设计理念是:一行代码完成认证鉴权。
// 登录
StpUtil.login(userId);
// 校验登录
StpUtil.checkLogin();
// 校验权限
StpUtil.checkPermission("user:add");
就这么简单。但简单不代表简陋,Sa-Token 覆盖了登录认证、权限认证、Session 会话、单点登录、OAuth2.0 等几乎所有认证场景。
Sa-Token 的核心架构分为四层:
应用层 (Controller / Service)
↓
StpUtil (工具类门面层,提供静态方法)
↓
StpLogic (核心逻辑层,所有鉴权逻辑在这里)
↓
SaTokenDao (数据持久层接口)
↓
具体实现 (内存 / Redis / 等)
- StpUtil 是业务代码直接调用的入口,本质上是一个静态工具类的门面
- StpLogic 是真正的核心逻辑所在,登录、校验、Session 操作都在这里
- SaTokenDao 是数据存取的抽象接口,默认用内存实现,引入
sa-token-redis-jackson后就切换为 Redis 实现
理解了这个分层,后面看任何 Sa-Token 的行为,你都能知道它"在哪一层做的"。
二、实现登录认证
2.1 引入依赖
首先在 pom.xml 中引入 Sa-Token 和 Redis 相关依赖:
<!-- Sa-Token -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redis 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
引入 sa-token-redis-jackson 后,Sa-Token 会自动将所有数据(Token 映射、Session 数据)存入 Redis,无需额外配置。这就是 Sa-Token 的"插件化"设计——换一个依赖,底层存储就换了,上层代码完全不用改。
2.2 Sa-Token 配置
在 application.yml 中添加 Sa-Token 配置:
sa-token:
# Token 名称,同时也是 Cookie 名称和 Header key
token-name: satoken
# Token 有效期(秒),默认 30 天
timeout: 2592000
# Token 临时有效期(指定时间内无操作就过期),-1 表示不限制
active-timeout: -1
# 是否允许同一账号多地同时登录
is-concurrent: true
# 多人登录同一账号时,是否共用一个 Token(false = 每次登录新建 Token)
is-share: false
# Token 风格
token-style: uuid
# 是否开启日志
is-log: true
同时需要配置 Redis 连接信息(application-dev.yml):
spring:
data:
redis:
host: localhost
port: 6379
database: 0
lettuce:
pool:
max-active: 16
max-wait: -1ms
max-idle: 8
min-idle: 0
2.3 登录接口实现
下面是我本人项目中的登录流程,核心思路是:查用户 → 校验密码 → Sa-Token 登录 → 存 Session → 返回 Token。
// 登录核心流程(伪代码)
public LoginResponse login(LoginRequest request) {
// 1. 根据账号查询用户(联查部门、角色、权限)
LoginUser loginUser = loginUserService.findByUsername(request.getUsername());
// 2. 校验密码
if (!passwordUtil.matches(request.getPassword(), loginUser.getPasswordHash())) {
throw new BizException("用户名或密码错误");
}
// 3. Sa-Token 登录(核心:传入 userId,生成 Token 并建立映射)
StpUtil.login(loginUser.getUserId());
// 4. 将用户信息存入 Session(后续鉴权直接从 Redis 取,不再查库)
UserContext.setCurrentUser(loginUser);
// 5. 返回 Token 给前端
LoginResponse response = new LoginResponse();
response.setToken(StpUtil.getTokenValue()); // Token 值
response.setTokenName(StpUtil.getTokenName()); // Header key(即 "satoken")
response.setUser(loginUser);
return response;
}
其中 LoginUser 是存入 Session 的用户快照,包含 userId、username、roleCodes、permissionCodes 等鉴权所需字段。LoginResponse 返回给前端的 token 和 tokenName,前端后续请求需以 tokenName 作为 Header key 携带 Token。
核心就是 StpUtil.login(loginUser.getUserId()) 这一行。调用后,Sa-Token 会生成一个 UUID Token,并在 Redis 中建立 Token → userId 的映射。后续请求只要带上这个 Token,Sa-Token 就能识别出是哪个用户。
三、实现路由拦截
登录接口本身当然不能被拦截,否则用户永远无法登录。其他所有接口都需要校验登录状态。Sa-Token 通过 Spring MVC 拦截器来实现这一点。
3.1 拦截器配置
下面是我本人项目中的拦截器配置:
// SaTokenConfig.java
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**")
.excludePathPatterns(
// 登录接口放行
"/auth/login",
// Knife4j 文档放行
"/doc.html",
"/swagger-resources",
"/swagger-resources/**",
"/v3/api-docs",
"/v3/api-docs/**",
"/webjars/**",
"/favicon.ico"
);
}
}
这段配置的含义:
addPathPatterns("/**"):拦截所有路径excludePathPatterns(...):排除登录接口和 API 文档路径SaInterceptor(handle -> StpUtil.checkLogin()):拦截器的核心逻辑就是调用StpUtil.checkLogin()
当请求到达时,SaInterceptor 会先执行 StpUtil.checkLogin()。如果当前请求没有携带有效的 Token,就会抛出 NotLoginException,请求被拦截。如果 Token 有效,请求放行,继续执行后续的业务逻辑。
3.2 退出登录
退出登录只需要一行代码:
StpUtil.logout();
这会清除 Redis 中当前 Token 对应的所有映射和 Session 数据,之后这个 Token 就失效了。
四、实现权限校验
4.1 实现 StpInterface 接口
Sa-Token 的权限校验(如 StpUtil.checkPermission("user:add"))需要知道当前用户拥有哪些权限和角色。这些数据从哪来?Sa-Token 不会帮你查数据库,它定义了一个接口 StpInterface,你必须实现这个接口,告诉 Sa-Token 当前用户有哪些权限和角色。
这是 Sa-Token 官方给出的示例代码:
// 官方示例:StpInterface 的基本实现
@Component
public class StpInterfaceImpl implements StpInterface {
@Autowired
private PermissionService permissionService;
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 根据 loginId 查询数据库,获取该用户的权限码列表
Long userId = Long.parseLong(loginId.toString());
return permissionService.getPermissionsByUserId(userId);
}
/**
* 返回一个账号所拥有的角色标识集合
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 根据 loginId 查询数据库,获取该用户的角色编码列表
Long userId = Long.parseLong(loginId.toString());
return permissionService.getRolesByUserId(userId);
}
}
可以看到,官方示例的实现方式是每次鉴权都查数据库。当调用 StpUtil.checkPermission("user:add") 时,Sa-Token 会调用 getPermissionList() 获取权限列表,然后在列表中查找是否包含 "user:add"。
4.2 使用注解鉴权
实现了 StpInterface 之后,就可以在 Controller 中使用注解进行权限校验:
// 需要登录才能访问
@SaCheckLogin
@GetMapping("/user/info")
public Result getUserInfo() { ... }
// 需要 user:add 权限才能访问
@SaCheckPermission("user:add")
@PostMapping("/user/add")
public Result addUser() { ... }
// 需要 admin 角色
@SaCheckRole("admin")
@DeleteMapping("/user/delete/{id}")
public Result deleteUser() { ... }
也可以在代码中手动校验:
// 校验权限,没有则抛异常
StpUtil.checkPermission("user:add");
// 判断是否有权限,返回 boolean
StpUtil.hasPermission("user:add");
4.3 问题:每次鉴权都查数据库?
按照官方示例的实现方式,每次调用 StpUtil.checkPermission() 都会触发 StpInterfaceImpl.getPermissionList(),进而查询数据库。在一个权限校验密集的系统中,一个页面可能涉及多个接口,每个接口都可能校验权限,数据库压力可想而知。
而用户信息和权限是改动极少的低频变更数据,每次都查库完全是浪费。有没有更好的方案?
五、深入机制:Sa-Token 登录校验的底层原理
在讲优化方案之前,我们需要先搞清楚 Sa-Token 的登录校验机制到底是怎么运作的。理解了底层,优化思路自然就出来了。
5.1 StpUtil.login() 存了什么?
当调用 StpUtil.login(userId) 时,Sa-Token 在底层做了以下事情:
- 检查此账号是否已被封禁(如果之前调用了
StpUtil.disable()封禁账号,这里会抛异常) - 生成 Token 凭证:根据配置的
token-style(我们用的是uuid)生成一个唯一的 token 字符串 - 在 Redis 中建立 Token → userId 的映射:
satoken:login:token:{tokenValue}→userId - 在 Redis 中建立 userId → Token 列表的映射:
satoken:login:session:{userId}→[token1, token2, ...](因为同一账号可能多处登录) - 创建 Account-Session:
satoken:session:account:{userId}→{}(空 Session) - 将 Token 写入响应:默认写入 Cookie,也可以通过响应体返回给前端
用 Redis 的视角来看,登录后 Redis 中多了这些 key:
satoken:login:token:abc-123-uuid → 10001 (Token → userId)
satoken:login:session:10001 → [abc-123-uuid] (userId → Token列表)
satoken:session:account:10001 → {} (Account-Session,暂为空)
5.2 StpUtil.checkLogin() 怎么知道登录没有?
当请求到达拦截器,调用 StpUtil.checkLogin() 时,核心逻辑如下:
// StpLogic 核心逻辑(简化版)
public void checkLogin() {
// 1. 从请求中获取 Token 值
String tokenValue = getTokenValue();
// 2. 如果 Token 为空,抛出未登录异常
if (tokenValue == null || tokenValue.isEmpty()) {
throw new NotLoginException(...);
}
// 3. 根据 Token 从 Redis 获取对应的 userId
Object loginId = getLoginIdByToken(tokenValue);
// 4. 如果找不到对应的 userId,说明 Token 无效或已过期
if (loginId == null) {
throw new NotLoginException(...);
}
// 5. 校验通过,可能还会续签 Token 有效期
}
关键就在第 1 步 getTokenValue()。Sa-Token 从请求中读取 Token 的优先级顺序是:
Header → Cookie → Request Body (parameter)
具体来说:
- 先从 Header 读:读取请求头中名为
satoken(配置中token-name: satoken)的字段值 - 再从 Cookie 读:读取 Cookie 中名为
satoken的值 - 最后从请求参数读:读取 URL 参数或请求体中名为
satoken的值
这三个读取开关可以分别通过配置控制:
sa-token:
is-read-head: true # 是否从 Header 读取 Token(默认 true)
is-read-cookie: true # 是否从 Cookie 读取 Token(默认 true)
is-read-body: true # 是否从请求参数读取 Token(默认 true)
5.3 Sa-Token 是怎么拿到当前请求的 Header 和 Cookie 的?
这是很多人困惑的地方:StpUtil.checkLogin() 是一个静态方法调用,它怎么知道当前请求是谁发的?
答案在 Spring 框架的 RequestContextHolder。整个调用链如下:
RequestContextHolder 是 Spring 提供的工具类,它内部使用 ThreadLocal 将当前线程的 HttpServletRequest 绑定到线程上。Spring MVC 在处理每个请求时,都会将 Request 对象存入 ThreadLocal,这样在任何代码位置都能通过 RequestContextHolder 获取到当前请求——这就是线程隔离。
所以,StpUtil.checkLogin() 能读到 Header 中的 Token,本质上是 Spring MVC 通过 ThreadLocal 把当前请求对象暴露出来了,Sa-Token 只是读取了这个请求对象中的 Header 信息。
5.4 完整的登录校验流程
把上面的知识点串起来,一个请求从到达后端到完成登录校验的完整流程:
如果 Redis 中找不到对应的 userId,或者 Token 已过期,就会抛出 NotLoginException,拦截器捕获异常后返回未登录的响应。
六、Sa-Token 的 Session 模型:不是 HttpSession!
在讲优化方案之前,还需要理解 Sa-Token 的 Session 体系。因为后面的 UserContext 优化方案,正是基于 Sa-Token 的 Account-Session 实现的。
6.1 先说清楚:这不是 HttpSession
提到 Session,很多人第一反应是 JSP/Servlet 中的 HttpSession。那是一种基于 Cookie 的会话机制:
- 客户端首次访问时,服务器创建一个
HttpSession,生成一个JSESSIONID JSESSIONID通过 Cookie 返回给浏览器- 后续请求浏览器自动带上这个 Cookie,服务器根据
JSESSIONID找到对应的 Session
这种机制有几个致命问题:
- 每个访问者都创建 Session:即使只是浏览了一个页面,服务器也会创建 Session 对象,浪费资源
- 绑定客户端:Session 是分配给客户端的,同一账号在 PC 和 APP 登录会被识别为两个不相干的会话
- 强依赖 Cookie:在不支持 Cookie 的环境(如小程序、APP)下失效
- 无法跨域:Cookie 受同源策略限制,前后端分离部署时跨域无法携带
- 无法共享:多实例部署时,Session 存在各自 JVM 内存中,无法共享
Sa-Token 的 Session 完全不同。 它只在调用 StpUtil.login(id) 时才创建,是分配给账号 id 的,而不是分配给客户端的。它不依赖 Cookie,数据存在 Redis 中,天然支持分布式共享。
6.2 三种 Session 模型
Sa-Token 提供了三种 Session 模型,适用于不同场景:
Account-Session(账号会话)—— 最常用
以账号 id 为主,同一账号的所有登录共享同一个 Session。
// 获取当前账号的 Account-Session
SaSession session = StpUtil.getSession();
// 存取数据
session.set("loginUser", loginUser);
LoginUser user = (LoginUser) session.get("loginUser");
特点:只要 Token 指向的账号 id 一致,对应的 Session 对象就是同一个。也就是说,用户在 PC 和 APP 上登录同一账号,它们操作的是同一个 Session,数据天然同步。
这是最常用的 Session 模型,我本人项目就是用 Account-Session 来存储用户信息的。
Token-Session(令牌会话)
以 Token 为主,不同的 Token 对应不同的 Session。
// 获取当前 Token 的 Token-Session
SaSession session = StpUtil.getTokenSession();
特点:即使两个设备登录了同一账号,只要它们的 Token 不同,对应的 Token-Session 就不同。这为不同端的独立数据读写提供了支持。
典型场景:实现"指定设备超过 2 小时无操作就自动下线"。如果把最后操作时间存在 Account-Session 里,PC 端无操作但 APP 端在操作,PC 端也不会过期——因为它们共享同一个 Session。而存在 Token-Session 里,每个设备的操作记录是独立的,互不影响。
Custom-Session(自定义会话)
以自定义 key 为主,不依赖账号 id 或 Token。
// 获取指定 key 的 Custom-Session
SaSession session = SaSessionCustomUtil.getSessionById("goods-10001");
特点:只要 key 相同,就是同一个 Session。完全自定义,灵活度最高。
典型场景:按业务实体维度缓存数据,比如按商品 ID 缓存库存信息。
三种 Session 的关系
假设三个客户端登录同一账号,且配置了不共享 Token(is-share: false),Session 模型如下:
Account-Session (userId=10001)
├── Token-A (PC端) → Token-Session-A
├── Token-B (APP端) → Token-Session-B
└── Token-C (H5端) → Token-Session-C
Custom-Session (key="goods-10001") ← 独立于账号和Token
| Session 类型 | 绑定维度 | 共享范围 | 常用程度 |
|---|---|---|---|
| Account-Session | 账号 id | 同一账号所有登录共享 | 最常用 |
| Token-Session | Token 值 | 同一 Token 独享 | 特定场景 |
| Custom-Session | 自定义 key | 同 key 共享 | 按需使用 |
七、优化:UserContext 方案,告别每次查库
7.1 回顾问题
按照前面官方示例的 StpInterfaceImpl 实现,每次鉴权都要查数据库:
请求到达 → StpUtil.checkLogin() 校验通过 → 拿到 loginId(userId)
→ StpInterfaceImpl.getPermissionList() 被调用
→ 根据 userId 查数据库获取角色和权限 → 返回权限列表
→ StpUtil.checkPermission() 判断权限
这意味着每个需要鉴权的请求都要查数据库,而用户信息和权限是改动极少的低频变更数据,每次都查库完全是浪费。
7.2 优化思路:在 Session 中留下快照
既然 Sa-Token 的 Session 数据存在 Redis 中,而 Redis 的读取速度是微秒级的,那我们完全可以在登录时把用户信息一次性查出来,存入 Account-Session,后续请求直接从 Session 中取,不再查库。
这就是我本人项目中 UserContext 的设计思路:
// UserContext.java
public class UserContext {
private static final String SESSION_KEY = "loginUser";
private UserContext() {
}
/**
* 获取当前登录用户,未登录返回 null
*/
public static LoginUser getCurrentUser() {
if (!StpUtil.isLogin()) {
return null;
}
return (LoginUser) StpUtil.getSession().get(SESSION_KEY);
}
/**
* 将登录用户信息存入 Session
*/
public static void setCurrentUser(LoginUser loginUser) {
StpUtil.getSession().set(SESSION_KEY, loginUser);
}
/**
* 获取当前用户ID,未登录返回 null
*/
public static Long getUserId() {
LoginUser user = getCurrentUser();
return user != null ? user.getUserId() : null;
}
/**
* 获取当前用户登录账号,未登录返回 null
*/
public static String getUsername() {
LoginUser user = getCurrentUser();
return user != null ? user.getUsername() : null;
}
/**
* 判断当前用户是否超级管理员,未登录返回 false
*/
public static boolean isAdmin() {
LoginUser user = getCurrentUser();
return user != null && Boolean.TRUE.equals(user.getIsAdmin());
}
}
登录时存入(回顾前面的 AuthServiceImpl):
// 3. Sa-Token 登录
StpUtil.login(loginUser.getUserId());
// 5. 存入 Session(关键优化点)
UserContext.setCurrentUser(loginUser);
后续请求直接取:
// 任何需要当前用户信息的地方
LoginUser user = UserContext.getCurrentUser();
7.3 优化后的 StpInterfaceImpl
有了 UserContext,StpInterfaceImpl 就不需要查数据库了:
// StpInterfaceImpl.java — 优化后,从 Session 读取权限
@Component
public class StpInterfaceImpl implements StpInterface {
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
LoginUser user = UserContext.getCurrentUser();
if (user == null) {
return Collections.emptyList();
}
return user.getPermissionCodes();
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
LoginUser user = UserContext.getCurrentUser();
if (user == null) {
return Collections.emptyList();
}
return user.getRoleCodes();
}
}
对比一下优化前后的差异:
| 对比项 | 优化前(查库) | 优化后(Session 缓存) |
|---|---|---|
| 数据来源 | 每次查 MySQL | 从 Redis Session 读取 |
| 响应时间 | 1-10ms(数据库查询) | 0.01-0.1ms(Redis 读取) |
| 数据库压力 | 高(每个鉴权请求一次查询) | 低(仅登录时查询一次) |
| 数据时效性 | 实时最新 | 可能有短暂延迟 |
7.4 权限变更怎么办?
有人可能会问:如果管理员修改了某个角色的权限,Session 中缓存的旧权限怎么办?
答案很简单:直接更新 Redis 中的 Session 数据,或者清除该用户对应的 Session 让其重新登录。
// 方案一:更新 Session 中的权限数据
StpUtil.getSessionByLoginId(userId).set("loginUser", newLoginUser);
// 方案二:强制用户重新登录(踢下线)
StpUtil.logout(userId);
由于权限变更是低频操作,而鉴权是高频操作,这种"写少读多"的场景非常适合用缓存。即使权限变更了,也只需要一次 Redis 写操作就能让所有实例同步生效——因为 Session 数据存在 Redis 中,所有服务实例读的都是同一份数据。
八、深入优化:Redis Session 共享与跨域问题
8.1 为什么需要 Redis 存储 Session?
如果 Sa-Token 使用默认的内存存储,会面临以下问题:
- 服务重启数据丢失:Token 映射和 Session 数据都在 JVM 内存中,重启就没了,所有用户需要重新登录
- 多实例无法共享:部署多个服务实例时,用户在实例 A 登录,请求被负载均衡到实例 B 就无法识别
- 内存占用:随着在线用户增多,JVM 内存压力越来越大
引入 sa-token-redis-jackson 后,所有数据存入 Redis:
- 服务重启不丢失:Redis 是独立进程,重启 Java 应用不影响
- 多实例共享:所有实例连接同一个 Redis,读取同一份数据
- 内存可控:Redis 的内存管理比 JVM 更可控,且支持淘汰策略
8.2 Cookie 无法跨域的问题
Sa-Token 默认在登录成功后,会将 Token 写入浏览器的 Cookie 中。后续请求时,浏览器会自动携带 Cookie,Sa-Token 从 Cookie 中读取 Token 完成校验。
这个默认行为在前后端同域部署时没问题,但在前后端分离、跨域部署时会出问题:
- 后端在
localhost:8080,前端在localhost:5173,端口不同,属于跨域 - Cookie 受同源策略限制,无法跨域携带
- 即使服务端设置了 Cookie,前端后续请求也不会自动带上
8.3 解决方案:Token 通过 Header 传递
我本人项目采用前后端分离架构,因此采用了 Token 通过 Header 传递 的方案:
- 登录成功后,后端不依赖 Cookie,而是将 Token 通过响应体返回给前端
- 前端将 Token 存储在
localStorage中 - 后续请求时,前端手动将 Token 添加到请求 Header 中
- 后端从 Header 中读取 Token 完成校验
前端代码(伪代码):
// 登录
const res = await axios.post('/auth/login', { username, password });
const { token, tokenName } = res.data;
localStorage.setItem(tokenName, token);
// 后续请求
axios.get('/some-api', {
headers: {
[tokenName]: localStorage.getItem(tokenName) // Header: satoken=xxx
}
});
8.4 为什么 StpUtil.checkLogin() 能从 Header 中读取 Token?
回到前面的问题。我们在配置中设置了 token-name: satoken,Sa-Token 的 Token 读取逻辑(在 StpLogic 中)大致如下:
// StpLogic.getTokenValue() 简化逻辑
public String getTokenValue() {
// 1. 先从当前请求的 Storage(ThreadLocal 缓存)中取
String tokenValue = getFromStorage();
if (tokenValue != null) return tokenValue;
// 2. 从 Header 中读取(key = 配置的 token-name,即 "satoken")
if (config.getIsReadHead()) {
tokenValue = SaHolder.getRequest().getHeader(config.getTokenName());
if (tokenValue != null) return tokenValue;
}
// 3. 从 Cookie 中读取
if (config.getIsReadCookie()) {
tokenValue = SaHolder.getRequest().getCookieValue(config.getTokenName());
if (tokenValue != null) return tokenValue;
}
// 4. 从请求参数中读取
if (config.getIsReadBody()) {
tokenValue = SaHolder.getRequest().getParam(config.getTokenName());
if (tokenValue != null) return tokenValue;
}
return null;
}
所以,当前端在请求头中加上 satoken: xxx 时,Sa-Token 在第 2 步就能读到,根本不需要走到 Cookie 那一步。
整个链路:
九、完整流转图
把所有知识点串起来,从登录到鉴权的完整流程:
关键点:整个鉴权过程中,没有任何数据库查询,所有数据都从 Redis 中读取。Redis 的读取速度是微秒级的,远快于数据库的毫秒级,所以鉴权几乎是无感的。
十、总结
回顾全文,我们按照"实现 → 机制 → 优化"的顺序,由浅入深地讲解了 Sa-Token 的认证鉴权体系:
实现层面:
- 登录:
StpUtil.login(userId)生成 Token、建立映射、创建 Session - 路由拦截:
SaInterceptor+StpUtil.checkLogin()拦截未登录请求 - 权限校验:实现
StpInterface接口,告诉 Sa-Token 当前用户的权限和角色
机制层面:
4. Token 读取:Sa-Token 按 Header → Cookie → Body 的优先级读取 Token,底层通过 Spring 的 RequestContextHolder + ThreadLocal 获取当前请求对象
5. Session 模型:Account-Session(按账号共享,最常用)、Token-Session(按 Token 隔离)、Custom-Session(自定义 key),这不是 HttpSession,是 Sa-Token 自己的 Session 体系
优化层面:
6. UserContext 方案:登录时将用户信息存入 Account-Session,鉴权时从 Redis 读取,避免每次查库,性能提升数量级
7. Redis Session 共享:引入 sa-token-redis-jackson,Session 数据存 Redis,多实例共享、服务重启不丢失
8. 跨域方案:前后端分离时,Token 不走 Cookie,改用 Header 传递,前端手动添加 satoken 请求头
核心思想:Sa-Token 的 Session 方案本质上是用 Redis 做了一个高性能的"用户快照缓存"。登录时一次性加载用户数据到 Redis,后续鉴权全部走 Redis 读取,既保证了性能,又通过 Redis 天然实现了分布式 Session 共享。权限变更时只需更新 Redis 中的 Session 数据,所有实例立即生效。
这就是 Sa-Token 的优雅之处——用最简单的 API,背后是精心设计的 Session 体系和存储策略。
更多推荐

所有评论(0)