很多 Spring Boot 项目在补登录态和权限控制时,最容易卡住的不是“能不能接入”,而是 Sa-Token、JWT、拦截器和业务代码各自该放在哪里。这篇文章按“先看改造结果,再走接入步骤,最后处理常见坑”的顺序整理一遍,读完你可以判断自己是否适合用 Sa-Token + JWT,以及一套最小可运行方案该怎么搭。
Sa-Token 和 JWT 分别负责什么
先把两个概念拆开看:
- Sa-Token:一套 Java 认证授权框架,负责登录认证、权限校验、Token 管理和请求拦截。
- JWT(JSON Web Token):一种 Token 格式,用来承载用户身份和过期信息。
Sa-Token 默认用随机字符串作为 token;如果你希望令牌是自包含的、便于在分布式场景中传递,可以接入 JWT 作为 token 格式。
┌────────────┬───────────────────────────────────────────────────────┐
│ 功能 │ 说明 │
├────────────┼───────────────────────────────────────────────────────┤
│ 登录认证 │ 用户输入账号密码 → 签发 token → 后续请求带 token 访问 │
├────────────┼───────────────────────────────────────────────────────┤
│ 权限校验 │ 判断用户是否有某个权限或角色 │
├────────────┼───────────────────────────────────────────────────────┤
│ Token 管理 │ token 的生成、存储、过期、续期 │
├────────────┼───────────────────────────────────────────────────────┤
│ 拦截器 │ 自动拦截未登录的请求,返回 401 │
└────────────┴───────────────────────────────────────────────────────┘
从职责上说,Sa-Token 是认证授权框架,JWT 只是其中一种令牌表现形式,这一点在选型和排错时很关键。
项目接入后会发生哪些变化
如果你的接口现在还是完全裸奔状态,引入 Sa-Token 之后,最直观的变化就是访问路径会开始分层:哪些接口允许匿名访问,哪些接口必须登录,哪些接口还要额外校验角色。
改造前
- POST /api/users → 任何人都能访问,不用登录
- GET /api/users/1 → 任何人都能查
- GET /api/users/1/avatar → 任何人都能下载
改造后
- POST /api/users/login → 登录接口,返回 token
- GET /api/users/1 → 需要携带 token 才能访问
- DELETE /api/users/1 → 需要管理员权限才能删
请求头示例:
Authorization: token值
这类改造的重点不在于“把所有接口都拦住”,而在于把匿名接口、登录态接口和管理接口区分清楚。
Spring Boot 中的集成步骤
1. 在 pom.xml 中加入依赖
cn.dev33
sa-token-spring-boot3-starter
1.39.0
cn.dev33
sa-token-redis-jackson
1.39.0
cn.dev33
sa-token-jwt
1.39.0
这里的版本号都保持为 1.39.0。如果你的项目是多模块,建议统一管理版本,避免 starter 和扩展包不一致。
2. 配置 application.yml
Sa-Token 的基础配置决定 token 名称、有效期、并发策略和日志输出;JWT 部分则决定签名密钥和签发者。
############## Sa-Token 配置 ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: Authorization
# token 有效期(秒),7天
timeout: 604800
# 是否允许同一账号同时在线
is-concurrent: true
# 是否允许同一账号多地登录
is-share: true
# token 风格(可选:uuid、simple-uuid、random-32、random-64、random-128)
token-style: uuid
# 是否输出操作日志
is-log: true
# JWT 配置
jwt:
# 签名密钥,随便写一段字符串
secret-key: abcdefghijklmnopqrstuvwxyz0123456789
# token 签发者
issuer: test-project
原文里这段 YAML 的缩进和注释位置比较混乱,实际使用时要保证格式正确,否则 Spring Boot 无法正常加载。另一个容易忽略的点是:既然你打算用 JWT,就要认真管理 secret-key,不要在生产环境里长期使用随意写死的测试值。
3. 用拦截器统一做登录与角色校验
拦截器是这套方案的核心,它把“哪些路径需要登录、哪些路径跳过校验、哪些请求要检查角色”集中到一个配置类里,而不是散落在每个 Controller 中。
新建文件:src/main/java/com/test/config/SaTokenConfig.java
package com.test.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器
registry.addInterceptor(new SaInterceptor(handle -> {
// 登录校验:需要登录才能访问的路径
SaRouter.match("/api/**")
.notMatch("/api/users/login")
.notMatch("/api/users/register")
.check(r -> StpUtil.checkLogin());
// 角色校验示例:只有 admin 才能删除用户
SaRouter.match("/api/users/**", r -> {
if (handle.getRequest().getMethod().equals("DELETE")) {
StpUtil.checkRole("admin");
}
});
})).addPathPatterns("/**");
}
}
这段配置体现了两个规则:
/api/users/login和/api/users/register放行,因为用户必须先能登录或注册。/api/**默认要求登录,而对DELETE用户接口再追加admin角色校验。
这种写法的好处是路径规则一眼能看清,后续扩展 /api/admin/** 之类的后台接口也更方便。
4. 在 Controller 中补齐登录、退出和当前用户接口
接入拦截器之后,业务侧还需要真正提供登录入口,让前端拿到 token,并能查询当前登录用户。
// ========== 登录 ==========
@PostMapping("/login")
public Map login(@RequestBody Map params) {
String username = params.get("username");
String password = params.get("password");
// 校验账号密码
User user = userService.getUserByUsername(username);
if (user == null || !user.getPassword().equals(password)) {
return Map.of("success", false, "message", "用户名或密码错误");
}
// 登录,生成 token
StpUtil.login(user.getId());
String tokenValue = StpUtil.getTokenValue();
return Map.of("success", true, "message", "登录成功", "token", tokenValue, "user", user);
}
@GetMapping("/logout")
public Map logout() {
StpUtil.logout();
return Map.of("success", true, "message", "退出成功");
}
// 查看当前登录的用户信息
@GetMapping("/me")
public Map me() {
long userId = StpUtil.getLoginIdAsLong();
User user = userService.getUserById(userId);
return Map.of("success", true, "user", user);
}
这里最关键的调用是 StpUtil.login(user.getId())。它完成登录态创建;随后通过 StpUtil.getTokenValue() 取出 token,返回给前端保存并用于后续请求。
JWT 在这套方案里的角色,以及业务代码怎么用
JWT 在登录流程中处于哪一层
JWT 并不替代 Sa-Token,而是作为 token 的一种输出格式参与登录流程。
登录成功
↓
StpUtil.login(用户ID)
↓
Sa-Token 生成 Token
↓
默认:随机字符串格式 → "d8f9a0e1-2b3c-4d5e-..."
JWT:令牌格式 → "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
↓
返回给前端
↓
前端每次请求携带此 token
JWT 的价值在于,token 本身可以包含用户 ID、过期时间等信息,服务端验证时不一定每次都需要查数据库或 Redis,这对分布式部署更友好。不过这并不意味着业务数据完全不查库,例如你要拿完整用户资料时,仍然需要根据登录 ID 查询用户表。
业务代码中的常见用法
1. 获取当前登录用户
// 在 Controller 或 Service 中
long userId = StpUtil.getLoginIdAsLong(); // 获取当前登录的用户ID
User user = userService.getUserById(userId);
2. 校验登录、角色和权限
// 判断是否登录
StpUtil.isLogin() // true/false
StpUtil.checkLogin() // 未登录会抛出异常,返回 401
// 判断角色
StpUtil.hasRole("admin") // true/false
StpUtil.checkRole("admin") // 没有会抛出异常
// 判断权限
StpUtil.hasPermission("user:delete") // true/false
3. 给 token 会话写入角色信息
// 在登录成功后给用户分配角色
StpUtil.login(userId);
StpUtil.getTokenSession().set("role", "admin");
实际项目里,角色与权限更常见的做法是从用户、角色、权限表中动态查询,而不是直接把全部授权逻辑硬编码在登录接口里。上面的代码更适合作为快速验证接入是否成功的示例。
前端请求方式与几个必须处理的注意事项
前端如何携带 token 请求接口
前端流程很直接:先调登录接口拿到 token,再把它放进 Authorization 请求头。
# 1. 登录获取 token
curl -X POST http://localhost:8080/api/users/login
-H "Content-Type: application/json"
-d '{"username":"admin","password":"123456"}'
返回:
{"success":true,"token":"eyJhbGciOiJIUzI1NiIs...","user":{...}}
# 2. 后续请求携带 token
curl http://localhost:8080/api/users/1
-H "Authorization: eyJhbGciOiJIUzI1NiIs..."
# 3. 查看个人信息
curl http://localhost:8080/api/users/me
-H "Authorization: eyJhbGciOiJIUzI1NiIs..."
无论是前端框架还是移动端调用,本质上都遵循这个过程。真正需要统一的是 token 存储方式、失效后的处理,以及 401 返回时是否自动跳转登录页。
上线前至少要补上的几个安全细节
1. 密码不能明文存储
原示例里直接拿数据库密码和输入密码做字符串比较,这只适合演示流程,不适合生产环境。
// 注册时加密
String encodedPassword = BCrypt.gensalt().encode(password);
// 登录时校验
boolean matches = BCrypt.checkpw(inputPassword, user.getPassword());
2. 返回用户对象时要隐藏密码字段
// 序列化时忽略密码字段
@JsonIgnore
private String password;
3. 拦截器放行与拦截范围要明确
/api/users/login→ 不拦截(要登录)/api/users/register→ 不拦截(要注册)/api/**→ 拦截(需要登录)/api/admin/**→ 拦截(需要管理员角色)
这几项看起来基础,但大多数“明明接了鉴权却还是不安全”的问题,基本都出在这里:密码明文、用户对象泄漏敏感字段,或者拦截路径写错导致本该保护的接口没有保护。
总结:这套组合适合什么场景
┌───────────────┬────────────────────────────────────────────────┐
│ 组件 │ 在这个项目中的作用 │
├───────────────┼────────────────────────────────────────────────┤
│ Sa-Token │ 登录认证、token 管理、权限校验、请求拦截 │
├───────────────┼────────────────────────────────────────────────┤
│ JWT │ Token 的格式标准(用户信息编码在 token 中) │
├───────────────┼────────────────────────────────────────────────┤
│ SaInterceptor │ 自动拦截未登录请求,不用每个 Controller 写判断 │
├───────────────┼────────────────────────────────────────────────┤
│ StpUtil │ 核心工具类,登录/登出/获取用户/校验权限 │
└───────────────┴────────────────────────────────────────────────┘
如果你的 Spring Boot 项目正需要一套上手快、路径拦截清晰、权限校验也能逐步扩展的认证方案,Sa-Token 是比较实用的选择;而 JWT 更像是它在分布式或前后端分离场景下的一种令牌增强。落地时把依赖、配置、拦截器、登录接口和安全细节这几部分接好,基本就能形成完整闭环。










