Sa-Token Auth 框架:从登录认证到微服务权限体系的轻量级方案
本文聚焦 Sa-Token 在真实项目中的能力边界与落地方式。
权限系统的复杂度往往不在于“加密算法有多难”,而在于业务场景会不断叠加:多端登录、同端互斥、按钮级权限、微服务网关、内部服务隔离、单点登录、第三方授权。Sa-Token 的设计思路很直接:把这些高频问题抽象成一组静态 API 和一套可扩展接口,让认证与鉴权尽量少侵入业务代码。
一、先分清两个概念
安全认证有两个基础问题,也是所有权限框架的起点:
Sa-Token 将这两件事拆得很清楚:
- 认证围绕 StpUtil.login(id)、StpUtil.checkLogin() 展开。
- 鉴权围绕 StpUtil.checkPermission("xxx")、StpUtil.checkRole("admin") 展开。
下面先从一个最小示例建立整体印象。
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.38.0</version>
</dependency>
server:
port: 8081
sa-token:
token-name: satoken
timeout: 2592000
active-timeout: -1
is-concurrent: true
is-share: true
token-style: uuid
is-log: true
@RestController
@RequestMapping("/acc/")
public class LoginController {
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {
if ("zhang".equals(name) && "123456".equals(pwd)) {
StpUtil.login(10001);
return SaResult.ok("登录成功");
}
return SaResult.error("登录失败");
}
@RequestMapping("isLogin")
public SaResult isLogin() {
return SaResult.ok("是否登录:" + StpUtil.isLogin());
}
@RequestMapping("tokenInfo")
public SaResult tokenInfo() {
return SaResult.data(StpUtil.getTokenInfo());
}
}
这里最值得注意的一点是:StpUtil.login(10001) 本身没有显式返回 Token,但浏览器端可以继续访问受保护接口。原因是在 Cookie 模式下,Sa-Token 会自动把 Token 写入浏览器,并在后续请求中读取。这个细节看似简单,其实决定了它在传统 Web 项目中的“零侵入”体验。
二、Sa-Token 是什么
Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:
- 登录认证
- 权限认证与角色认证
- 单点登录 SSO
- OAuth2.0
- 分布式 Session
- 微服务网关鉴权
- 内部服务间鉴权
与 Spring Security、Shiro 相比,Sa-Token 最明显的差异不是功能数量,而是 API 风格。很多能力可以通过 StpUtil 的一行静态调用完成,配置和接口也尽量保持轻量。
核心能力可以概括为几条:
- 登录控制:单端登录、多端登录、同端互斥登录、记住我、指定 Token 有效期。
- 会话管理:账号级 Session、Token 级 Session、自定义 Session、会话查询。
- 鉴权方式:代码鉴权、注解鉴权、路由拦截鉴权。
- 安全增强:二级认证、账号封禁、密码加密、全局侦听器。
- 分布式能力:Redis 会话共享、JWT 集成、网关转发鉴权、RPC 调用鉴权、Same-Token。
- 平台适配:SpringBoot、WebFlux、Gateway、ShenYu、Zuul、Solon 等。
三、认证:从登录到离线控制
1. 登录、注销与 Token 查询
常用 API 如下:
StpUtil.login(10001);
StpUtil.logout();
StpUtil.isLogin();
StpUtil.checkLogin();
StpUtil.getLoginId();
StpUtil.getLoginIdAsString();
StpUtil.getLoginIdAsLong();
StpUtil.getTokenValue();
StpUtil.getTokenName();
StpUtil.getTokenTimeout();
StpUtil.getTokenInfo();
getTokenInfo() 返回的不只是 Token 字符串,还包括登录账号、账号类型、剩余有效期、设备类型等上下文信息。前后端分离场景下,我们通常会把 tokenName 和 tokenValue 一起返回给前端。
2. 踢人下线和强制注销
Sa-Token 区分了两种“让用户下线”的方式:
StpUtil.logout(10001);
StpUtil.logout(10001, "PC");
StpUtil.logoutByTokenValue("token");
StpUtil.kickout(10001);
StpUtil.kickout(10001, "PC");
StpUtil.kickoutByTokenValue("token");
两者区别:
- 强制注销:相当于对方主动注销,之后访问提示 Token 无效。
- 踢人下线:保留 Token,但打上“已踢下线”标记,之后访问提示 Token 已被踢下线。
这种区分在运营系统里很有价值。比如客服操作“强制退出”和风控系统操作“踢出违规账号”,前端可以展示不同文案。
3. 未登录场景要分清楚
NotLoginException 不只是“未登录”一种情况,而是有多种场景值:
| -1 | 未读取到有效 Token |
| -2 | Token 无效 |
| -3 | Token 已过期 |
| -4 | Token 已被顶下线 |
| -5 | Token 已被踢下线 |
| -6 | Token 已被冻结 |
| -7 | 未按指定前缀提交 Token |
建议在全局异常处理中根据场景值返回不同提示,而不是把一切都笼统显示成“请先登录”。
@ExceptionHandler(NotLoginException.class)
public SaResult handlerNotLoginException(NotLoginException nle) {
String message;
if (nle.getType().equals(NotLoginException.NOT_TOKEN)) {
message = "未能读取到有效 token";
} else if (nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
message = "token 已过期";
} else if (nle.getType().equals(NotLoginException.BE_REPLACED)) {
message = "token 已被顶下线";
} else if (nle.getType().equals(NotLoginException.KICK_OUT)) {
message = "token 已被踢下线";
} else {
message = "当前会话未登录";
}
return SaResult.error(message);
}
4. 二级认证
二级认证解决的是“已登录,但还要再确认一次”的高风险操作,例如删除仓库、修改支付密码、导出敏感数据。
StpUtil.openSafe(120);
StpUtil.isSafe();
StpUtil.checkSafe();
StpUtil.getSafeTime();
StpUtil.closeSafe();
如果系统有多条敏感业务线,可以指定业务标识,让不同业务的二级认证互不影响:
StpUtil.openSafe("client", 600);
StpUtil.isSafe("client");
StpUtil.checkSafe("client");
StpUtil.closeSafe("client");
也可以使用注解:
@SaCheckSafe
@RequestMapping("deleteProject")
public SaResult deleteProject() {
return SaResult.ok();
}
5. 同端互斥登录
同端互斥的典型场景是 QQ:手机和电脑可以同时在线,但两台手机不能同时登录同一个账号。
sa-token:
is-concurrent: false
StpUtil.login(10001, "PC");
StpUtil.login(10001, "APP");
StpUtil.getLoginDevice();
StpUtil.getTokenValueByLoginId(10001, "APP");
这里的关键不是“禁止多端登录”,而是把设备类型作为一个会话维度。同一账号、同一设备类型只保留一个有效登录,不同设备类型互不影响。
6. Http Basic / Digest
Sa-Token 也提供了对传统 HTTP 认证的支持,适合内部工具、基础接口或兼容老系统:
SaHttpBasicUtil.check("sa:123456");
SaHttpDigestUtil.check("sa", "123456");
也可以通过注解声明:
@SaCheckHttpBasic(account = "sa:123456")
@RequestMapping("test")
public SaResult test() {
return SaResult.ok();
}
四、鉴权:从权限码到路由拦截
1. 权限码从哪里来
Sa-Token 不假设你的权限数据存在哪里。它要求你实现 StpInterface,把当前账号的权限码和角色列表返回给框架。
@Component
public class StpInterfaceImpl implements StpInterface {
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 实际项目从数据库、缓存或权限中心查询
return Arrays.asList("user.add", "user.update", "user.get", "art.*");
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
return Arrays.asList("admin", "super-admin");
}
}
这个接口的作用类似 Spring Security 的 UserDetailsService:框架负责校验,你负责提供数据。
2. 权限与角色校验
StpUtil.hasPermission("user.add");
StpUtil.checkPermission("user.add");
StpUtil.checkPermissionAnd("user.add", "user.delete");
StpUtil.checkPermissionOr("user.add", "user.delete");
StpUtil.hasRole("super-admin");
StpUtil.checkRole("super-admin");
StpUtil.checkRoleAnd("super-admin", "shop-admin");
StpUtil.checkRoleOr("super-admin", "shop-admin");
权限码支持通配符:
StpUtil.hasPermission("art.add"); // 拥有 art.* 时返回 true
StpUtil.hasPermission("art.delete"); // 拥有 *.delete 时返回 true
当一个账号拥有 * 权限时,可以匹配任何权限码,通常称为“上帝权限”。
3. 按钮级权限
按钮级权限不能只依赖后端。常见做法是:
<button v-if="permissions.includes('user.delete')">删除按钮</button>
但要强调:前端隐藏只是体验优化,不能替代后端鉴权。所有关键接口仍然必须再次校验。
4. 注解鉴权
注解鉴权适合把权限逻辑和业务方法分离。使用前需要注册拦截器:
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor())
.addPathPatterns("/**");
}
}
常用注解:
@SaCheckLogin
@SaCheckRole("super-admin")
@SaCheckPermission("user-add")
@SaCheckSafe
@SaCheckHttpBasic(account = "sa:123456")
@SaCheckDisable("comment")
多个注解天然是“且”关系:
@SaCheckLogin
@SaCheckRole("admin")
@SaCheckPermission("user.add")
public SaResult addUser() {
return SaResult.ok();
}
如果希望“权限或角色”满足一个即可,可以使用 orRole:
@SaCheckPermission(value = "user.add", orRole = "admin")
public SaResult addUser() {
return SaResult.ok();
}
5. 路由拦截鉴权
当项目规则是“默认全部需要登录,只有登录接口放开”,逐个加注解很麻烦。此时更适合路由拦截。
registry.addInterceptor(new SaInterceptor(handler -> {
SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin());
SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
})).addPathPatterns("/**");
SaRouter 支持按路径、请求方法、布尔条件、Lambda 条件进行组合匹配,也支持 notMatch、stop、back 和 free 作用域。它比较适合在网关或统一入口处做集中鉴权。
五、Session:理解账号级与 Token 级缓存
传统 HttpSession 的典型问题是:同一个账号在 PC 和 APP 登录会被当成两个会话,而且一个陌生请求也可能产生 Session。Sa-Token 将 Session 分成了三类:
SaSession accountSession = StpUtil.getSession();
accountSession.set("name", "张三");
SaSession tokenSession = StpUtil.getTokenSession();
tokenSession.set("lastActiveTime", System.currentTimeMillis());
SaSession customSession = SaSessionCustomUtil.getSessionById("goods-10001");
customSession.set("stock", 100);
一个典型区别场景:
- 点赞列表等账号级数据放 Account-Session。
- 当前设备的最后活跃时间、设备指纹、浏览上下文等放 Token-Session。
六、进阶能力
1. 身份切换
运营后台经常需要“以某个用户视角”排查问题。Sa-Token 支持临时身份切换:
StpUtil.switchTo(10044);
StpUtil.getLoginId(); // 10044
StpUtil.endSwitch();
也可以使用 Lambda 作用域,自动恢复:
StpUtil.switchTo(10044, () -> {
System.out.println(StpUtil.getLoginId());
});
2. 记住我
Sa-Token 默认是“记住我”模式。若希望关闭浏览器后需要重新登录,可以显式指定:
StpUtil.login(10001, false);
原理是利用浏览器的临时 Cookie 与持久 Cookie:
- 持久 Cookie:关闭浏览器后仍然存在,对应记住我。
- 临时 Cookie:关闭浏览器后消失,对应非记住我。
前后端分离场景下,通常用 localStorage 模拟持久 Token,用 sessionStorage 模拟临时 Token。
3. 账号封禁
StpUtil.disable(10001, 86400);
StpUtil.isDisable(10001);
StpUtil.checkDisable(10001);
StpUtil.getDisableTime(10001);
StpUtil.untieDisable(10001);
Sa-Token 还支持分类封禁和阶梯封禁。比如电商系统可以只封禁评论能力,不封禁下单能力:
StpUtil.disable(10001, "comment", 86400);
StpUtil.checkDisable(10001, "comment");
阶梯封禁则适用于“首次警告、二次禁言、三次封号”的处罚策略。
4. 密码加密与全局侦听器
Sa-Token 提供 MD5、SHA1、SHA256、AES、RSA、Base64、BCrypt 等基础工具,但更重要的是,密码加密属于认证外围能力,不应把它与权限框架本身过度耦合。
全局侦听器可以订阅登录、注销、被踢、被顶、被封禁、二级认证、Session 创建等关键事件:
@Component
public class MySaTokenListener extends SaTokenListenerForSimple {
@Override
public void doLogin(String loginType, Object loginId,
String tokenValue, SaLoginModel loginModel) {
System.out.println("账号登录:" + loginId);
}
}
这里最适合接审计日志、风控埋点、用户状态同步等横切逻辑。
七、微服务下的认证架构
单机 Session 在分布式环境下会失效:用户在 A 节点登录,下一次请求落到 B 节点,B 节点并不知道他已经登录。
常见解决方案有四种:
Sa-Token 更推荐“Redis 会话中心”和“JWT”两条路线。
1. 集成 Redis
引入对应 Redis 集成包即可:
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.38.0</version>
</dependency>
Redis 集成解决两个问题:
- 服务重启后会话不丢失。
- 多个服务节点共享登录状态。
如果权限缓存和业务缓存不希望混在一起,还可以使用 sa-token-alone-redis 插件做独立 Redis。
2. 集成 JWT 的三种模式
Sa-Token 对 JWT 提供三种整合方式:
| Simple | JWT | Redis 中 | Redis 中 | 支持 |
| Mixin | JWT | Token 中 | Redis 中 | 部分支持 |
| Stateless | JWT | Token 中 | 无 Session | 前端清理 |
选择时主要看业务是否需要服务端控制会话:
- 需要踢人下线、账号封禁、会话管理,选择 Simple。
- 更偏向无状态,选择 Stateless。
- 想兼顾 JWT 和 Redis,Mixin 是一种折中,但要接受部分会话管理能力缺失。
3. 前后端分离:无 Cookie 模式
App、小程序没有浏览器 Cookie 机制,因此认证流程需要前端配合:
@RequestMapping("doLogin")
public SaResult doLogin() {
StpUtil.login(10001);
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
return SaResult.data(tokenInfo);
}
4. 内部服务与外网隔离
微服务通常希望子服务不直接暴露给外网,只能通过网关访问。Sa-Token 提供 Same-Token 模块完成服务间身份校验。
核心思路是:
@Component
public class ForwardAuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest()
.mutate()
.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken())
.build();
return chain.filter(exchange.mutate().request(request).build());
}
}
String token = SaHolder.getRequest().getHeader(SaSameUtil.SAME_TOKEN);
SaSameUtil.checkToken(token);
Same-Token 适合作为内部调用的身份凭证,不建议把它当作面向用户的登录凭证。
八、SSO:三种模式如何选
SSO 解决的是多个互相信任系统之间的“一次登录,处处通行”。
Sa-Token-SSO 提供三种模式:
| 前端同域 + 后端同 Redis | 模式一 | 共享 Cookie 同步会话 |
| 前端不同域 + 后端同 Redis | 模式二 | URL 重定向传播会话 |
| 前端不同域 + 后端不同 Redis | 模式三 | Http 请求获取会话 |
模式一:共享 Cookie
通过父域名 Cookie 共享 Token,通过 Redis 共享 Session。适合多个系统都在同一个主域名下,例如 s1.stp.com、s2.stp.com。
模式二:URL 重定向
跨域场景下 Cookie 无法共享,因此通过认证中心下发一次性 Ticket:
这里刻意不直接回传 Token,而是回传一次性 Ticket。原因是 Token 长期有效,不应频繁暴露在 URL 中。
模式三:Http 请求获取会话
当后端也无法共享 Redis 时,Client 端不能直接查 Redis 校验 Ticket,只能通过 HTTP 请求认证中心完成校验和用户信息同步。模式三还支持单点注销,即任意一个 Client 发起注销,其他 Client 也一并下线。
三种模式的本质可以总结为:
- 模式一:共享 Cookie。
- 模式二:重定向传播 Ticket。
- 模式三:Http 主动查询会话。
九、SSO 与 OAuth2.0 怎么选
很多人会把 SSO 和 OAuth2.0 混为一谈。它们在能力上有重叠,但目标不同:
| 统一认证 | 强 | 强 |
| 统一注销 | 强 | 弱 |
| 多系统会话一致性 | 强一致 | 弱一致 |
| 第三方应用授权管理 | 不支持 | 强 |
| 自有系统授权管理 | 强 | 较弱 |
| Client 级权限校验 | 不支持 | 强 |
| 集成复杂度 | 较低 | 中等 |
简单理解:
- 自己的多个业务系统共享登录态,优先用 SSO。
- 需要让第三方应用获得受限资源授权,用 OAuth2.0。
Sa-Token 的 OAuth2 模块支持四种标准模式:
其中授权码模式最常用,也最安全;凭证模式则很适合网关转发鉴权、内部服务身份校验。
十、落地建议
Sa-Token 的优势不是把所有权限问题都内置死,而是用一套统一的 StpUtil、SaRouter、StpInterface、SaInterceptor 扩展点,把认证、鉴权、Session、微服务和 SSO 串起来。对大多数 Java 项目来说,它是一条比传统重型安全框架更轻的落地方案。
网硕互联帮助中心




评论前必须登录!
注册