Sa-Token 深入原理
轻量级 Java 权限认证框架,核心是登录凭证(Token)管理。开箱即用,API 极简,文档友好。适用场景:不想引入 Spring Security 的复杂性,又需要一个完整的认证授权方案。
1. 核心概念
| 概念 | 说明 |
|---|---|
| Token | 登录凭证,一次登录生成,后续请求携带 |
| Session | 服务端存储的用户会话数据 |
| Token 模式 | Token 存在 Redis,验证时不查数据库 |
| 注解鉴权 | @SaCheckLogin、@SaCheckRole 等注解 |
| StpLogic | 核心接口,实现登录、验证、踢人等逻辑 |
2. 工作原理
2.1 登录流程
// 业务层:验证用户名密码后,调用 Sa-Token 登录
@PostMapping("/login")
public Result<?> doLogin(String username, String password) {
// 1. 业务校验:验证用户名密码是否正确
User user = userService.checkPassword(username, password);
if (user == null) {
return Result.error("用户名或密码错误");
}
// 2. 登录:创建 Token,存入 Redis
StpUtil.login(user.getId()); // 参数是用户唯一标识
// 默认 Token 名:satoken,值是一串 UUID
// 存入 Redis:satoken:uuid → userId,过期时间 30 天
// 3. 返回 Token 给前端
return Result.ok().put("token", StpUtil.getTokenValue());
}
前端获取 Token 后,后续请求携带:
Header: satoken: <token值>
或
Cookie: satoken=<token值>
2.2 验证流程
请求到达
↓
SaTokenFilter(拦截器)读取 satoken
↓
Redis 查询 Token 是否存在且未过期
↓
Token 有效 → 取出 userId → 创建 Authentication → 继续业务代码
↓
Token 无效/过期 → 抛出 NotLoginException → 全局异常处理返回 401
2.3 StpLogic 核心接口
public interface StpLogic {
// 登录
void login(Object id);
// 登出
void logout();
// 检查是否已登录
void checkLogin();
// 检查角色
void checkRole(String... roles);
// 检查权限码
void checkPermission(String code);
// 获取当前 userId
Object getLoginId();
// 获取 Token 信息
TokenInfo getTokenInfo();
}
3. 核心功能
3.1 登录与验证
// 登录(返回 Token)
StpUtil.login(userId);
// 验证是否已登录(未登录抛 NotLoginException)
StpUtil.checkLogin();
// 获取当前登录用户 ID
StpUtil.getLoginIdAsLong();
// 获取当前登录用户 ID(通用,返回 Object)
StpUtil.getLoginId();
// 登出
StpUtil.logout();
3.2 角色与权限校验
// 角色校验(只校验角色,不校验权限)
StpUtil.checkRole("admin");
// 角色校验(必须拥有所有指定角色)
StpUtil.checkRole("admin", "user");
// 角色校验(拥有任一角色即可)
StpUtil.checkRoleOr("admin", "super_admin");
// 权限码校验
StpUtil.checkPermission("user:add");
// 权限码校验(拥有任一即可)
StpUtil.checkPermissionOr("user:add", "user:export");
3.3 注解鉴权
// 登录校验:必须登录才能访问
@SaCheckLogin
@GetMapping("/user/info")
public User getUserInfo() { ... }
// 角色校验:必须有 admin 角色
@SaCheckRole("admin")
@DeleteMapping("/user/{id}")
public void deleteUser(@PathVariable Long id) { ... }
// 权限码校验:必须有 user:add 权限
@SaCheckPermission("user:add")
@PostMapping("/user")
public void addUser(@RequestBody User user) { ... }
// 注解组合:同时校验角色和权限
@SaCheckLogin
@SaCheckPermission(value = "user:export", mode = SaMode.OR)
@GetMapping("/user/export")
public void exportUsers() { ... }
3.4 踢人下线
// 让指定用户的 Token 失效(强制下线)
StpUtil.kickout(userId);
// 让指定 Token 失效
StpUtil.kickoutByToken(tokenValue);
// 查询当前在线用户数
StpUtil.getSession().getLoginCount();
4. Token 存储模式
4.1 Redis 存储(默认)
# application.yml
sa-token:
token-name: satoken
timeout: 86400 # Token 有效期(秒),默认 30 天
token-session: -1 # Token-Session 超时(-1 不创建)
token style: uuid # Token 生成风格
数据存在 Redis:
Key: satoken:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Value: userId
TTL: 30 天
4.2 JWT 模式(无状态)
不依赖 Redis,服务端不存储 Token,Token 本身包含用户信息:
@Configuration
public class SaTokenConfig {
@Bean
public StpLogic jwtStpLogic() {
return new StpLogicJwtFor通行 Jwt {
// 返回 JWT 秘钥
return "xxxx";
};
}
}
JWT Token 结构:
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"loginId": 10086, "loginType": "login", "exp": 1735689600}
Signature: HMAC-SHA256(header + "." + payload, secret)
JWT 优点:不依赖 Redis,可跨服务验证 JWT 缺点:Token 一旦签发无法主动撤销,必须等过期
4.3 Cookie 模式
// 登录时指定 Cookie 写入
StpUtil.login(userId, "SatokenCookie"); // 第二个参数是 Cookie 名
// 获取 Cookie 值
StpUtil.getCookie().getValue();
5. SSO 单点登录
5.1 什么是 SSO
用户在一个系统登录后,访问另一个系统无需重新登录。
用户登录 app1.com → 获取 Token
↓
访问 app2.com → 自动带上 app1.com 的 Token
↓
app2.com 验证 Token → 发现是跨域 → 向 app1.com 发起校验
↓
app1.com 返回:有效,返回用户信息
5.2 Sa-Token SSO 配置
sa-token:
# SSO 相关配置
is-sso: true
sso:
ticket-name: satoken_ticket # 票据名称
ticket-timeout: 86400 # 票据有效期
allow-share: true # 是否允许 Ticket 共享
SSO 有三种模式:
- OAuth2 模式:标准单点登录,通过 Ticket 票据交换
- Header 模式:前端把 Token 一并带走(简单,不推荐跨域)
- URL 重定向模式:通过 URL 参数传递 Ticket
6. 与 Spring Security / Shiro 综合对比
6.1 功能维度对比
| 功能 | Sa-Token | Spring Security | Shiro |
|---|---|---|---|
| 登录认证 | ✅ | ✅ | ✅ |
| 角色校验 | ✅ | ✅ | ✅ |
| 权限码校验 | ✅ | ✅ | ✅ |
| 注解鉴权 | ✅ | ✅ | ✅ |
| 踢人下线 | ✅ | ✅ | ✅ |
| Token / Session | ✅ | ✅ | ✅ |
| Redis 共享 | ✅ | ✅ | ✅ |
| SSOO 单点登录 | ✅(内置) | ❌(需 Spring Security OAuth2) | ❌(需扩展) |
| OAuth2 第三方登录 | ❌ | ✅ | ❌ |
| 方法级安全注解 | ✅ | ✅ | ✅ |
| 密码加密 | ✅(内置多种) | ✅ | ✅ |
| CSRF 防护 | ❌ | ✅ | ❌ |
| 细粒度权限(数据权限) | ❌ | ❌ | ❌(需插件) |
| 与 Spring 耦合度 | 低(独立使用) | 高(强关联) | 中 |
6.2 学习曲线对比
| 维度 | Sa-Token | Spring Security | Shiro |
|---|---|---|---|
| 文档友好度 | ⭐⭐⭐⭐⭐(文档简洁,示例丰富) | ⭐⭐⭐(官方文档晦涩) | ⭐⭐⭐⭐ |
| 上手难度 | ⭐⭐(5 分钟跑通) | ⭐⭐⭐⭐⭐(学习曲线陡峭) | ⭐⭐⭐ |
| 配置复杂度 | ⭐⭐(几行配置) | ⭐⭐⭐⭐⭐(配置项极多) | ⭐⭐⭐ |
6.3 适用场景对比
| 场景 | 推荐 | 原因 |
|---|---|---|
| 快速中小型项目 | Sa-Token | 配置少,上手快 |
| Spring 全家桶 + 复杂权限 | Spring Security | 生态完整,集成度高 |
| 非 Spring 项目 | Shiro | 不强依赖 Spring |
| 需要 SSO | Sa-Token(简单 SSO) | 内置 SSO,配置简单 |
| 需要 OAuth2 第三方登录 | Spring Security | 原生支持 OAuth2 Client |
| 需要微服务认证 | Sa-Token(JWT)或 Spring Security | 都支持 JWT |
| 需要细粒度数据权限 | 目前都不满足 | 需自研或用 MyBatis-Plus 插件 |
6.4 性能与轻量对比
| 维度 | Sa-Token | Spring Security | Shiro |
|---|---|---|---|
| 依赖包大小 | ~200KB(极轻量) | ~5MB(庞大) | ~1MB |
| 启动时间影响 | 几乎无 | 显著 | 较小 |
| 内存占用 | 低 | 高 | 中 |
| Redis 操作 | 每次验证 1 次 | 可配置 | 可配置 |
6.5 生态与维护
| 维度 | Sa-Token | Spring Security | Shiro |
|---|---|---|---|
| 开源维护 | 活跃(国内开源) | 非常活跃(VMware) | 较活跃(Apache) |
| 社区规模 | 中等 | 庞大 | 中等 |
| 最后更新时间 | 持续更新 | 持续更新 | 2024 年仍有更新 |
| 资料丰富度 | 中文资料多 | 中英文资料丰富 | 中文资料多 |
6.6 选型建议
项目需求判断树:
需要 OAuth2 / 第三方登录?
└── 是 → Spring Security
需要极简配置 / 快速上线?
└── 是 → Sa-Token
非 Spring 项目?
└── 是 → Shiro
需要 SSO 单点登录?
└── 是 → Sa-Token(简单场景)/ Spring Security OAuth2(复杂场景)
Spring 全家桶 + 复杂安全场景?
└── 是 → Spring Security(即使配置复杂,但收益高)
一般业务系统,认证授权不复杂?
└── Sa-Token(性价比最高)
7. 常见问题
7.1 Token 过期后如何处理
// 全局异常处理
@ExceptionHandler(NotLoginException.class)
public Result<?> handleNotLogin(NotLoginException e) {
return Result.error(401, "未登录或登录已过期");
}
7.2 如何自定义 Token 存储
// 实现 StpLogic 接口,替换默认实现
@Configuration
public class SaTokenConfig {
@Bean
public StpLogic myStpLogic() {
return new StpLogicSimpleExt("my-token") {
// 自定义登录逻辑
@Override
public void login(Object id, LoginModel model) {
// 自定义存储到数据库
}
};
}
}
7.3 前后端分离如何传递 Token
// 前端:放在 Header 中
axios.defaults.headers.common['satoken'] = localStorage.getItem('satoken');
// 或放在 Cookie 中(自动携带)
document.cookie = "satoken=" + token;
7.4 Sa-Token 无法替代 Spring Security 的场景
- 需要细粒度行级数据权限控制
- 需要 OAuth2 资源服务器认证
- 需要与 Spring Security OAuth2 配合的第三方登录(微信、GitHub)
- 复杂的企业级安全管理(多租户、敏感操作审计)