正如官方文档所说的,Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证 、权限认证 、单点登录 、OAuth2.0 、分布式Session会话 、微服务网关鉴权 等一系列权限相关问题。
引言
在现代 Web 应用和微服务架构中,权限认证 是系统安全的第一道防线。一个优秀的权限认证框架不仅需要提供全面的安全保障,还应该具备简洁易用的 API 设计和良好的扩展性。Sa-Token 作为一款轻量级 Java 权限认证框架,以其 "简单、优雅" 的设计理念,彻底改变了 Java 开发者处理权限认证的方式
本文将深入解析 Sa-Token 中的核心技术,从最基础的登录认证到复杂的微服务网关鉴权,通过大量详实的代码案例,帮助读者全面掌握 Sa-Token 的使用方法和最佳实践。无论你是刚接触权限认证的新手,还是希望提升系统安全性的资深开发者,都能从本文中获得有价值的技术知识。
一、Sa-Token 快速入门
1.1 环境准备
Sa-Token 支持几乎所有主流的 Java 开发环境,包括:
- JDK 8+(推荐 JDK 17 及以上)
- Spring Boot 2.x/3.x/4.x
- Spring MVC、WebFlux、Solon 等 Web 框架
1.2 Spring Boot 集成
Sa-Token 支持在多种环境下进行集成,这里以 SpringBoot 框架为例,其他的框架集成可以看官方文档详情。
在 Spring Boot 项目中集成 Sa-Token 非常简单,只需要在 pom.xml中添加一个依赖:
XML
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.45.0</version>
</dependency>
你可以零配置启动项目 ,但同时你也可以在 application.yml 中增加如下配置,定制性使用框架:
bash
server:
port: 8085
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: false
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
token-style: uuid
# 是否输出操作日志
is-log: true
1.3 第一个示例:登录认证
让我们通过一个最简单的示例来体验 Sa-Token 的强大之处。创建一个 LoginController,实现基础的登录和登出功能:
java
@RestController
@RequestMapping("/auth")
public class LoginController {
/**
* 登录接口
* @param username 用户名
* @param password 密码
* @return 登录结果
*/
@PostMapping("/login")
public SaResult login(String username, String password) {
// 实际项目中应从数据库查询用户信息进行校验
if ("admin".equals(username) && "123456".equals(password)) {
// 登录成功,将用户ID写入会话
StpUtil.login(10001);
// 返回登录信息
return SaResult.ok("登录成功")
.set("token", StpUtil.getTokenValue())
.set("userId", StpUtil.getLoginId());
}
return SaResult.error("用户名或密码错误");
}
/**
* 登出接口
* @return 登出结果
*/
@PostMapping("/logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok("登出成功");
}
/**
* 获取当前登录用户信息
* @return 用户信息
*/
@GetMapping("/info")
public SaResult getUserInfo() {
// 校验登录状态
StpUtil.checkLogin();
// 返回用户信息
return SaResult.ok()
.set("userId", StpUtil.getLoginId())
.set("token", StpUtil.getTokenValue())
.set("isLogin", StpUtil.isLogin());
}
/**
* 测试是否登录
* @return 登录状态
*/
@GetMapping("/is-login")
public SaResult isLogin() {
return SaResult.ok()
.set("isLogin", StpUtil.isLogin());
}
}
启动项目后,我们可以通过以下步骤测试功能:
- 调用
POST /auth/login?username=admin&password=123456,获取登录 Token - 在请求头中携带Token(格式:
Authorization: Bearer {token}),调用GET /auth/info,成功获取用户信息 - 调用
POST /auth/logout,登出当前用户 - 再次调用
GET /auth/info,会抛出NotLoginException异常
仅仅几行代码,我们就完成了完整的登录认证流程,这就是 Sa-Token 的魅力所在。
二、登录认证核心技术
登录认证是权限系统的基础,Sa-Token 提供了丰富的登录认证功能,满足各种复杂的业务需求。
2.1 基础登录
StpUtil.login() 方法是 Sa-Token 登录认证的核心,它接受一个登录 ID 作为参数,这个 ID 可以是用户的主键、用户名或其他唯一标识。
java
// 基础登录
StpUtil.login(10001);
// 登录并指定设备类型(用于同端互斥登录)
StpUtil.login(10001, "PC");
StpUtil.login(10001, "APP");
StpUtil.login(10001, "MINI_PROGRAM");
// 登录并指定是否记住我
StpUtil.login(10001, true); // 记住我,有效期7天
StpUtil.login(10001, false); // 不记住我,浏览器关闭即失效
// 登录并指定设备类型和是否记住我
StpUtil.login(10001, "PC", true);
2.2 登录状态校验
Sa-Token 提供了多种方法来校验登录状态:
java
// 校验当前客户端是否已登录,未登录则抛出NotLoginException异常
StpUtil.checkLogin();
// 判断当前客户端是否已登录,返回boolean值
boolean isLogin = StpUtil.isLogin();
// 获取当前登录用户的ID,如果未登录则抛出异常
Object loginId = StpUtil.getLoginId();
// 获取当前登录用户的ID,转换为String类型
String loginIdStr = StpUtil.getLoginIdAsString();
// 获取当前登录用户的ID,转换为int类型
int loginIdInt = StpUtil.getLoginIdAsInt();
// 获取当前登录用户的ID,转换为long类型
long loginIdLong = StpUtil.getLoginIdAsLong();
// 获取当前登录用户的ID,如果未登录则返回默认值
Object loginIdDefault = StpUtil.getLoginIdDefault(null);
2.3 登出操作
Sa-Token 提供了多种登出方式:
java
// 登出当前客户端
StpUtil.logout();
// 登出指定用户的所有客户端
StpUtil.logout(10001);
// 登出指定用户的指定设备类型
StpUtil.logout(10001, "PC");
// 登出指定Token对应的客户端
StpUtil.logoutByTokenValue("token-xxxxxx");
2.4 Token 操作
Token 是 Sa-Token 身份认证的核心,Sa-Token 提供了丰富的 Token 操作方法:
java
// 获取当前登录用户的Token
String token = StpUtil.getTokenValue();
// 获取当前登录用户的Token信息(包含Token值、有效期等)
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
System.out.println("Token值:" + tokenInfo.getTokenValue());
System.out.println("Token有效期:" + tokenInfo.getTokenTimeout());
System.out.println("Token创建时间:" + tokenInfo.getTokenCreateTime());
System.out.println("用户ID:" + tokenInfo.getLoginId());
System.out.println("设备类型:" + tokenInfo.getDeviceType());
// 判断指定Token是否有效
boolean isValid = StpUtil.isValidToken("token-xxxxxx");
// 刷新Token的有效期
StpUtil.renewTimeout();
// 获取指定用户的Token列表
List<String> tokenList = StpUtil.getTokenListByLoginId(10001);
2.5 同端互斥登录
同端互斥登录是指同一账号在同一设备类型上只能登录一个客户端,例如一个账号不能同时在两个手机上登录。Sa-Token 提供了非常简单的实现方式:
bash
// 登录时指定设备类型
StpUtil.login(10001, "APP");
// 配置文件中开启同端互斥登录
// application.yml
sa-token:
is-concurrent: false # 禁止同一账号多端登录
is-share: false # 不同设备类型不共享Token
当同一账号在同一设备类型上再次登录时,之前登录的客户端会被自动踢下线。
2.6 记住我模式
记住我模式允许用户在关闭浏览器后再次打开网站时无需重新登录。Sa-Token 默认支持记住我模式,有效期为 7 天:
bash
// 登录时开启记住我模式
StpUtil.login(10001, true);
// 配置文件中修改记住我有效期
// application.yml
sa-token:
timeout: 2592000 # Token默认有效期,单位秒(30天)
remember-me-timeout: 604800 # 记住我模式有效期,单位秒(7天)
2.7 自动续签
Sa-Token 提供了两种 Token 过期策略,并支持自动续签:
bash
// 配置文件中开启自动续签
// application.yml
sa-token:
auto-renew: true # 开启自动续签
timeout: 1800 # Token有效期,单位秒(30分钟)
activity-timeout: 1800 # 活跃超时时间,单位秒(30分钟)
当 auto-renew 设置为 true 时,每次用户操作都会自动刷新 Token 的有效期。如果用户在activity-timeout 时间内没有任何操作,Token 会自动失效。
三、权限认证核心技术
权限认证是在登录认证的基础上,进一步控制用户对系统资源的访问权限。Sa-Token 提供了多种权限认证方式,包括代码鉴权 、注解鉴权 和路由拦截鉴权。
3.1 权限认证基础
Sa-Token 的权限认证基于 "用户 - 角色 - 权限" 模型(RBAC 模型),每个用户可以拥有多个角色,每个角色可以拥有多个权限。
首先,我们需要实现 StpInterface 接口,告诉 Sa-Token 某个用户拥有哪些权限和角色:
java
@Component
public class StpInterfaceImpl implements StpInterface {
/**
* 返回指定用户拥有的权限列表
* @param loginId 登录ID
* @param loginType 登录类型
* @return 权限列表
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 实际项目中应从数据库查询用户的权限
List<String> permissionList = new ArrayList<>();
// 假设用户ID为10001的是管理员,拥有所有权限
if (loginId.equals(10001)) {
permissionList.add("user:add");
permissionList.add("user:delete");
permissionList.add("user:update");
permissionList.add("user:query");
permissionList.add("admin:system");
}
// 假设用户ID为10002的是普通用户,只有查询权限
else if (loginId.equals(10002)) {
permissionList.add("user:query");
}
return permissionList;
}
/**
* 返回指定用户拥有的角色列表
* @param loginId 登录ID
* @param loginType 登录类型
* @return 角色列表
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 实际项目中应从数据库查询用户的角色
List<String> roleList = new ArrayList<>();
if (loginId.equals(10001)) {
roleList.add("admin");
roleList.add("super-admin");
}
else if (loginId.equals(10002)) {
roleList.add("user");
}
return roleList;
}
}
3.2 代码鉴权
代码鉴权是最基础的权限认证方式,通过调用 StpUtil的静态方法来校验权限:
java
// 校验当前用户是否拥有指定权限,没有则抛出NotPermissionException异常
StpUtil.checkPermission("user:add");
// 校验当前用户是否拥有指定权限,返回boolean值
boolean hasPermission = StpUtil.hasPermission("user:add");
// 校验当前用户是否拥有指定权限中的任意一个
StpUtil.checkPermissionOr("user:add", "user:update");
// 校验当前用户是否拥有指定权限中的所有
StpUtil.checkPermissionAnd("user:add", "user:delete");
// 校验当前用户是否拥有指定角色,没有则抛出NotRoleException异常
StpUtil.checkRole("admin");
// 校验当前用户是否拥有指定角色,返回boolean值
boolean hasRole = StpUtil.hasRole("admin");
// 校验当前用户是否拥有指定角色中的任意一个
StpUtil.checkRoleOr("admin", "super-admin");
// 校验当前用户是否拥有指定角色中的所有
StpUtil.checkRoleAnd("admin", "user");
3.3 注解鉴权
注解鉴权是最优雅的权限认证方式,通过在方法上添加注解来控制访问权限:
java
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 只有登录后才能访问
*/
@SaCheckLogin
@GetMapping("/list")
public SaResult getUserList() {
return SaResult.ok("获取用户列表成功");
}
/**
* 只有拥有"user:add"权限的用户才能访问
*/
@SaCheckPermission("user:add")
@GetMapping("/add")
public SaResult addUser() {
return SaResult.ok("添加用户成功");
}
/**
* 只有拥有"user:delete"权限的用户才能访问
*/
@SaCheckPermission("user:delete")
@GetMapping("/delete")
public SaResult deleteUser() {
return SaResult.ok("删除用户成功");
}
/**
* 只有拥有"admin"角色的用户才能访问
*/
@SaCheckRole("admin")
@GetMapping("/admin")
public SaResult adminPage() {
return SaResult.ok("管理员页面");
}
/**
* 拥有"admin"或"super-admin"角色的用户都能访问
*/
@SaCheckRole(or = {"admin", "super-admin"})
@GetMapping("/super-admin")
public SaResult superAdminPage() {
return SaResult.ok("超级管理员页面");
}
/**
* 必须同时拥有"user:add"和"user:delete"权限才能访问
*/
@SaCheckPermission(and = {"user:add", "user:delete"})
@GetMapping("/manage")
public SaResult userManage() {
return SaResult.ok("用户管理页面");
}
}
3.4 路由拦截鉴权
路由拦截鉴权是通过拦截器来统一控制路由的访问权限,适合对大量路由进行批量权限控制:
java
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
/**
* 注册Sa-Token拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handler -> {
// 所有请求都需要登录认证
SaRouter.match("/**")
.notMatch("/auth/login", "/auth/register", "/doc.html", "/webjars/**", "/v3/api-docs/**")
.check(r -> StpUtil.checkLogin());
// 用户模块权限控制
SaRouter.match("/user/add", "/user/delete", "/user/update")
.check(r -> StpUtil.checkPermission("user:manage"));
// 管理员模块权限控制
SaRouter.match("/admin/**")
.check(r -> StpUtil.checkRole("admin"));
// 超级管理员模块权限控制
SaRouter.match("/super-admin/**")
.check(r -> StpUtil.checkRole("super-admin"));
})).addPathPatterns("/**");
}
}
3.5 权限通配符
Sa-Token 支持权限通配符,可以更灵活地定义权限:
java
// 配置用户拥有"user:*"权限,表示拥有user模块的所有权限
permissionList.add("user:*");
// 校验是否拥有"user:add"权限,会匹配"user:*"
StpUtil.checkPermission("user:add"); // 通过
// 配置用户拥有"*:query"权限,表示拥有所有模块的查询权限
permissionList.add("*:query");
// 校验是否拥有"user:query"权限,会匹配"*:query"
StpUtil.checkPermission("user:query"); // 通过
// 配置用户拥有"*"权限,表示拥有所有权限(上帝权限)
permissionList.add("*");
// 校验任何权限都会通过
StpUtil.checkPermission("any:permission"); // 通过
3.6 二级认证
二级认证是在已登录的基础上,再次要求用户进行身份验证,以提高系统的安全性。例如,用户在修改密码、转账等敏感操作时,需要再次输入密码进行验证。
java
// 开启二级认证,有效期120秒
StpUtil.openSafe(120);
// 校验是否已经通过二级认证
StpUtil.checkSafe();
// 判断是否已经通过二级认证
boolean isSafe = StpUtil.isSafe();
// 关闭二级认证
StpUtil.closeSafe();
// 获取二级认证剩余有效期
long timeout = StpUtil.getSafeTime();
在实际项目中,我们可以这样使用二级认证:
java
/**
* 修改密码接口
*/
@PostMapping("/update-password")
public SaResult updatePassword(String oldPassword, String newPassword) {
// 校验登录状态
StpUtil.checkLogin();
// 校验是否已经通过二级认证
if (!StpUtil.isSafe()) {
return SaResult.error("请先进行二级认证");
}
// 校验旧密码
// ...
// 修改密码
// ...
return SaResult.ok("密码修改成功");
}
/**
* 二级认证接口
*/
@PostMapping("/safe-auth")
public SaResult safeAuth(String password) {
// 校验登录状态
StpUtil.checkLogin();
// 校验密码
// ...
// 开启二级认证,有效期5分钟
StpUtil.openSafe(300);
return SaResult.ok("二级认证成功");
}
四、会话管理核心技术
会话管理是权限系统的重要组成部分,Sa-Token 提供了强大的会话管理功能,支持 全端共享 Session 、单端独享 Session 、自定义 Session等多种模式。
4.1 Session 基础操作
Sa-Token 的 Session 分为两种:User-Session 和 Token-Session 。User-Session 是与用户 ID 关联的会话 ,Token-Session 是与 Token 关联的会话。
java
// 获取当前用户的User-Session
SaSession userSession = StpUtil.getSession();
// 获取指定用户的User-Session
SaSession userSessionById = StpUtil.getSessionByLoginId(10001);
// 获取当前Token的Token-Session
SaSession tokenSession = StpUtil.getTokenSession();
// 获取指定Token的Token-Session
SaSession tokenSessionByToken = StpUtil.getTokenSessionByToken("token-xxxxxx");
// 在Session中存储数据
userSession.set("username", "yonghu");
userSession.set("loginTime", System.currentTimeMillis());
// 从Session中获取数据
String username = (String) userSession.get("username");
Long loginTime = (Long) userSession.get("loginTime");
// 从Session中获取数据,如果不存在则返回默认值
String email = (String) userSession.get("email", "default@example.com");
// 删除Session中的数据
userSession.delete("username");
// 清空Session中的所有数据
userSession.clear();
// 获取Session的ID
String sessionId = userSession.getId();
// 获取Session的创建时间
long createTime = userSession.getCreateTime();
// 获取Session的最后访问时间
long lastAccessTime = userSession.getLastAccessTime();
// 获取Session的剩余有效期
long timeout = userSession.getTimeout();
// 设置Session的有效期
userSession.setTimeout(3600); // 1小时
4.2 自定义 Session
Sa-Token 支持自定义 Session,你可以根据业务需求扩展 Session 的功能:
java
@Component
public class MySaSessionCustomizer implements SaSessionCustomizer {
@Override
public void customize(SaSession session) {
// 自定义Session的初始化逻辑
System.out.println("Session创建:" + session.getId());
// 可以在这里添加一些默认数据
session.set("createTime", System.currentTimeMillis());
}
}
4.3 会话查询
Sa-Token 提供了方便的会话查询接口,可以查询当前在线用户、指定用户的登录状态等信息:
java
// 查询当前在线用户的Token数量
int onlineCount = StpUtil.searchTokenValue("", 0, -1, false).size();
// 查询指定用户是否在线
boolean isOnline = StpUtil.isLogin(10001);
// 查询指定用户的所有登录设备
List<String> deviceList = StpUtil.getLoginDeviceList(10001);
// 查询指定用户的所有Token
List<String> tokenList = StpUtil.getTokenListByLoginId(10001);
// 分页查询所有在线Token
List<String> onlineTokens = StpUtil.searchTokenValue("", 0, 10, false);
4.4 踢人下线
踢人下线是会话管理的重要功能,Sa-Token 提供了多种踢人下线的方式:
java
// 将指定用户的所有客户端踢下线
StpUtil.kickout(10001);
// 将指定用户的指定设备类型踢下线
StpUtil.kickout(10001, "PC");
// 将指定Token对应的客户端踢下线
StpUtil.kickoutByTokenValue("token-xxxxxx");
// 将指定用户的所有客户端踢下线,并给出提示信息
StpUtil.kickout(10001, "您的账号在其他设备登录");
当用户被踢下线后,再次访问系统时会抛出 NotLoginException 异常,异常信息中包含踢下线的原因。
五、Token 定制与安全
5.1 Token 风格定制
Sa-Token 内置了六种 Token 风格,你可以根据业务需求选择合适的 Token 风格:
bash
// 配置文件中设置Token风格
// application.yml
sa-token:
token-style: uuid # Token风格,可选值:uuid、simple-uuid、random-32、random-64、random-128、tik
各种 Token 风格的说明:
uuid:标准 UUID 格式,例如:550e8400-e29b-41d4-a716-446655440000simple-uuid:去掉横线的 UUID 格式,例如:550e8400e29b41d4a716446655440000random-32:32 位随机字符串random-64:64 位随机字符串random-128:128 位随机字符串tik:自定义风格,例如:t_xxxxxx
你也可以自定义 Token 生成策略:
java
@Configuration
public class SaTokenStrategyConfig {
@Bean
public SaStrategy saStrategy() {
SaStrategy strategy = new SaStrategy();
// 自定义Token生成策略
strategy.createToken = (loginId, loginType, deviceType) -> {
// 生成自定义格式的Token
return "my-token-" + loginId + "-" + System.currentTimeMillis();
};
return strategy;
}
}
5.2 Token 前缀
Sa-Token 支持为 Token 添加前缀,例如常见的Bearer 前缀:
bash
// 配置文件中设置Token前缀
// application.yml
sa-token:
token-prefix: Bearer # Token前缀
配置后,客户端需要在请求头中这样携带 Token:
bash
Authorization: Bearer token-xxxxxx
5.3 密码加密
Sa-Token 提供了基础的密码加密工具类,可以快速实现 MD5、SHA1、SHA256、AES 等加密算法:
java
import cn.dev33.satoken.secure.SaSecureUtil;
// MD5加密
String md5 = SaSecureUtil.md5("123456");
// SHA1加密
String sha1 = SaSecureUtil.sha1("123456");
// SHA256加密
String sha256 = SaSecureUtil.sha256("123456");
// AES加密
String key = "1234567890123456"; // 16位密钥
String aesEncrypt = SaSecureUtil.aesEncrypt(key, "123456");
String aesDecrypt = SaSecureUtil.aesDecrypt(key, aesEncrypt);
// BCrypt加密(推荐用于密码存储)
String bcryptHash = SaSecureUtil.bcryptHash("123456");
boolean bcryptCheck = SaSecureUtil.bcryptCheck("123456", bcryptHash);
5.4 账号封禁
Sa-Token 提供了账号封禁功能,可以临时或永久封禁用户账号:
java
// 封禁指定用户,有效期为1天(单位:秒)
StpUtil.disable(10001, 86400);
// 封禁指定用户的指定服务,有效期为1小时
StpUtil.disable(10001, "comment", 3600);
// 解封指定用户
StpUtil.untieDisable(10001);
// 解封指定用户的指定服务
StpUtil.untieDisable(10001, "comment");
// 检查指定用户是否被封禁
boolean isDisabled = StpUtil.isDisable(10001);
// 检查指定用户的指定服务是否被封禁
boolean isCommentDisabled = StpUtil.isDisable(10001, "comment");
// 获取封禁剩余时间
long disableTime = StpUtil.getDisableTime(10001);
在实际项目中,我们可以在拦截器中添加账号封禁检查:
java
registry.addInterceptor(new SaInterceptor(handler -> {
// 所有请求都需要登录认证
SaRouter.match("/**")
.notMatch("/auth/login", "/auth/register")
.check(r -> {
StpUtil.checkLogin();
// 检查账号是否被封禁
StpUtil.checkDisable(StpUtil.getLoginId());
});
})).addPathPatterns("/**");
5.5 全局安全响应头
Sa-Token 提供了全局过滤器,可以方便地设置安全响应头,提高系统的安全性:
java
@Configuration
public class SaTokenFilterConfig {
@Bean
public SaServletFilter saServletFilter() {
return new SaServletFilter()
// 拦截所有请求
.addInclude("/**")
// 排除静态资源
.addExclude("/static/**", "/favicon.ico")
// 前置处理
.setBeforeAuth(req -> {
// 设置安全响应头
SaServletFilter.setResponseHeader("X-Frame-Options", "DENY");
SaServletFilter.setResponseHeader("X-XSS-Protection", "1; mode=block");
SaServletFilter.setResponseHeader("X-Content-Type-Options", "nosniff");
SaServletFilter.setResponseHeader("Content-Security-Policy", "default-src 'self'");
})
// 异常处理
.setError(e -> {
return SaResult.error(e.getMessage());
});
}
}
六、高级特性
6.1 模拟他人账号
模拟他人账号功能允许管理员临时切换到其他用户的身份,方便排查问题:
java
// 切换到用户ID为10002的身份
StpUtil.switchTo(10002);
// 此时获取的登录ID是10002
System.out.println(StpUtil.getLoginId()); // 输出:10002
// 结束模拟,切换回原来的身份
StpUtil.endSwitch();
// 此时获取的登录ID是原来的用户ID
System.out.println(StpUtil.getLoginId()); // 输出:10001
// 判断是否正在模拟他人
boolean isSwitch = StpUtil.isSwitch();
6.2 临时身份切换
临时身份切换功能允许用户临时切换到另一个身份,执行特定操作后再切换回来:
java
// 临时切换到用户ID为10002的身份,执行操作
StpUtil.runAs(10002, () -> {
// 这里的代码会以10002的身份执行
System.out.println("当前登录ID:" + StpUtil.getLoginId()); // 输出:10002
});
// 执行完后自动切换回原来的身份
System.out.println("当前登录ID:" + StpUtil.getLoginId()); // 输出:10001
6.3 Http Basic 认证
Sa-Token 支持一行代码接入 Http Basic 认证:
java
import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;
import cn.dev33.satoken.util.SaResult;
@RestController
@RequestMapping("/basic")
public class HttpBasicController {
@GetMapping("/test")
public SaResult testBasicAuth() {
// 校验Http Basic认证,用户名:admin,密码:123456
SaHttpBasicUtil.check("admin", "123456");
return SaResult.ok("Http Basic认证成功");
}
}
6.4 全局侦听器
Sa-Token 提供了全局侦听器,可以在用户登录、注销、被踢下线等关键操作时执行自定义逻辑:
java
import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.SaLoginModel;
import org.springframework.stereotype.Component;
@Component
public class MySaTokenListener implements SaTokenListener {
/**
* 用户登录时触发
*/
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
System.out.println("用户登录:" + loginId + ",Token:" + tokenValue + ",设备类型:" + loginModel.getDevice());
// 可以在这里记录登录日志
}
/**
* 用户注销时触发
*/
@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 doDisable(String loginType, Object loginId, String service, long time) {
System.out.println("用户被封禁:" + loginId + ",服务:" + service + ",时长:" + time + "秒");
// 可以在这里记录封禁日志
}
/**
* 用户被解封时触发
*/
@Override
public void doUntieDisable(String loginType, Object loginId, String service) {
System.out.println("用户被解封:" + loginId + ",服务:" + service);
// 可以在这里记录解封日志
}
/**
* Token续期时触发
*/
@Override
public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {
System.out.println("Token续期:" + tokenValue + ",剩余有效期:" + timeout + "秒");
}
}
6.5 多账号体系认证
Sa-Token 支持一个系统多套账号分开鉴权,例如商城系统中的用户账号和管理员账号:
java
// 定义用户账号的StpUtil
public class StpUserUtil {
public static final String TYPE = "user";
public static final StpLogic stpLogic = new StpLogic(TYPE);
}
// 定义管理员账号的StpUtil
public class StpAdminUtil {
public static final String TYPE = "admin";
public static final StpLogic stpLogic = new StpLogic(TYPE);
}
使用方式和普通的 StpUtil 完全一样:
java
// 用户登录
StpUserUtil.stpLogic.login(10001);
// 管理员登录
StpAdminUtil.stpLogic.login(20001);
// 校验用户是否登录
StpUserUtil.stpLogic.checkLogin();
// 校验管理员是否登录
StpAdminUtil.stpLogic.checkLogin();
6.6 JWT 集成
Sa-Token 提供了三种模式的 JWT 集成方案:
模式一:简单模式
将 JWT 作为 Token 的生成策略:
XML
<!-- 引入JWT依赖 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>1.45.0</version>
</dependency>
bash
// 配置文件中开启JWT
// application.yml
sa-token:
jwt-secret-key: your-secret-key # JWT密钥
is-jwt: true # 开启JWT模式
模式二:混入模式
在 Sa-Token 的 Token 中混入 JWT 信息:
java
// 登录时添加JWT额外信息
StpUtil.login(10001, SaLoginModel.create()
.setExtra("username", "yonghu")
.setExtra("email", "admin@example.com"));
// 获取JWT中的额外信息
String username = (String) StpUtil.getExtra("username");
String email = (String) StpUtil.getExtra("email");
模式三:独立模式
完全使用 JWT 作为认证方式,不依赖 Sa-Token 的 Token 存储:
java
import cn.dev33.satoken.jwt.SaJwtUtil;
import cn.dev33.satoken.util.SaResult;
import io.jsonwebtoken.Claims;
// 生成JWT
String jwt = SaJwtUtil.createToken(10001);
// 解析JWT
Claims claims = SaJwtUtil.parseToken(jwt);
Object loginId = claims.getSubject();
// 校验JWT
SaJwtUtil.checkToken(jwt);
七、持久化与分布式
7.1 Redis 集成
默认情况下,Sa-Token 将数据存储在内存中,重启服务后数据会丢失。在生产环境中,我们需要将数据持久化到 Redis 中:
XML
<!-- 引入Redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 引入Sa-Token Redis集成依赖 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis</artifactId>
<version>1.45.0</version>
</dependency>
bash
// 配置文件中配置Redis
// application.yml
spring:
redis:
host: localhost
port: 6379
password: your-password
database: 0
sa-token:
# 使用Redis存储数据
dao: cn.dev33.satoken.dao.SaTokenDaoRedisJackson
7.2 独立 Redis
Sa-Token 支持将权限缓存与业务缓存分离,使用独立的 Redis 实例:
XML
<!-- 引入独立Redis插件依赖 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-alone-redis</artifactId>
<version>1.45.0</version>
</dependency>
bash
// 配置文件中配置独立Redis
// application.yml
sa-token:
alone-redis:
host: localhost
port: 6379
password: your-password
database: 1
7.3 分布式会话
通过 Redis 集成,Sa-Token 天然支持分布式会话。在微服务架构中,所有服务共享同一个 Redis 实例,就可以实现跨服务的会话共享。
7.4 微服务网关鉴权
在微服务架构中,我们通常将鉴权逻辑放在网关层统一处理。Sa-Token 提供了与 Spring Cloud Gateway 的集成方案:
XML
<!-- 引入Gateway集成依赖 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
<version>1.45.0</version>
</dependency>
java
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SaTokenGatewayConfig {
@Bean
public SaReactorFilter saReactorFilter() {
return new SaReactorFilter()
// 拦截所有请求
.addInclude("/**")
// 排除登录接口和静态资源
.addExclude("/auth/login", "/auth/register", "/doc.html", "/webjars/**", "/v3/api-docs/**")
// 鉴权逻辑
.setAuth(obj -> {
// 所有请求都需要登录认证
SaRouter.match("/**").check(r -> StpUtil.checkLogin());
// 用户模块权限控制
SaRouter.match("/user/**").check(r -> StpUtil.checkPermission("user"));
// 管理员模块权限控制
SaRouter.match("/admin/**").check(r -> StpUtil.checkRole("admin"));
})
// 异常处理
.setError(e -> {
return SaResult.error(e.getMessage());
});
}
}
7.5 RPC 调用鉴权
在微服务架构中,服务之间的 RPC 调用也需要进行鉴权。Sa-Token 提供了 Dubbo 和 gRPC 的集成方案:
XML
<!-- 引入Dubbo集成依赖 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dubbo</artifactId>
<version>1.45.0</version>
</dependency>
java
// 服务提供者端添加鉴权注解
@Service
public class UserServiceImpl implements UserService {
@SaCheckPermission("user:query")
@Override
public User getUserById(Long id) {
// ...
}
}
// 服务消费者端传递Token
StpUtil.setToken("token-xxxxxx");
User user = userService.getUserById(1L);
八、最佳实践
8.1 统一异常处理
在实际项目中,我们需要统一处理 Sa-Token 抛出的各种异常:
java
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理未登录异常
*/
@ExceptionHandler(NotLoginException.class)
public SaResult handleNotLoginException(NotLoginException e) {
return SaResult.error("未登录或登录已过期").setCode(401);
}
/**
* 处理权限不足异常
*/
@ExceptionHandler(NotPermissionException.class)
public SaResult handleNotPermissionException(NotPermissionException e) {
return SaResult.error("权限不足:" + e.getPermission()).setCode(403);
}
/**
* 处理角色不足异常
*/
@ExceptionHandler(NotRoleException.class)
public SaResult handleNotRoleException(NotRoleException e) {
return SaResult.error("角色不足:" + e.getRole()).setCode(403);
}
/**
* 处理Sa-Token其他异常
*/
@ExceptionHandler(SaTokenException.class)
public SaResult handleSaTokenException(SaTokenException e) {
return SaResult.error(e.getMessage()).setCode(500);
}
/**
* 处理其他异常
*/
@ExceptionHandler(Exception.class)
public SaResult handleException(Exception e) {
e.printStackTrace();
return SaResult.error("系统异常:" + e.getMessage()).setCode(500);
}
}
8.2 前后端分离项目最佳实践
在前后端分离项目中,前端通常将 Token 存储在 localStorage 中,每次请求时在请求头中携带 Token:
javascript
// 登录成功后保存Token
localStorage.setItem('token', res.data.token);
// 请求拦截器中添加Token
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = 'Bearer ' + token;
}
return config;
});
// 响应拦截器中处理未登录异常
axios.interceptors.response.use(
response => response,
error => {
if (error.response && error.response.status === 401) {
// 清除Token并跳转到登录页
localStorage.removeItem('token');
router.push('/login');
}
return Promise.reject(error);
}
);
8.3 安全最佳实践
- 使用 HTTPS:所有请求都应该使用 HTTPS 协议,防止 Token 被窃取
- 设置合理的 Token 有效期:不要将 Token 有效期设置得过长,建议设置为 30 分钟到 2 小时
- 开启自动续签:在用户活跃时自动续签 Token,不活跃时自动失效
- 使用 BCrypt 加密密码:不要使用 MD5 等弱加密算法存储密码
- 开启账号封禁功能:对异常登录的账号进行临时封禁
- 设置安全响应头:防止 XSS(跨站脚本攻击)、CSRF(跨站请求伪造) 等常见攻击
- 定期备份数据:定期备份 Redis 中的会话数据,防止数据丢失
总结
Sa-Token 作为一款轻量级 Java 权限认证框架,以其简洁的 API 设计、全面的功能覆盖和良好的扩展性,为 Java 开发者提供了一个优秀的权限认证解决方案。本文详细介绍了 Sa-Token 的核心技术,包括登录认证、权限认证、会话管理、Token 定制、持久化与分布式等方面,并提供了大量详实的代码案例。
Sa-Token 的优势不仅在于其功能的全面性,更在于其设计理念的先进性。它将复杂的权限认证逻辑封装成简单易用的静态方法,让开发者能够专注于业务逻辑,而非权限配置的细枝末节。随着微服务架构的普及,Sa-Token 的分布式会话和网关鉴权功能将发挥越来越重要的作用。