云计算百科
云计算领域专业知识百科平台

Sa-Token Auth框架:从登录认证到微服务权限体系的轻量级方案

Sa-Token Auth 框架:从登录认证到微服务权限体系的轻量级方案

本文聚焦 Sa-Token 在真实项目中的能力边界与落地方式。

权限系统的复杂度往往不在于“加密算法有多难”,而在于业务场景会不断叠加:多端登录、同端互斥、按钮级权限、微服务网关、内部服务隔离、单点登录、第三方授权。Sa-Token 的设计思路很直接:把这些高频问题抽象成一组静态 API 和一套可扩展接口,让认证与鉴权尽量少侵入业务代码。

一、先分清两个概念

安全认证有两个基础问题,也是所有权限框架的起点:

  • Authentication,认证:你是谁。通常表现为用户名、密码登录,成功后服务端给客户端颁发 Token。
  • Authorization,授权/鉴权:你能做什么。服务端判断当前账号是否拥有某个权限码、角色或安全级别。
  • 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. 按钮级权限

    按钮级权限不能只依赖后端。常见做法是:

  • 登录时返回当前账号的完整权限码集合。
  • 前端把权限码存入 localStorage 或全局状态。
  • 按钮通过 v-if 等方式控制显示。
  • <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 分成了三类:

  • Account-Session:按 loginId 分配。同一账号在 PC、APP、小程序登录,只要账号 ID 相同,访问的是同一个 Session。
  • Token-Session:按 Token 分配。同一账号在不同设备上有不同 Token,因此适合保存“当前设备独立”的数据。
  • Custom-Session:按自定义 Key 分配,适合保存与具体账号或 Token 无关的业务缓存。
  • 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 节点并不知道他已经登录。

    常见解决方案有四种:

  • Session 同步:节点间强同步,性能成本高。
  • Session 粘滞:把同一用户固定到同一节点,依赖网关策略。
  • 建立会话中心:例如 Redis,让所有节点变成无状态服务。
  • 使用无状态 Token:例如 JWT,把会话数据写进 Token 本身。
  • 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 提供三种整合方式:

    模式Token 风格登录数据Session下线能力
    Simple JWT Redis 中 Redis 中 支持
    Mixin JWT Token 中 Redis 中 部分支持
    Stateless JWT Token 中 无 Session 前端清理

    选择时主要看业务是否需要服务端控制会话:

    • 需要踢人下线、账号封禁、会话管理,选择 Simple。
    • 更偏向无状态,选择 Stateless。
    • 想兼顾 JWT 和 Redis,Mixin 是一种折中,但要接受部分会话管理能力缺失。

    3. 前后端分离:无 Cookie 模式

    App、小程序没有浏览器 Cookie 机制,因此认证流程需要前端配合:

  • 后端登录后返回 tokenName 和 tokenValue。
  • 前端保存 Token。
  • 前端请求时把 Token 放入 Header,例如 satoken: tokenValue。
  • @RequestMapping("doLogin")
    public SaResult doLogin() {
    StpUtil.login(10001);
    SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
    return SaResult.data(tokenInfo);
    }

    4. 内部服务与外网隔离

    微服务通常希望子服务不直接暴露给外网,只能通过网关访问。Sa-Token 提供 Same-Token 模块完成服务间身份校验。

    核心思路是:

  • 网关为转发请求追加 Same-Token 请求头。
  • 子服务校验该请求头。
  • Feign 或 RPC 调用时也自动携带 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:

  • 用户访问子系统的登录入口。
  • 子系统将用户重定向到认证中心。
  • 用户在认证中心登录。
  • 认证中心携带 Ticket 重定向回子系统。
  • 子系统用 Ticket 换取登录账号。
  • 这里刻意不直接回传 Token,而是回传一次性 Ticket。原因是 Token 长期有效,不应频繁暴露在 URL 中。

    模式三:Http 请求获取会话

    当后端也无法共享 Redis 时,Client 端不能直接查 Redis 校验 Ticket,只能通过 HTTP 请求认证中心完成校验和用户信息同步。模式三还支持单点注销,即任意一个 Client 发起注销,其他 Client 也一并下线。

    三种模式的本质可以总结为:

    • 模式一:共享 Cookie。
    • 模式二:重定向传播 Ticket。
    • 模式三:Http 主动查询会话。

    九、SSO 与 OAuth2.0 怎么选

    很多人会把 SSO 和 OAuth2.0 混为一谈。它们在能力上有重叠,但目标不同:

    能力SSOOAuth2.0
    统一认证 强 强
    统一注销 强 弱
    多系统会话一致性 强一致 弱一致
    第三方应用授权管理 不支持 强
    自有系统授权管理 强 较弱
    Client 级权限校验 不支持 强
    集成复杂度 较低 中等

    简单理解:

    • 自己的多个业务系统共享登录态,优先用 SSO。
    • 需要让第三方应用获得受限资源授权,用 OAuth2.0。

    Sa-Token 的 OAuth2 模块支持四种标准模式:

  • 授权码模式:最标准,后端参与换取 Access-Token。
  • 隐式模式:直接通过 URL 返回 Token,适合无后端参与的场景。
  • 密码模式:客户端直接拿用户名密码换取 Token,适合高度受信任的应用。
  • 凭证模式:获取 Client 级 Token,代表应用自身授权,常用于服务间调用。
  • 其中授权码模式最常用,也最安全;凭证模式则很适合网关转发鉴权、内部服务身份校验。

    十、落地建议

  • 认证与鉴权分开设计。登录只是发 Token,权限应由 StpInterface 或独立权限中心提供。
  • 路由鉴权优先。系统默认“全站登录”,登录接口单独放开,避免每个接口重复写注解。
  • 注解鉴权做业务约束。把关键业务接口的权限、角色、二级认证要求放在方法附近,增强可读性。
  • 前后端分离时明确 Token 传输方式。统一封装请求拦截器,不要在每个 ajax 里手写 Header。
  • 微服务场景优先选 Redis 会话中心。它能同时支持会话共享、踢人下线、账号封禁等强会话能力。
  • 内部服务鉴权与用户认证分开。网关负责用户认证,Same-Token 或 Client-Token 负责服务间身份校验。
  • 前端隐藏按钮不能替代后端鉴权。前端权限只是体验优化,服务端必须再次校验。
  • Sa-Token 的优势不是把所有权限问题都内置死,而是用一套统一的 StpUtil、SaRouter、StpInterface、SaInterceptor 扩展点,把认证、鉴权、Session、微服务和 SSO 串起来。对大多数 Java 项目来说,它是一条比传统重型安全框架更轻的落地方案。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Sa-Token Auth框架:从登录认证到微服务权限体系的轻量级方案
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!