Sa-Token 1.37.x 完全指南
概述
Sa-Token 是一个轻量级 Java 权限认证框架,主要解决登录认证、权限验证、Session 会话、单点登录、OAuth2 等一系列权限相关问题。相比 Spring Security 和 Shiro,Sa-Token 的 API 调用非常简单,学习成本极低。
兼容性:Sa-Token 1.37.x 基于 JDK 8+,与 Spring Boot 2.7.x 完全兼容。
框架对比
| 对比维度 | Sa-Token | Spring Security | Shiro |
|---|---|---|---|
| 上手难度 | 🟢 极低 | 🔴 高 | 🟡 中 |
| API 简洁度 | 🟢 一行代码搞定 | 🔴 配置复杂 | 🟡 中 |
| 注解鉴权 | 🟢 支持 | 🟢 支持 | 🟢 支持 |
| OAuth2 | 🟢 内置 | 🟡 需 Spring Security OAuth2 | 🔴 需额外集成 |
| 分布式 Session | 🟢 内置 Redis 支持 | 🔴 需 Spring Session | 🔴 需额外配置 |
| 文档质量 | 🟢 详细 | 🟢 官方文档丰富 | 🟡 中 |
一、快速开始
1.1 Maven 依赖
xml
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.37.0</version>
</dependency>1.2 application.yml
yaml
# Sa-Token 配置
sa-token:
# Token 名称(同时也是 Cookie 名称)
token-name: satoken
# Token 有效期(秒),默认 30 天
timeout: 2592000
# Token 临时有效期(指定时间内无操作则过期)
activity-timeout: -1
# 是否允许同一账号并发登录
is-concurrent: true
# 是否共享同一账号的 Token
is-share: true
# Token 风格
token-style: uuid
# 是否输出操作日志
is-log: true1.3 第一个登录示例
java
@RestController
@RequestMapping("/api")
public class LoginController {
/**
* 登录
*/
@PostMapping("/login")
public Result<String> login(@RequestBody LoginRequest request) {
// 1. 校验用户名密码(略)
if (!"admin".equals(request.getUsername()) || !"123456".equals(request.getPassword())) {
return Result.error("账号或密码错误");
}
// 2. 执行登录(一行代码)
StpUtil.login(10001); // 参数为 userId
// 3. 返回 Token
String token = StpUtil.getTokenValue();
return Result.success(token);
}
/**
* 检查登录状态
*/
@GetMapping("/isLogin")
public Result<Boolean> isLogin() {
return Result.success(StpUtil.isLogin());
}
/**
* 退出登录
*/
@PostMapping("/logout")
public Result<String> logout() {
StpUtil.logout();
return Result.success("退出成功");
}
}二、登录认证
2.1 StpUtil 核心方法
| 方法 | 说明 |
|---|---|
StpUtil.login(id) | 登录,参数为账号 ID |
StpUtil.logout() | 退出当前会话 |
StpUtil.logout(id) | 踢出指定账号 |
StpUtil.isLogin() | 是否已登录 |
StpUtil.getLoginId() | 获取当前登录账号 ID |
StpUtil.getLoginIdAsString() | 获取 String 类型的账号 ID |
StpUtil.getLoginIdAsLong() | 获取 Long 类型的账号 ID |
StpUtil.getLoginIdAsInt() | 获取 Int 类型的账号 ID |
StpUtil.getTokenValue() | 获取当前 Token 值 |
StpUtil.getTokenInfo() | 获取当前 Token 详细信息 |
2.2 自定义登录逻辑
java
@Component
public class StpInterfaceImpl implements StpInterface {
/**
* 返回指定账号的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 从数据库查询权限列表
List<String> list = new ArrayList<>();
list.add("user:add");
list.add("user:update");
list.add("user:delete");
return list;
}
/**
* 返回指定账号的角色集合
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 从数据库查询角色列表
List<String> list = new ArrayList<>();
list.add("admin");
list.add("super-admin");
return list;
}
}2.3 多账号体系
Sa-Token 支持多账号体系(如:用户端、管理端分开):
java
// 用户端登录
StpUtil.login(10001);
StpUtil.logout();
// 管理端登录(使用 loginType 区分)
StpAdminUtil.login(10001);
StpAdminUtil.logout();三、权限验证
3.1 注解鉴权
java
@RestController
@RequestMapping("/api/user")
public class UserController {
/**
* 必须登录才能访问
*/
@SaCheckLogin
@GetMapping("/profile")
public Result<UserVO> profile() {
Long userId = StpUtil.getLoginIdAsLong();
return Result.success(userService.getById(userId));
}
/**
* 必须具备指定权限才能访问
*/
@SaCheckPermission("user:add")
@PostMapping
public Result<String> addUser(@RequestBody UserDTO dto) {
userService.add(dto);
return Result.success("添加成功");
}
/**
* 必须具备任一权限(OR 逻辑)
*/
@SaCheckPermission(value = {"user:update", "user:admin"}, mode = SaMode.OR)
@PutMapping
public Result<String> updateUser(@RequestBody UserDTO dto) {
userService.update(dto);
return Result.success("更新成功");
}
/**
* 必须具备指定角色
*/
@SaCheckRole("admin")
@DeleteMapping("/{id}")
public Result<String> deleteUser(@PathVariable Long id) {
userService.delete(id);
return Result.success("删除成功");
}
/**
* 具备任一角色
*/
@SaCheckRole(value = {"admin", "super-admin"}, mode = SaMode.OR)
@GetMapping("/list")
public Result<List<UserVO>> listUsers() {
return Result.success(userService.list());
}
/**
* 二级认证(敏感操作需再次验证密码)
*/
@SaCheckSafe
@PostMapping("/reset-password")
public Result<String> resetPassword(@RequestBody ResetPasswordRequest request) {
userService.resetPassword(request);
return Result.success("重置成功");
}
}3.2 代码鉴权
java
// 判断是否登录
StpUtil.isLogin();
// 判断是否具有指定权限
StpUtil.hasPermission("user:add");
// 校验权限(不通过则抛出异常)
StpUtil.checkPermission("user:add");
// 判断是否具有指定角色
StpUtil.hasRole("admin");
// 校验角色
StpUtil.checkRole("admin");
// 判断是否具有任一权限
StpUtil.hasPermissionOr("user:add", "user:update");
// 判断是否具有全部权限
StpUtil.hasPermissionAnd("user:add", "user:update");四、Session 管理
4.1 Token 会话
java
// 登录后,Sa-Token 自动创建会话
// 获取当前会话
SaSession session = StpUtil.getSession();
// 设置会话属性
session.set("username", "admin");
session.set("loginTime", new Date());
// 获取会话属性
String username = (String) session.get("username");
// 获取指定账号的会话
SaSession userSession = StpUtil.getSessionByLoginId(10001);4.2 自定义 Session 存储
java
// 获取 Token 专属 Session
SaSession tokenSession = StpUtil.getTokenSession();
// 获取自定义 Session
SaSession customSession = SaSessionCustomUtil.getSessionById("custom-key");
customSession.set("data", "value");五、Redis 集成(分布式 Session)
5.1 Maven 依赖
xml
<!-- Sa-Token Redis 集成 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.37.0</version>
</dependency>
<!-- Spring Boot Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>5.2 application.yml
yaml
spring:
redis:
host: localhost
port: 6379
password:
database: 0
sa-token:
# 使用 Redis 存储 Token
alone-redis:
host: localhost
port: 6379
database: 1六、JWT 集成(无状态认证)
6.1 Maven 依赖
xml
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>1.37.0</version>
</dependency>6.2 配置
yaml
sa-token:
# JWT 密钥
jwt-secret-key: abcdefghijklmnopqrstuvwxyz1234566.3 使用方式
java
// 登录(与普通模式完全一样)
StpUtil.login(10001);
// 获取 Token(此时返回的是 JWT 字符串)
String token = StpUtil.getTokenValue();
// 格式:eyJhbGciOiJIUzI1NiJ9.eyJsb2dpbklkIjoiMTAwMDEi...
// 前端将 Token 放在请求头中
// Authorization: Bearer eyJhbGciOi...JWT 模式的限制
JWT 模式下 Token 是无状态的,服务端无法主动踢人下线、无法修改 Token 有效期。如需这些功能,请使用 Redis 模式。
七、OAuth2 集成
7.1 授权码模式
java
// 第一步:构建授权 URL,引导用户跳转
String authorizeUrl = SaOAuth2Util.buildAuthorizeUrl(
"http://oauth-server.com/oauth2/authorize",
"client_id",
"http://client.com/callback"
);
// 第二步:回调接口,用授权码换取 Access Token
@GetMapping("/oauth2/callback")
public Result<String> callback(@RequestParam String code) {
// 用 code 换取 access_token
SaOAuth2AccessToken token = SaOAuth2Util.getAccessToken(
"http://oauth-server.com/oauth2/token",
"client_id",
"client_secret",
code
);
return Result.success(token.getAccessToken());
}7.2 密码模式
java
// 直接使用用户名密码获取 Token
SaOAuth2AccessToken token = SaOAuth2Util.getAccessTokenByPassword(
"http://oauth-server.com/oauth2/token",
"client_id",
"client_secret",
"username",
"password"
);八、路由拦截器(全局鉴权)
8.1 配置拦截器
java
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
/**
* 注册 Sa-Token 路由拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handler -> {
// 所有路由都需要登录
SaRouter.match("/**")
.check(r -> StpUtil.checkLogin());
// 排除登录接口
SaRouter.notMatch("/api/login")
.notMatch("/api/register")
.notMatch("/api/public/**");
// 指定路由需要特定权限
SaRouter.match("/api/admin/**")
.check(r -> StpUtil.checkPermission("admin"));
SaRouter.match("/api/user/**")
.check(r -> StpUtil.checkRoleOr("admin", "user"));
}))
.addPathPatterns("/**")
.excludePathPatterns("/api/login", "/api/register", "/api/public/**");
}
}8.2 注解注册拦截器
java
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注解鉴权拦截器
registry.addInterceptor(new SaInterceptor())
.addPathPatterns("/**");
}
}九、账号踢人与封禁
9.1 强制下线
java
// 将指定账号踢下线
StpUtil.logout(10001);
// 将指定账号踢下线(指定 Token)
StpUtil.logoutByTokenValue("token-value");
// 封禁账号(禁止登录)
StpUtil.disable(10001, 3600); // 封禁 3600 秒9.2 封禁检查
java
// 检查是否被封禁
StpUtil.isDisable(10001);
// 解封账号
StpUtil.untieDisable(10001);十、Spring Boot 2.7.x 完整集成示例
10.1 项目结构
text
src/main/java/com/example/demo/
├── config/
│ ├── SaTokenConfigure.java # 拦截器配置
│ └── StpInterfaceImpl.java # 权限加载
├── controller/
│ └── AuthController.java # 登录/注册
├── entity/
│ └── User.java
├── service/
│ └── UserService.java
└── DemoApplication.java10.2 权限加载实现
java
@Component
public class StpInterfaceImpl implements StpInterface {
@Autowired
private UserService userService;
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
Long userId = Long.valueOf(loginId.toString());
return userService.getUserPermissions(userId);
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
Long userId = Long.valueOf(loginId.toString());
return userService.getUserRoles(userId);
}
}10.3 登录 Controller
java
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class AuthController {
private final UserService userService;
/**
* 用户登录
*/
@PostMapping("/login")
public Result<Map<String, Object>> login(@RequestBody LoginRequest request) {
// 验证码校验(略)
// 校验用户名密码
User user = userService.getByUsername(request.getUsername());
if (user == null || !user.getPassword().equals(request.getPassword())) {
return Result.error("账号或密码错误");
}
// 检查账号状态
if (user.getStatus() == 0) {
return Result.error("账号已被禁用");
}
// 执行登录
StpUtil.login(user.getId());
// 获取 Token
String token = StpUtil.getTokenValue();
// 返回 Token 和用户信息
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("userInfo", userService.toVO(user));
return Result.success(result);
}
/**
* 获取当前用户信息
*/
@SaCheckLogin
@GetMapping("/user/info")
public Result<UserVO> getUserInfo() {
Long userId = StpUtil.getLoginIdAsLong();
return Result.success(userService.getUserVO(userId));
}
/**
* 退出登录
*/
@SaCheckLogin
@PostMapping("/logout")
public Result<String> logout() {
StpUtil.logout();
return Result.success("退出成功");
}
}10.4 前端调用示例
javascript
// 登录
const login = async () => {
const res = await axios.post('/api/login', {
username: 'admin',
password: '123456'
});
// 保存 Token
localStorage.setItem('token', res.data.data.token);
};
// 后续请求带 Token
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['satoken'] = token; // 默认 Header 名
}
return config;
});十一、常见问题
Q: Token 存放在 Cookie 还是 Header 中?
默认情况下,Sa-Token 会把 Token 同时放在 Cookie 和响应体中。前后端分离项目建议使用 Header 方式传递 Token。
Q: 如何修改 Token 传递的 Header 名称?
yaml
sa-token:
token-name: Authorization # 改为 AuthorizationQ: Token 过期了怎么办?
Sa-Token 会在每次请求时判断 Token 是否过期。如果设置了 activity-timeout,只要用户保持活跃,Token 就不会过期。
yaml
sa-token:
timeout: 604800 # 7 天绝对过期
activity-timeout: 1800 # 30 分钟无操作则过期Q: 跨域问题怎么解决?
java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true);
}
}Q: Sa-Token 与 Spring Security 能共存吗?
可以共存,但强烈不推荐。两者功能重叠,同时使用会增加复杂度。
Q: 如何实现"记住我"功能?
设置更长的 timeout:
java
// 登录时设置更长的有效期
StpUtil.login(10001, 60 * 60 * 24 * 30); // 30 天