Sa-Token 使用方法及实现原理
目录
一、Sa-Token 简介
1.1 什么是 Sa-Token
Sa-Token 是一个轻量级的 Java 权限认证框架,由国内开源社区 Dromara 孵化,专注于解决:登录认证、权限认证、Session 会话、单点登录、OAuth2.0、微服务网关鉴权等一系列权限相关问题。
核心理念: 让权限认证变得简单、优雅!
1.2 为什么选择 Sa-Token
轻量级: 核心代码仅几百 KB,无侵入式集成,对项目依赖极小
简单易用: API 设计简洁直观,一行代码完成登录认证
功能完整: 覆盖权限认证的各个场景,从简单到复杂应有尽有
扩展性强: 提供多种扩展接口,可自定义底层实现
生态丰富: 支持多种集成方案,包括 Redis、JWT、OAuth2.0 等
1.3 适用场景
- 中小型项目的权限认证
- 微服务架构的统一鉴权
- 前后端分离项目的 Token 管理
- 单点登录(SSO)系统
- OAuth2.0 授权服务器
- 需要灵活控制会话的业务系统
二、核心特性
2.1 功能清单
| 功能模块 | 说明 |
|---|---|
| 登录认证 | 单账号登录、多账号登录、同端互斥登录 |
| 权限认证 | 角色认证、权限认证、二级认证 |
| Session 会话 | 全端会话、单端会话、会话查询、会话治理 |
| 踢人下线 | 根据账号 id 踢人、根据 Token 踢人 |
| 账号封禁 | 指定账号封禁、临时封禁、永久封禁 |
| 持久层扩展 | 内存、Redis、自定义存储 |
| 分布式会话 | 分布式 Session、Redis 集成 |
| 微服务鉴权 | 网关统一鉴权、RPC 调用鉴权 |
| 单点登录 | SSO 模式一、SSO 模式二、SSO 模式三 |
| OAuth2.0 | 授权码模式、隐式模式、密码模式、客户端模式 |
| 二级认证 | 在已登录的基础上再次认证 |
| Basic 认证 | Http Basic 认证 |
| 独立 Redis | 使用独立的 Redis,与业务 Redis 分离 |
| Token 风格定制 | uuid、简单 uuid、雪花算法、JWT |
| 记住我模式 | 延长 Token 有效期 |
| 密码加密 | 提供密码加密模块,多种加密算法 |
| 会话治理 | 查询所有会话、强制下线、活跃度统计 |
| 全局侦听器 | 监听用户登录、注销、被踢下线、被封禁等行为 |
| 开箱即用 | 零配置启动框架,所有特性都有默认实现 |
2.2 技术架构
┌─────────────────────────────────────────────────────┐
│ 应用层(业务代码) │
├─────────────────────────────────────────────────────┤
│ Sa-Token API 层(StpUtil) │
├─────────────────────────────────────────────────────┤
│ Sa-Token 核心层(StpLogic) │
├─────────────────────────────────────────────────────┤
│ Sa-Token 接口层(StpInterface 等) │
├─────────────────────────────────────────────────────┤
│ 持久化层(SaTokenDao - Redis/内存/自定义) │
└─────────────────────────────────────────────────────┘
三、快速开始
3.1 环境准备
- JDK 1.8+
- Spring Boot 2.x 或 3.x
- Maven 或 Gradle
3.2 Maven 依赖
基础依赖(Spring Boot 2.x)
<!-- Sa-Token 核心依赖 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.44.0</version>
</dependency>
Spring Boot 3.x(Jakarta EE)
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.44.0</version>
</dependency>
Redis 集成(推荐生产环境使用)
<!-- Sa-Token 整合 Redis(使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.44.0</version>
</dependency>
<!-- Redis 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
3.3 配置文件
在 application.yml 中配置 Sa-Token:
# Sa-Token 配置
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
# token 有效期,单位秒,-1 代表永不过期
timeout: 2592000
# token 最低活跃时间(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号并发登录(为 true 时允许一起登录,为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token(为 true 时所有登录共用一个 token,为 false 时每次登录新建一个 token)
is-share: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
token-style: uuid
# 是否输出操作日志
is-log: false
# 是否从 cookie 中读取 token
is-read-cookie: true
# 是否从 header 中读取 token
is-read-header: true
# token 前缀
token-prefix: Bearer
# 是否在初始化配置时打印版本字符画
is-print: true
# Redis 配置(如果使用 Redis 存储)
spring:
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 10s
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
3.4 第一个示例 - 登录认证
3.4.1 创建登录控制器
@RestController
@RequestMapping("/auth")
public class AuthController {
/**
* 用户登录
* @param username 用户名
* @param password 密码
* @return 登录结果
*/
@PostMapping("/login")
public Result login(@RequestParam String username, @RequestParam String password) {
// 1. 验证用户名密码(此处仅示例,实际应查询数据库)
if ("admin".equals(username) && "123456".equals(password)) {
// 2. 登录成功,为用户创建 Session 并生成 Token
StpUtil.login(10001); // 10001 是用户 id
// 3. 获取 Token 返回给前端
String token = StpUtil.getTokenValue();
return Result.success("登录成功", token);
}
return Result.error("用户名或密码错误");
}
/**
* 查询登录状态
*/
@GetMapping("/isLogin")
public Result isLogin() {
boolean isLogin = StpUtil.isLogin();
return Result.success("是否登录:" + isLogin);
}
/**
* 退出登录
*/
@PostMapping("/logout")
public Result logout() {
StpUtil.logout();
return Result.success("退出成功");
}
/**
* 获取当前登录用户信息
*/
@GetMapping("/userInfo")
public Result getUserInfo() {
// 检查登录状态,未登录会抛出异常
StpUtil.checkLogin();
// 获取当前登录用户 id
long userId = StpUtil.getLoginIdAsLong();
// 根据 userId 查询用户信息(此处省略)
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("userId", userId);
userInfo.put("username", "admin");
return Result.success(userInfo);
}
}
3.4.2 前端调用示例
// 登录
axios.post('/auth/login', {
username: 'admin',
password: '123456'
}).then(res => {
// 保存 Token 到本地存储
localStorage.setItem('satoken', res.data.data);
console.log('登录成功');
});
// 后续请求携带 Token
axios.get('/auth/userInfo', {
headers: {
'satoken': localStorage.getItem('satoken')
}
}).then(res => {
console.log('用户信息:', res.data);
});
四、核心功能详解
4.1 登录认证
4.1.1 基本登录
// 会话登录,参数可以是任意类型,建议使用用户 id
StpUtil.login(10001);
// 获取当前会话是否已登录
boolean isLogin = StpUtil.isLogin();
// 检查当前会话是否已登录,未登录则抛出异常
StpUtil.checkLogin();
// 获取当前会话登录 id
Object loginId = StpUtil.getLoginId();
long loginIdAsLong = StpUtil.getLoginIdAsLong();
String loginIdAsString = StpUtil.getLoginIdAsString();
int loginIdAsInt = StpUtil.getLoginIdAsInt();
// 获取当前会话的 Token 值
String tokenValue = StpUtil.getTokenValue();
// 获取当前会话的 Token 信息
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
// 会话注销
StpUtil.logout();
// 指定账号注销(踢人下线)
StpUtil.logout(10001);
// 强制指定账号下线
StpUtil.kickout(10001);
4.1.2 指定设备登录
Sa-Token 支持同一账号在不同设备上登录:
// 指定设备标识登录
StpUtil.login(10001, "PC"); // PC 端登录
StpUtil.login(10001, "APP"); // APP 端登录
StpUtil.login(10001, "WAP"); // H5 端登录
// 查询指定设备是否登录
StpUtil.isLogin("PC");
// 踢出指定设备
StpUtil.logout(10001, "PC");
// 查询指定账号在所有设备的登录情况
List<String> list = StpUtil.getTokenValueListByLoginId(10001);
4.1.3 记住我模式
// 登录时设置记住我,7 天免登录
StpUtil.login(10001, new SaLoginModel().setTimeout(60 * 60 * 24 * 7));
// 或者使用快捷方法
StpUtil.login(10001, true); // true 表示开启记住我
4.1.4 自定义 Token 生成策略
// 登录时自定义 Token
StpUtil.login(10001, new SaLoginModel()
.setToken("自定义Token值") // 自定义此次登录的 Token 值
.setTimeout(60 * 60 * 24) // 设置有效期 24 小时
.setIsLastingCookie(true) // 设置为持久化 Cookie
);
4.2 权限认证
Sa-Token 的权限认证模型基于 RBAC(Role-Based Access Control)思想。
4.2.1 实现权限接口
首先需要实现 StpInterface 接口,告诉框架当前用户拥有哪些权限:
@Component
public class StpInterfaceImpl implements StpInterface {
@Autowired
private UserService userService;
/**
* 返回指定账号所拥有的权限列表
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 根据 loginId 查询用户拥有的权限列表
List<String> permissions = userService.getPermissionsByUserId(Long.parseLong(loginId.toString()));
return permissions;
}
/**
* 返回指定账号所拥有的角色列表
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 根据 loginId 查询用户拥有的角色列表
List<String> roles = userService.getRolesByUserId(Long.parseLong(loginId.toString()));
return roles;
}
}
4.2.2 权限认证示例
// 判断当前账号是否含有指定权限
StpUtil.hasPermission("user:add");
// 校验当前账号是否含有指定权限,没有则抛出异常
StpUtil.checkPermission("user:delete");
// 校验当前账号是否含有指定权限(支持多个,只要有一个满足即可)
StpUtil.checkPermissionOr("user:add", "user:update", "user:delete");
// 校验当前账号是否含有指定权限(支持多个,必须全部满足)
StpUtil.checkPermissionAnd("user:add", "user:update");
// 判断当前账号是否含有指定角色
StpUtil.hasRole("admin");
// 校验当前账号是否含有指定角色,没有则抛出异常
StpUtil.checkRole("admin");
// 校验当前账号是否含有指定角色(支持多个,只要有一个满足即可)
StpUtil.checkRoleOr("admin", "super-admin");
// 校验当前账号是否含有指定角色(支持多个,必须全部满足)
StpUtil.checkRoleAnd("admin", "super-admin");
4.2.3 使用注解鉴权
Sa-Token 提供了丰富的注解来简化权限校验:
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 需要登录才能访问
*/
@SaCheckLogin
@GetMapping("/info")
public Result getUserInfo() {
return Result.success("用户信息");
}
/**
* 需要 user:add 权限才能访问
*/
@SaCheckPermission("user:add")
@PostMapping("/add")
public Result addUser(@RequestBody User user) {
return Result.success("添加成功");
}
/**
* 需要 user:update 或 user:delete 权限才能访问(满足其一即可)
*/
@SaCheckPermission(value = {"user:update", "user:delete"}, mode = SaMode.OR)
@PostMapping("/update")
public Result updateUser(@RequestBody User user) {
return Result.success("更新成功");
}
/**
* 需要 admin 角色才能访问
*/
@SaCheckRole("admin")
@DeleteMapping("/delete/{id}")
public Result deleteUser(@PathVariable Long id) {
return Result.success("删除成功");
}
/**
* 需要 admin 和 super-admin 角色才能访问(必须同时拥有)
*/
@SaCheckRole(value = {"admin", "super-admin"}, mode = SaMode.AND)
@PostMapping("/superOperation")
public Result superOperation() {
return Result.success("操作成功");
}
}
4.2.4 权限通配符
Sa-Token 支持权限通配符匹配:
// 用户拥有权限:user:*
// 可以匹配:user:add、user:delete、user:update 等
// 用户拥有权限:*:delete
// 可以匹配:user:delete、order:delete、product:delete 等
// 在配置时开启通配符匹配
sa-token:
check-id-token: true
4.3 Session 会话
4.3.1 User-Session(用户级会话)
// 获取当前登录用户的 Session
SaSession session = StpUtil.getSession();
// 在 Session 中存取数据
session.set("name", "张三");
session.set("age", 18);
String name = session.get("name", "");
int age = session.get("age", 0);
// 使用快捷方法
StpUtil.getSession().set("name", "张三");
String name = StpUtil.getSession().get("name", "");
// 获取指定用户的 Session(无需登录)
SaSession session = StpUtil.getSessionByLoginId(10001);
4.3.2 Token-Session(令牌级会话)
// 获取当前 Token 的 Token-Session
SaSession tokenSession = StpUtil.getTokenSession();
// 存取数据
tokenSession.set("device", "PC");
String device = tokenSession.get("device", "");
4.3.3 Custom-Session(自定义会话)
// 创建自定义 Session
SaSession session = SaSessionCustomUtil.getSessionById("order-" + orderId);
session.set("status", "已支付");
4.4 踢人下线
// 踢掉指定账号(此账号在所有设备上的会话都会被清除)
StpUtil.kickout(10001);
// 踢掉指定账号在指定设备上的登录
StpUtil.kickout(10001, "PC");
// 踢掉指定 Token
StpUtil.kickoutByTokenValue("token值");
4.5 账号封禁
// 封禁指定账号,时长为 86400 秒(1 天)
StpUtil.disable(10001, 86400);
// 永久封禁指定账号
StpUtil.disable(10001, -1);
// 解除封禁
StpUtil.untieDisable(10001);
// 查询指定账号是否被封禁
boolean isDisable = StpUtil.isDisable(10001);
// 获取指定账号剩余封禁时间(秒)
long time = StpUtil.getDisableTime(10001);
// 在登录前检查此账号是否被封禁,被封禁则抛出异常
StpUtil.checkDisable(10001);
4.6 二级认证
某些敏感操作需要在已登录的基础上再次认证:
// 在需要二级认证的接口,进行二级认证检查
StpUtil.checkSafe();
// 开启二级认证(有效期为 120 秒)
StpUtil.openSafe(120);
// 获取二级认证剩余有效时间(秒)
long time = StpUtil.getSafeTime();
// 检查当前会话是否已完成二级认证
boolean isSafe = StpUtil.isSafe();
// 关闭二级认证
StpUtil.closeSafe();
示例场景:
@RestController
@RequestMapping("/account")
public class AccountController {
/**
* 修改密码 - 需要二级认证
*/
@SaCheckSafe
@PostMapping("/changePassword")
public Result changePassword(@RequestParam String oldPwd, @RequestParam String newPwd) {
// 执行修改密码逻辑
return Result.success("密码修改成功");
}
/**
* 输入密码,开启二级认证
*/
@PostMapping("/openSafe")
public Result openSafe(@RequestParam String password) {
// 验证密码
if (checkPassword(password)) {
// 开启二级认证,120 秒有效
StpUtil.openSafe(120);
return Result.success("二级认证开启成功");
}
return Result.error("密码错误");
}
}
五、实现原理深度解析
5.1 核心架构设计
5.1.1 整体架构
Sa-Token 采用分层架构设计:
应用层 (Controller/Service)
↓
StpUtil (工具类门面层)
↓
StpLogic (核心逻辑层)
↓
SaTokenDao (数据持久层接口)
↓
具体实现 (SaTokenDaoDefaultImpl/SaTokenDaoRedis等)
5.1.2 核心类说明
| 类名 | 说明 |
|---|---|
| StpUtil | 工具类门面,提供静态方法供业务层调用 |
| StpLogic | 核心逻辑实现,所有的鉴权逻辑都在这里 |
| SaTokenDao | 数据持久层接口,定义了数据存取规范 |
| SaTokenConfig | 配置类,负责读取和存储配置信息 |
| SaSession | Session 模型,封装会话数据 |
| SaTokenInfo | Token 信息封装类 |
| StpInterface | 权限接口,业务层需实现此接口提供权限数据 |
| SaStrategy | 策略类,提供各种自定义策略 |
5.2 登录认证原理
5.2.1 登录流程
// 用户调用登录方法
StpUtil.login(10001);
// 内部执行流程:
public void login(Object id) {
// 1. 生成 Token
String tokenValue = createToken(id);
// 2. 将 Token 与账号 id 的映射关系保存到持久层
// Key: satoken:login:token:${tokenValue}
// Value: ${loginId}
setTokenValue(tokenValue, id);
// 3. 将账号 id 与 Token 的映射关系保存到持久层(支持多设备登录)
// Key: satoken:login:session:${loginId}
// Value: [token1, token2, ...]
setLoginIdToToken(id, tokenValue);
// 4. 将 Token 写入到 Cookie/Header 返回给前端
setTokenToCookie(tokenValue);
// 5. 触发登录监听器
SaTokenListener.onLogin(id);
}
5.2.2 Token 生成策略
Sa-Token 支持多种 Token 生成策略:
public interface SaTokenGenerate {
/**
* 生成 Token
*/
String createToken(Object loginId, String loginType);
}
默认实现:
// UUID 风格
public String createToken(Object loginId, String loginType) {
return UUID.randomUUID().toString();
}
// Simple-UUID 风格(去掉中划线)
public String createToken(Object loginId, String loginType) {
return UUID.randomUUID().toString().replaceAll("-", "");
}
// 雪花算法
public String createToken(Object loginId, String loginType) {
return String.valueOf(snowflakeIdWorker.nextId());
}
// JWT 风格
public String createToken(Object loginId, String loginType) {
return JWTUtil.createToken(loginId, loginType);
}
5.2.3 身份验证流程
// 用户调用验证方法
StpUtil.checkLogin();
// 内部执行流程:
public void checkLogin() {
// 1. 从请求中获取 Token(优先级:Header > Cookie > Parameter)
String tokenValue = getTokenValue();
// 2. 如果 Token 为空,抛出未登录异常
if(tokenValue == null || tokenValue.equals("")) {
throw new NotLoginException();
}
// 3. 根据 Token 从持久层获取 loginId
Object loginId = getLoginIdByToken(tokenValue);
// 4. 如果 loginId 为空,说明 Token 已过期或无效
if(loginId == null) {
throw new NotLoginException();
}
// 5. 验证通过,刷新 Token 活跃时间
updateLastActiveTime(tokenValue);
// 6. 返回 loginId
return loginId;
}
5.3 权限认证原理
5.3.1 权限数据模型
Sa-Token 的权限模型:
用户 (User)
↓ 1:N
角色 (Role)
↓ 1:N
权限 (Permission)
5.3.2 权限验证流程
// 用户调用权限验证方法
StpUtil.checkPermission("user:add");
// 内部执行流程:
public void checkPermission(String permission) {
// 1. 获取当前登录用户 id
Object loginId = getLoginId();
// 2. 调用业务层实现的 StpInterface 接口,获取此用户的权限列表
List<String> permissionList = stpInterface.getPermissionList(loginId, loginType);
// 3. 判断权限列表中是否包含指定权限
if(!permissionList.contains(permission)) {
// 4. 如果不包含,抛出无权限异常
throw new NotPermissionException(permission);
}
// 5. 验证通过
}
5.3.3 角色验证流程
// 用户调用角色验证方法
StpUtil.checkRole("admin");
// 内部执行流程(与权限验证类似)
public void checkRole(String role) {
Object loginId = getLoginId();
List<String> roleList = stpInterface.getRoleList(loginId, loginType);
if(!roleList.contains(role)) {
throw new NotRoleException(role);
}
}
5.4 Session 原理
5.4.1 Session 模型
Sa-Token 的 Session 是一个分布式会话模型:
public class SaSession implements Serializable {
// Session 唯一标识
private String id;
// Session 中存储的数据
private Map<String, Object> dataMap = new ConcurrentHashMap<>();
// Session 创建时间
private long createTime;
// Session 最后活跃时间
private long lastAccessTime;
// Session 超时时间(-1 表示永不超时)
private long timeout = -1;
}
5.4.2 Session 存取流程
// 获取 Session
SaSession session = StpUtil.getSession();
// 内部执行流程:
public SaSession getSession() {
// 1. 获取当前登录用户 id
Object loginId = getLoginId();
// 2. 根据 loginId 生成 Session 的 key
String sessionKey = splicingSessionKey(loginId); // 例如:satoken:session:10001
// 3. 从持久层获取 Session
SaSession session = saTokenDao.getSession(sessionKey);
// 4. 如果 Session 不存在,创建新的 Session
if(session == null) {
session = new SaSession(sessionKey);
saTokenDao.setSession(sessionKey, session, timeout);
}
// 5. 返回 Session
return session;
}
// 存储数据
session.set("key", "value");
// 内部会调用 saTokenDao.updateSession() 将更新后的 Session 保存到持久层
// 获取数据
Object value = session.get("key");
// 内部从 Session 的 dataMap 中获取数据
5.4.3 Session 持久化
Session 数据通过 SaTokenDao 接口进行持久化:
public interface SaTokenDao {
/**
* 获取 Session
*/
SaSession getSession(String sessionId);
/**
* 保存 Session
*/
void setSession(String sessionId, SaSession session, long timeout);
/**
* 更新 Session
*/
void updateSession(String sessionId);
/**
* 删除 Session
*/
void deleteSession(String sessionId);
}
在 Redis 中的存储结构:
Key: satoken:session:10001
Value: {
"id": "satoken:session:10001",
"dataMap": {
"name": "张三",
"age": 18
},
"createTime": 1609459200000,
"lastAccessTime": 1609545600000,
"timeout": 2592000
}
Expire: 2592000 秒
5.5 持久层原理
5.5.1 SaTokenDao 接口
Sa-Token 通过 SaTokenDao 接口定义数据持久化规范:
public interface SaTokenDao {
// -------------------- String 数据类型操作 --------------------
/**
* 获取 Value
*/
String get(String key);
/**
* 写入 Value,并设定存活时间(单位:秒)
*/
void set(String key, String value, long timeout);
/**
* 更新 Value 的存活时间
*/
void update(String key, long timeout);
/**
* 删除 Value
*/
void delete(String key);
/**
* 获取 Value 的剩余存活时间(秒)
*/
long getTimeout(String key);
/**
* 修改 Value 的剩余存活时间(秒)
*/
void updateTimeout(String key, long timeout);
// -------------------- Object 数据类型操作 --------------------
/**
* 获取 Object
*/
Object getObject(String key);
/**
* 写入 Object,并设定存活时间(单位:秒)
*/
void setObject(String key, Object object, long timeout);
/**
* 更新 Object 的存活时间
*/
void updateObject(String key, long timeout);
/**
* 删除 Object
*/
void deleteObject(String key);
/**
* 获取 Object 的剩余存活时间(秒)
*/
long getObjectTimeout(String key);
/**
* 修改 Object 的剩余存活时间(秒)
*/
void updateObjectTimeout(String key, long timeout);
// -------------------- Session 数据类型操作 --------------------
/**
* 获取 Session
*/
SaSession getSession(String sessionId);
/**
* 写入 Session
*/
void setSession(String sessionId, SaSession session, long timeout);
/**
* 更新 Session
*/
void updateSession(String sessionId);
/**
* 删除 Session
*/
void deleteSession(String sessionId);
// -------------------- 会话管理 --------------------
/**
* 搜索数据
*/
List<String> searchData(String prefix, String keyword, int start, int size);
}
5.5.2 Redis 实现
Sa-Token 提供了基于 Redis 的持久化实现:
@Component
public class SaTokenDaoRedis implements SaTokenDao {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
@Override
public void set(String key, String value, long timeout) {
if(timeout == SaTokenDao.NEVER_EXPIRE) {
// 永不过期
stringRedisTemplate.opsForValue().set(key, value);
} else {
// 指定过期时间
stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
}
@Override
public void delete(String key) {
stringRedisTemplate.delete(key);
}
@Override
public long getTimeout(String key) {
Long expire = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
return expire == null ? SaTokenDao.NOT_VALUE_EXPIRE : expire;
}
// ... 其他方法实现
}
5.5.3 数据存储结构
在 Redis 中的数据存储结构:
1. Token → LoginId 映射
Key: satoken:login:token:${tokenValue}
Value: ${loginId}
Expire: ${timeout}
2. LoginId → Token 映射
Key: satoken:login:session:${loginId}
Value: Set<tokenValue>
Expire: ${timeout}
3. User-Session
Key: satoken:session:${loginId}
Value: SaSession 对象(JSON 序列化)
Expire: ${timeout}
4. Token-Session
Key: satoken:token:${tokenValue}
Value: SaSession 对象(JSON 序列化)
Expire: ${timeout}
5. 账号封禁
Key: satoken:disable:${loginId}
Value: ${disableTime}
Expire: ${disableTime}
5.6 拦截器与过滤器
5.6.1 Sa-Token 路由拦截器
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 路由拦截器
registry.addInterceptor(new SaInterceptor(handle -> {
// 指定哪些路由需要拦截
SaRouter.match("/**")
.notMatch("/auth/login") // 排除登录接口
.notMatch("/auth/register") // 排除注册接口
.notMatch("/favicon.ico") // 排除图标
.check(r -> StpUtil.checkLogin()); // 检查登录状态
// 权限认证 - 不同角色不同权限
SaRouter.match("/admin/**", r -> StpUtil.checkRole("admin"));
SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
})).addPathPatterns("/**");
}
}
5.6.2 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 未登录异常
*/
@ExceptionHandler(NotLoginException.class)
public Result handleNotLoginException(NotLoginException e) {
String message = "";
if(e.getType().equals(NotLoginException.NOT_TOKEN)) {
message = "未提供 Token";
} else if(e.getType().equals(NotLoginException.INVALID_TOKEN)) {
message = "Token 无效";
} else if(e.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
message = "Token 已过期";
} else if(e.getType().equals(NotLoginException.BE_REPLACED)) {
message = "Token 已被顶下线";
} else if(e.getType().equals(NotLoginException.KICK_OUT)) {
message = "Token 已被踢下线";
} else {
message = "当前会话未登录";
}
return Result.error(401, message);
}
/**
* 无权限异常
*/
@ExceptionHandler(NotPermissionException.class)
public Result handleNotPermissionException(NotPermissionException e) {
return Result.error(403, "无此权限:" + e.getPermission());
}
/**
* 无角色异常
*/
@ExceptionHandler(NotRoleException.class)
public Result handleNotRoleException(NotRoleException e) {
return Result.error(403, "无此角色:" + e.getRole());
}
/**
* 账号被封禁异常
*/
@ExceptionHandler(DisableServiceException.class)
public Result handleDisableServiceException(DisableServiceException e) {
return Result.error(403, "账号已被封禁,剩余时间:" + e.getDisableTime() + "秒");
}
}
六、高级特性
6.1 多账号体系认证
Sa-Token 支持在一个项目中同时存在多套账号体系(如:用户账号、管理员账号)
6.1.1 定义多个 StpLogic
// 用户账号体系
@Component
public class StpUserUtil {
public static final StpLogic stpLogic = new StpLogic("user");
public static void login(Object id) {
stpLogic.login(id);
}
public static void checkLogin() {
stpLogic.checkLogin();
}
// ... 其他方法
}
// 管理员账号体系
@Component
public class StpAdminUtil {
public static final StpLogic stpLogic = new StpLogic("admin");
public static void login(Object id) {
stpLogic.login(id);
}
public static void checkLogin() {
stpLogic.checkLogin();
}
// ... 其他方法
}
6.1.2 使用示例
// 用户登录
StpUserUtil.login(10001);
// 管理员登录
StpAdminUtil.login(20001);
// 用户权限验证
StpUserUtil.checkLogin();
// 管理员权限验证
StpAdminUtil.checkLogin();
6.2 JWT 集成
6.2.1 引入 JWT 依赖
<!-- Sa-Token 整合 JWT -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>1.44.0</version>
</dependency>
6.2.2 配置 JWT
sa-token:
# JWT 密钥
jwt-secret-key: your-secret-key-here
# Token 风格改为 JWT
token-style: jwt
6.2.3 JWT 模式
Sa-Token 提供两种 JWT 集成模式:
Simple 模式(推荐):
// 配置
@Configuration
public class SaTokenConfig {
@Bean
@Primary
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForSimple();
}
}
// 使用(与普通模式完全一致)
StpUtil.login(10001);
String token = StpUtil.getTokenValue();
Stateless 模式(无状态):
// 配置
@Configuration
public class SaTokenConfig {
@Bean
@Primary
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForStateless();
}
}
// 使用
StpUtil.login(10001);
String token = StpUtil.getTokenValue(); // JWT Token
6.2.4 自定义 JWT Payload
// 登录时自定义 JWT 载荷
StpUtil.login(10001, SaLoginConfig
.setExtra("username", "zhangsan")
.setExtra("email", "zhangsan@example.com")
);
// 获取 JWT 载荷
String username = StpUtil.getExtra("username").toString();
6.3 OAuth2.0 授权
Sa-Token 提供完整的 OAuth2.0 Server 实现。
6.3.1 引入 OAuth2 依赖
<!-- Sa-Token OAuth2.0 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-oauth2</artifactId>
<version>1.44.0</version>
</dependency>
6.3.2 实现 OAuth2 接口
@Component
public class SaOAuth2TemplateImpl extends SaOAuth2Template {
/**
* 根据 ClientId 获取 Client 信息
*/
@Override
public SaClientModel getClientModel(String clientId) {
// 从数据库查询 Client 信息
return new SaClientModel()
.setClientId(clientId)
.setClientSecret("client-secret")
.setAllowUrl("*") // 允许的回调地址
.setContractScope("userinfo") // 允许的授权范围
.setIsAutoMode(true); // 是否自动授权
}
/**
* 根据 Code 生成 Access Token
*/
@Override
public SaTokenInfo generateAccessToken(String code) {
// 默认实现即可
return super.generateAccessToken(code);
}
}
6.3.3 OAuth2 控制器
@RestController
@RequestMapping("/oauth2")
public class OAuth2Controller {
/**
* OAuth2 授权入口
*/
@GetMapping("/authorize")
public Object authorize(SaRequest req, SaResponse res) {
return SaOAuth2Util.authorize(req, res);
}
/**
* OAuth2 Token 获取
*/
@PostMapping("/token")
public Object token(SaRequest req) {
return SaOAuth2Util.token(req);
}
/**
* 刷新 Access Token
*/
@PostMapping("/refresh")
public Object refresh(SaRequest req) {
return SaOAuth2Util.refresh(req);
}
/**
* 撤销 Access Token
*/
@PostMapping("/revoke")
public Object revoke(SaRequest req) {
return SaOAuth2Util.revoke(req);
}
/**
* 获取用户信息
*/
@GetMapping("/userinfo")
public Object userinfo() {
// 校验 Access Token
SaOAuth2Util.checkAccessToken();
// 获取 loginId
Object loginId = StpUtil.getLoginId();
// 返回用户信息
return Result.success(getUserInfo(loginId));
}
}
6.4 单点登录(SSO)
Sa-Token 提供三种 SSO 模式。
6.4.1 模式一:共享 Cookie(同域)
适用场景:多个系统部署在同一个主域名下
sa-token:
# Cookie 的作用域设置为主域名
cookie-domain: example.com
6.4.2 模式二:URL 重定向(跨域)
适用场景:多个系统部署在不同域名下
认证中心配置:
<!-- Sa-Token SSO 服务端 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso-server</artifactId>
<version>1.44.0</version>
</dependency>
@RestController
public class SsoServerController {
/**
* SSO 认证中心首页
*/
@GetMapping("/sso/auth")
public Object ssoAuth(SaRequest req, SaResponse res) {
return SaSsoUtil.ssoAuth(req, res);
}
/**
* SSO 登录页面
*/
@GetMapping("/sso/doLogin")
public Object ssoDoLogin(@RequestParam String name, @RequestParam String pwd) {
// 验证用户名密码
if("admin".equals(name) && "123456".equals(pwd)) {
StpUtil.login(10001);
return SaSsoUtil.ssoDoLogin();
}
return "用户名或密码错误";
}
/**
* 校验 Ticket
*/
@GetMapping("/sso/checkTicket")
public Object checkTicket(@RequestParam String ticket, @RequestParam String ssoLogoutCall) {
return SaSsoUtil.checkTicket(ticket, ssoLogoutCall);
}
}
客户端配置:
<!-- Sa-Token SSO 客户端 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso-client</artifactId>
<version>1.44.0</version>
</dependency>
sa-token:
sso:
# SSO 认证中心地址
auth-url: http://sso.example.com/sso/auth
# 当前系统的回调地址
sso-logout-call: http://client1.example.com/sso/logout
@RestController
public class SsoClientController {
/**
* SSO 登录回调
*/
@GetMapping("/sso/login")
public Object ssoLogin(@RequestParam String ticket) {
// 使用 Ticket 换取用户信息
Object loginId = SaSsoUtil.checkTicket(ticket);
// 登录成功
StpUtil.login(loginId);
return "登录成功";
}
/**
* SSO 登出回调
*/
@GetMapping("/sso/logout")
public Object ssoLogout(@RequestParam String loginId) {
// 登出
StpUtil.logout(loginId);
return "登出成功";
}
}
6.4.3 模式三:Http 请求(跨域)
基于 Http 请求获取会话,不使用重定向。
6.5 微服务网关鉴权
6.5.1 Gateway 网关集成
<!-- Sa-Token 整合 Spring Cloud Gateway -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
<version>1.44.0</version>
</dependency>
@Configuration
public class SaTokenConfig {
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 拦截全部路径
.addInclude("/**")
// 排除登录接口
.addExclude("/auth/login")
// 鉴权方法
.setAuth(obj -> {
// 检查登录状态
SaRouter.match("/**", r -> StpUtil.checkLogin());
// 权限认证
SaRouter.match("/admin/**", r -> StpUtil.checkRole("admin"));
})
// 异常处理
.setError(e -> {
return SaResult.error(e.getMessage());
});
}
}
6.5.2 Dubbo RPC 鉴权
<!-- Sa-Token 整合 Dubbo -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dubbo</artifactId>
<version>1.44.0</version>
</dependency>
@Component
public class SaTokenDubboFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) {
try {
// 从上下文获取 Token
String token = RpcContext.getContext().getAttachment("satoken");
// 临时设置 Token
StpUtil.setTokenValue(token);
// 校验登录
StpUtil.checkLogin();
// 执行 RPC 调用
return invoker.invoke(invocation);
} finally {
// 清除临时 Token
StpUtil.clearTokenValue();
}
}
}
6.6 全局监听器
Sa-Token 提供了全局监听器,可以监听用户的各种行为。
@Component
public class SaTokenListener implements SaTokenEventListener {
/**
* 每次登录时触发
*/
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
System.out.println("用户 " + loginId + " 登录成功,Token:" + tokenValue);
// 记录登录日志
}
/**
* 每次注销时触发
*/
@Override
public void doLogout(String loginType, Object loginId, String tokenValue) {
System.out.println("用户 " + loginId + " 注销登录,Token:" + tokenValue);
// 记录注销日志
}
/**
* 每次被踢下线时触发
*/
@Override
public void doKickout(String loginType, Object loginId, String tokenValue) {
System.out.println("用户 " + loginId + " 被踢下线,Token:" + tokenValue);
// 发送通知
}
/**
* 每次被顶下线时触发
*/
@Override
public void doReplaced(String loginType, Object loginId, String tokenValue) {
System.out.println("用户 " + loginId + " 被顶下线,Token:" + tokenValue);
// 发送通知
}
/**
* 每次被封禁时触发
*/
@Override
public void doDisable(String loginType, Object loginId, long disableTime) {
System.out.println("用户 " + loginId + " 被封禁,封禁时长:" + disableTime + "秒");
// 发送通知
}
/**
* 每次被解封时触发
*/
@Override
public void doUntieDisable(String loginType, Object loginId) {
System.out.println("用户 " + loginId + " 被解封");
// 发送通知
}
/**
* 每次创建 Session 时触发
*/
@Override
public void doCreateSession(String id) {
System.out.println("创建 Session:" + id);
}
/**
* 每次注销 Session 时触发
*/
@Override
public void doLogoutSession(String id) {
System.out.println("注销 Session:" + id);
}
}
七、最佳实践
7.1 前后端分离架构
7.1.1 Token 传递方式
方式一:Header 方式(推荐)
前端:
axios.get('/api/user/info', {
headers: {
'satoken': localStorage.getItem('satoken')
}
});
后端配置:
sa-token:
# 从 Header 中读取 Token
is-read-header: true
# Token 名称
token-name: satoken
方式二:请求参数方式
前端:
axios.get('/api/user/info?satoken=' + localStorage.getItem('satoken'));
7.1.2 完整示例
前端代码(Vue3):
// api/auth.js
import axios from 'axios';
// 创建 axios 实例
const request = axios.create({
baseURL: 'http://localhost:8080',
timeout: 5000
});
// 请求拦截器 - 添加 Token
request.interceptors.request.use(
config => {
const token = localStorage.getItem('satoken');
if (token) {
config.headers['satoken'] = token;
}
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器 - 处理未登录
request.interceptors.response.use(
response => {
return response.data;
},
error => {
if (error.response && error.response.status === 401) {
// 未登录,跳转到登录页
localStorage.removeItem('satoken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// 登录
export function login(username, password) {
return request({
url: '/auth/login',
method: 'post',
data: { username, password }
});
}
// 获取用户信息
export function getUserInfo() {
return request({
url: '/auth/userInfo',
method: 'get'
});
}
// 退出登录
export function logout() {
return request({
url: '/auth/logout',
method: 'post'
});
}
登录组件:
<template>
<div class="login-container">
<el-form :model="loginForm" :rules="rules" ref="loginFormRef">
<el-form-item prop="username">
<el-input v-model="loginForm.username" placeholder="用户名"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="密码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { login } from '@/api/auth';
import { ElMessage } from 'element-plus';
const router = useRouter();
const loginFormRef = ref(null);
const loginForm = ref({
username: '',
password: ''
});
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
};
const handleLogin = async () => {
await loginFormRef.value.validate(async (valid) => {
if (valid) {
try {
const res = await login(loginForm.value.username, loginForm.value.password);
if (res.code === 200) {
// 保存 Token
localStorage.setItem('satoken', res.data);
ElMessage.success('登录成功');
// 跳转到首页
router.push('/');
} else {
ElMessage.error(res.message);
}
} catch (error) {
ElMessage.error('登录失败');
}
}
});
};
</script>
后端代码:
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private UserService userService;
@PostMapping("/login")
public Result login(@RequestBody LoginReq req) {
// 验证用户名密码
User user = userService.getUserByUsername(req.getUsername());
if (user == null) {
return Result.error("用户不存在");
}
// 验证密码(使用 BCrypt)
if (!BCrypt.checkpw(req.getPassword(), user.getPassword())) {
return Result.error("密码错误");
}
// 检查账号是否被封禁
StpUtil.checkDisable(user.getId());
// 登录
StpUtil.login(user.getId(), new SaLoginModel()
.setDevice(req.getDevice()) // 设备类型
.setTimeout(60 * 60 * 24 * 7) // 7 天有效期
);
// 将用户信息存入 Session
SaSession session = StpUtil.getSession();
session.set("username", user.getUsername());
session.set("nickname", user.getNickname());
session.set("avatar", user.getAvatar());
// 返回 Token
return Result.success(StpUtil.getTokenValue());
}
@GetMapping("/userInfo")
public Result getUserInfo() {
// 检查登录
StpUtil.checkLogin();
// 从 Session 获取用户信息
SaSession session = StpUtil.getSession();
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("userId", StpUtil.getLoginIdAsLong());
userInfo.put("username", session.get("username"));
userInfo.put("nickname", session.get("nickname"));
userInfo.put("avatar", session.get("avatar"));
return Result.success(userInfo);
}
@PostMapping("/logout")
public Result logout() {
StpUtil.logout();
return Result.success("退出成功");
}
}
7.2 生产环境配置
7.2.1 Redis 集群配置
spring:
redis:
# Redis 集群配置
cluster:
nodes:
- 192.168.1.101:6379
- 192.168.1.102:6379
- 192.168.1.103:6379
max-redirects: 3
password: your-redis-password
lettuce:
pool:
max-active: 200
max-idle: 50
min-idle: 10
max-wait: -1ms
7.2.2 安全配置
sa-token:
# Token 有效期(7 天)
timeout: 604800
# Token 最低活跃时间(1 小时无操作则需要重新登录)
active-timeout: 3600
# 不允许同一账号并发登录
is-concurrent: false
# 每次登录不共用 Token
is-share: false
# 使用安全的 Token 生成策略
token-style: random-128
# JWT 密钥(请使用强密码)
jwt-secret-key: your-very-strong-secret-key-here
# 开启日志
is-log: true
7.2.3 性能优化
缓存权限数据:
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private UserMapper userMapper;
/**
* 获取用户权限列表(带缓存)
*/
@Cacheable(value = "user:permissions", key = "#userId")
public List<String> getPermissionsByUserId(Long userId) {
return userMapper.selectPermissionsByUserId(userId);
}
/**
* 获取用户角色列表(带缓存)
*/
@Cacheable(value = "user:roles", key = "#userId")
public List<String> getRolesByUserId(Long userId) {
return userMapper.selectRolesByUserId(userId);
}
}
批量验证:
// 批量踢下线
public void kickoutBatch(List<Long> userIds) {
for (Long userId : userIds) {
StpUtil.kickout(userId);
}
}
// 批量封禁
public void disableBatch(List<Long> userIds, long disableTime) {
for (Long userId : userIds) {
StpUtil.disable(userId, disableTime);
}
}
7.3 安全建议
7.3.1 密码加密
@Service
public class UserService {
/**
* 用户注册
*/
public void register(String username, String password) {
// 使用 BCrypt 加密密码
String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt(12));
// 保存用户
User user = new User();
user.setUsername(username);
user.setPassword(hashedPassword);
userMapper.insert(user);
}
/**
* 验证密码
*/
public boolean checkPassword(String rawPassword, String hashedPassword) {
return BCrypt.checkpw(rawPassword, hashedPassword);
}
}
7.3.2 防止暴力破解
@Component
public class LoginAttemptService {
private static final int MAX_ATTEMPT = 5;
private LoadingCache<String, Integer> attemptsCache;
public LoginAttemptService() {
attemptsCache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String key) {
return 0;
}
});
}
/**
* 登录成功,清除失败次数
*/
public void loginSucceeded(String key) {
attemptsCache.invalidate(key);
}
/**
* 登录失败,增加失败次数
*/
public void loginFailed(String key) {
int attempts = attemptsCache.getUnchecked(key);
attempts++;
attemptsCache.put(key, attempts);
}
/**
* 检查是否被锁定
*/
public boolean isBlocked(String key) {
return attemptsCache.getUnchecked(key) >= MAX_ATTEMPT;
}
}
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private LoginAttemptService loginAttemptService;
@PostMapping("/login")
public Result login(@RequestBody LoginReq req, HttpServletRequest request) {
String ip = getClientIP(request);
// 检查是否被锁定
if (loginAttemptService.isBlocked(ip)) {
return Result.error("登录失败次数过多,请 1 小时后再试");
}
// 验证用户名密码
User user = userService.getUserByUsername(req.getUsername());
if (user == null || !userService.checkPassword(req.getPassword(), user.getPassword())) {
// 记录登录失败
loginAttemptService.loginFailed(ip);
return Result.error("用户名或密码错误");
}
// 登录成功
loginAttemptService.loginSucceeded(ip);
StpUtil.login(user.getId());
return Result.success(StpUtil.getTokenValue());
}
}
7.3.3 防止 CSRF 攻击
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Bean
public SaInterceptor getSaInterceptor() {
return new SaInterceptor(handle -> {
// 开启 CSRF 防护
SaRouter.match("/**")
.notMatch("/auth/login")
.check(r -> {
// 检查 CSRF Token
SaCsrfUtil.checkToken();
});
});
}
}
八、总结
8.1 核心优势
- 轻量级:核心代码仅几百 KB,依赖少,性能高
- 易上手:API 简洁直观,文档详细,学习成本低
- 功能全:覆盖权限认证的各个场景
- 扩展性强:提供多种扩展点,灵活定制
- 生态丰富:支持多种集成方案
8.2 适用场景
- 中小型项目快速开发
- 微服务架构的统一鉴权
- 前后端分离项目
- 单点登录系统
- OAuth2.0 授权服务
8.3 学习资源
- 官方文档:sa-token.cc/
- GitHub:github.com/dromara/Sa-…
- Gitee:gitee.com/dromara/sa-…
- 视频教程:B 站搜索”Sa-Token”
8.4 社区支持
Sa-Token 拥有活跃的社区,提供:
- QQ 交流群
- 微信交流群
- Issue 提问
- PR 贡献
附录
A. 常见问题
Q1:Sa-Token 和 JWT 有什么区别?
A:JWT 是一种 Token 生成标准,Sa-Token 是一个权限认证框架。Sa-Token 可以使用 JWT 作为 Token 生成策略,也可以使用其他策略。
Q2:Sa-Token 支持分布式环境吗?
A:支持。通过 Redis 等分布式存储,Sa-Token 可以在分布式环境下使用。
Q3:如何实现单设备登录?
A:设置 is-concurrent: false 即可实现同一账号只能在一个设备登录。
Q4:如何自定义 Token 有效期?
A:在登录时使用 SaLoginModel 指定有效期:
StpUtil.login(10001, new SaLoginModel().setTimeout(3600));
Q5:如何实现记住我功能?
A:设置较长的 Token 有效期,并使用持久化 Cookie:
StpUtil.login(10001, new SaLoginModel()
.setTimeout(60 * 60 * 24 * 7) // 7 天
.setIsLastingCookie(true) // 持久化 Cookie
);