ARTICLE · 1138315
下篇 | Sa-Token认证框架,源码分析、简易SSO、SaaS多租户、快速上手!选型对比!
一个国产轻量级认证框架,能否替代 Spring Security?小 A 决定亲自探究。本文基于 Sa-Token 1.39.0 版本探究!
标签:轻量级、认证、单点登录、二级认证
注:此文为下篇
8. 分场景讨论
8.1 前后端分离:JWT + 无状态
在前后端分离场景中,前端(Vue/React)与后端独立部署,Token 通常通过请求头传递。
sa-token: token-name: Authorization is-read-cookie: false is-read-header: true # Token 前缀(可选,类似 Bearer) token-prefix: "Bearer"@Configurationpublic class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); }}@Configurationpublic class SaTokenConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())) .addPathPatterns("/**") .excludePathPatterns("/auth/login", "/auth/register", "/swagger-ui/**", "/v3/api-docs/**"); }}前端请求示例(JavaScript):
// 登录后保存 TokenlocalStorage.setItem('satoken', response.data.token);// 请求时携带 Tokenaxios.get('/api/user/list', { headers: { 'Authorization': localStorage.getItem('satoken') }});JWT 模式:
Sa-Token 也支持 JWT 模式,将用户信息直接编码到 Token 中,实现无状态认证。
<dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-jwt</artifactId> <version>1.39.0</version></dependency>// 配置 JWT 模式Sa-TokenConfigure configure = new Sa-TokenConfigure() .setTokenStyle("jwt");有状态 vs 无状态:
8.2 移动端 APP:多端独立
移动端场景下,用户可能在手机、平板、PC 等多个设备上登录,Sa-Token 的多端独立模式非常适合。
sa-token: is-concurrent: true is-share: false token-style: uuid@RestController@RequestMapping("/app/auth")public class AppAuthController { @PostMapping("/login") public Result<String> login(@RequestParam String username, @RequestParam String password, @RequestParam(defaultValue = "Mobile") String device) { User user = userService.login(username, password); if (user == null) { return Result.error("登录失败"); } // 指定设备类型登录 StpUtil.login(user.getId(), device); return Result.success(StpUtil.getTokenValue()); } @PostMapping("/switch-device") public Result<String> switchDevice(@RequestParam String device) { // 切换到指定设备 StpUtil.switchDevice(device); return Result.success(StpUtil.getTokenValue()); }}8.3 管理后台:角色 + 权限
管理后台通常需要精细的权限控制,Sa-Token 提供了完善的 RBAC(Role-Based Access Control)支持。
@Servicepublic class AuthServiceImpl implements AuthService { @Override public void setPermission(Long userId) { List<String> permissions = permissionMapper.selectByUserId(userId); StpUtil.getSession().set("permission", permissions); } @Override public List<String> getPermissionList(Object loginId) { return StpUtil.getSessionByLoginId(loginId).get("permission", List.class); } @Override public void setRole(Long userId) { List<String> roles = roleMapper.selectByUserId(userId); StpUtil.getSession().set("role", roles); }}数据模型设计:
+-----------+ +---------------+ +-----------+ | User | | User_Role | | Role | +-----------+ +---------------+ +-----------+ | id |<----->| user_id | | id | | username | | role_id |<----->| role_key | | password | +---------------+ | role_name | +-----------+ +-----------+ | +---------------+ | Role_Permission| +---------------+ | role_id | | permission_id | +---------------+ | +-----------+ |Permission | +-----------+ | id | | perm_key | | perm_name | +-----------+8.4 SaaS 多租户:多账号体系
SaaS 应用通常需要支持多租户隔离,每个租户有独立的用户体系和管理员体系。
// 租户级别的账号体系public class TenantStpLogic { private final String tenantId; private final StpLogic stpLogic; public TenantStpLogic(String tenantId) { this.tenantId = tenantId; this.stpLogic = new StpLogic("tenant:" + tenantId); } public void login(Object id) { stpLogic.login(id); } public boolean isLogin() { return stpLogic.isLogin(); } public Object getLoginId() { return stpLogic.getLoginId(); }}@Componentpublic class TenantContext { private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>(); public static void setTenantId(String tenantId) { CURRENT_TENANT.set(tenantId); } public static String getTenantId() { return CURRENT_TENANT.get(); } public static void clear() { CURRENT_TENANT.remove(); }}// 拦截器中提取租户信息public class TenantInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String tenantId = request.getHeader("X-Tenant-Id"); TenantContext.setTenantId(tenantId); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { TenantContext.clear(); }}9. 源码分析
9.1 StpLogic 接口设计
Sa-Token 的核心是 StpLogic 接口,它采用了门面模式(Facade Pattern),将复杂的认证逻辑封装在一组简洁的 API 背后。
+------------------+ | StpLogic | <-- 核心接口 +------------------+ | | | | 实现 v v +--------+ +----------+ | StpUtil| | 自定义 | | (门面) | | StpLogic | +--------+ +----------+ | | 委托 v +------------------+ | SaTokenManager | +------------------+ | | 委托 v +------------------+ | SaSessionDao | <-- 数据存储层 +------------------+ | +----+----+--------+ v v v内存 Redis ZooKeeperStpLogic 接口的设计体现了接口隔离原则——将所有认证相关操作集中在一个接口中,而不是分散在多个接口中。这样做的好处是:
1. API 统一:所有认证操作通过一个接口暴露,降低学习成本 2. 易于扩展:开发者可以实现自己的 StpLogic,替换默认行为3. 多账号体系:每个账号类型实例化一个 StpLogic,天然隔离
9.2 Sa-Token 过滤器链
Sa-Token 的过滤器链设计比 Spring Security 简单很多。Spring Security 的过滤器链有十多个过滤器,而 Sa-Token 仅有一个核心拦截器。
Spring Security Filter Chain: +-------+-------+-------+-------+-------+-------+-------+ | Channel| Sec. | Logout| Auth. | Basic | Rem. | Filter| | Filter | Context|Filter |Entry | Auth. | Mem. | Sec. | +-------+-------+-------+-------+-------+-------+-------+ Sa-Token Interceptor: +------------------+ | SaInterceptor | | .checkLogin() | | .checkPermission | | .checkRole | +------------------+// SaInterceptor 的核心逻辑@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 如果存在 @SaIgnore 注解,则跳过认证 if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; SaIgnore saIgnore = handlerMethod.getMethodAnnotation(SaIgnore.class); if (saIgnore != null) { return true; } } // 执行鉴权逻辑 runAsSuppler(stp); return true;}9.3 SSO 认证流程
Sa-Token SSO 的认证流程基于 code 模式,与 OAuth2 的授权码模式类似。

9.4 权限注解扫描机制
Sa-Token 的权限注解(如 @SaCheckPermission)通过 Spring AOP 实现。
// SaAnnotationInterceptor 核心逻辑@Overridepublic Object preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; // 扫描方法上的 SaCheckPermission 注解 SaCheckPermission saCheckPermission = handlerMethod.getMethodAnnotation(SaCheckPermission.class); if (saCheckPermission != null) { // 执行权限校验 StpUtil.checkPermission(saCheckPermission.value()); } // 扫描方法上的 SaCheckRole 注解 SaCheckRole saCheckRole = handlerMethod.getMethodAnnotation(SaCheckRole.class); if (saCheckRole != null) { StpUtil.checkRole(saCheckRole.value()); } } return true;}注解驱动的设计优势:
@SaCheckPermission) | ||
StpUtil.checkPermission()) | ||
SaInterceptor) |
10. 单元测试
10.1 Sa-Token Mock 测试
Sa-Token 提供了专门的测试工具类 SaTokenTestKit,可以在单元测试中模拟登录状态。
@SpringBootTest@AutoConfigureMockMvcclass AuthControllerTest { @Autowired private MockMvc mockMvc; @Test void testLogin() throws Exception { // 模拟登录 StpUtil.login(10001L); // 验证登录态 Assertions.assertTrue(StpUtil.isLogin()); Assertions.assertEquals(10001L, StpUtil.getLoginIdAsLong()); // 获取 Token String token = StpUtil.getTokenValue(); // 使用 MockMvc 请求受保护接口 mockMvc.perform(get("/user/list") .header("satoken", token)) .andExpect(status().isOk()); } @Test void testLogout() throws Exception { StpUtil.login(10001L); Assertions.assertTrue(StpUtil.isLogin()); StpUtil.logout(); Assertions.assertFalse(StpUtil.isLogin()); }}10.2 权限校验测试
@SpringBootTestclass PermissionTest { @BeforeEach void setUp() { // 清理测试数据 StpUtil.logoutByLoginId(10001L); } @Test void testHasPermission() { StpUtil.login(10001L); // 模拟设置权限 List<String> permissions = Arrays.asList("user:add", "user:list", "user:delete"); StpUtil.getSession().set("permission", permissions); // 验证权限 Assertions.assertTrue(StpUtil.hasPermission("user:add")); Assertions.assertTrue(StpUtil.hasPermission("user:list")); Assertions.assertFalse(StpUtil.hasPermission("order:add")); } @Test void testHasRole() { StpUtil.login(10001L); // 模拟设置角色 List<String> roles = Arrays.asList("admin", "user"); StpUtil.getSession().set("role", roles); // 验证角色 Assertions.assertTrue(StpUtil.hasRole("admin")); Assertions.assertTrue(StpUtil.hasRole("user")); Assertions.assertFalse(StpUtil.hasRole("super_admin")); } @Test void testCheckPermissionThrows() { StpUtil.login(10001L); // 没有设置权限时,校验应抛出异常 Assertions.assertThrows(NotPermissionException.class, () -> { StpUtil.checkPermission("user:add"); }); }}10.3 SSO 集成测试
@SpringBootTest@AutoConfigureMockMvcclass SsoIntegrationTest { @Autowired private MockMvc mockMvc; @Test void testSsoLoginFlow() throws Exception { // 1. 访问客户端应用,未登录时应重定向 MvcResult result = mockMvc.perform(get("/hello")) .andReturn(); // 应该被重定向到 SSO-Server Assertions.assertTrue( result.getResponse().getRedirectedUrl().contains("sso")); // 2. 在 SSO-Server 登录 mockMvc.perform(post("/doLogin") .param("username", "admin") .param("password", "123456")) .andExpect(status().isOk()); // 3. 再次访问客户端应用,应已登录 mockMvc.perform(get("/hello")) .andExpect(status().isOk()) .andExpect(content().string(containsString("admin"))); }}10.4 Redis 存储测试
@SpringBootTest@TestPropertySource(properties = { "sa-token.timeout=60", "spring.data.redis.port=6389"})class RedisStoreTest { @Autowired private RedisTemplate<String, Object> redisTemplate; @Test void testTokenStoredInRedis() { // 登录 StpUtil.login(10001L); String tokenValue = StpUtil.getTokenValue(); // 验证 Redis 中存在对应 Key String redisKey = "satoken:login:token:" + tokenValue; Boolean hasKey = redisTemplate.hasKey(redisKey); Assertions.assertTrue(hasKey); // 登出后 Key 应被删除 StpUtil.logout(); hasKey = redisTemplate.hasKey(redisKey); Assertions.assertFalse(hasKey); } @Test void testTokenTimeout() throws InterruptedException { StpUtil.login(10001L); // 等待 Token 过期 Thread.sleep(65000); // Token 应已失效 Assertions.assertFalse(StpUtil.isLogin()); }}11. 常见误区
误区一:"Sa-Token 不安全"
这是最常见的误解。Sa-Token 的安全性不取决于框架本身,而取决于使用方式。Sa-Token 提供了完整的 Token 管理、权限控制、二级认证等安全机制。关键在于:
• Token 传输是否使用 HTTPS • Token 有效期是否合理设置 • 敏感操作是否有二次验证 • 权限粒度是否适当
Sa-Token 的底层实现遵循了安全最佳实践,其安全性取决于开发者的配置和使用方式。
误区二:"只能做简单认证"
Sa-Token 的功能覆盖范围超出了"简单认证"。从基础登录到 SSO、OAuth2、二级认证、多端独立,Sa-Token 提供了完整的认证授权解决方案。"轻量级"指的是 API 简洁,不是功能有限。
误区三:"不支持分布式"
Sa-Token 通过 sa-token-dao 系列组件支持多种分布式存储方案:
误区四:"与 Spring 深度绑定"
Sa-Token 的核心模块(sa-token-core)不依赖任何 Spring 组件。它与 Spring 的集成通过 sa-token-spring-boot-starter 实现,是可选的。
// 在非 Spring 环境下使用 Sa-Tokenpublic class Main { public static void main(String[] args) { // 直接使用 StpUtil,无需 Spring 容器 StpUtil.login(10001L); System.out.println("isLogin: " + StpUtil.isLogin()); System.out.println("loginId: " + StpUtil.getLoginId()); }}误区五:"性能不如 Spring Security"
Sa-Token 的 API 调用链更短,过滤器链更简单(只有一个拦截器 vs Spring Security 的十余个过滤器),在大多数场景下性能表现不差。对于大多数业务应用,认证框架的性能差异通常不是瓶颈。
以上数据为参考值,具体性能需根据实际场景进行 JMH 测试。
误区六:"文档不全,遇到问题难解决"
Sa-Token 的官方文档(https://sa-token.cc)提供了完整的中文文档,覆盖入门指南、核心功能、高级特性、常见问题等。此外,社区活跃度较高,GitHub Issues 响应及时。对于国内开发者来说,中文文档的友好度是一个显著优势。
误区七:"没有企业级项目使用"
Sa-Token 已被多个企业和开源项目采用。根据 GitHub 上的 Stars 和 Fork 数据,以及社区反馈,Sa-Token 在国内中小型项目中有较高使用率。选择框架时,应关注其是否满足自身项目的技术需求,而非仅看使用人数。
12. 对比分析
12.1 Sa-Token vs Spring Security
| API 风格 | ||
| 学习曲线 | ||
| 核心概念 | ||
| 配置方式 | ||
| 扩展性 | ||
| 生态集成 | ||
| 安全性 | ||
| 适合场景 | ||
| 国际化支持 | ||
| 社区规模 |
12.2 Sa-Token vs Shiro
| 维护状态 | ||
| API 简洁度 | ||
| SSO 支持 | ||
| OAuth2 支持 | ||
| 二级认证 | ||
| 多端独立 | ||
| 文档质量 | ||
| Spring Boot 集成 | ||
| 社区活跃度 |
12.3 Sa-Token SSO vs OAuth2 SSO
| 协议标准 | ||
| 跨语言支持 | ||
| 复杂度 | ||
| 第三方集成 | ||
| 适用场景 | ||
| Token 类型 | ||
| 授权模式 |

13. 最佳实践
13.1 Token 有效期设计
Token 有效期的设计需要在安全性和用户体验之间取得平衡。
sa-token: # Token 有效期 8 小时 timeout: 28800 # 活跃频率 30 分钟:超过 30 分钟未操作则 Token 冻结 active-timeout: 180013.2 权限粒度设计
权限粒度设计建议遵循"最小权限原则",同时考虑管理成本。
权限设计层次: Level 1: 模块级(user、order、product) | +-- Level 2: 操作级(user:add、user:edit、user:delete、user:view) | +-- Level 3: 数据级(仅看自己创建的数据 vs 看全部数据)推荐的权限命名规范:
格式:{模块}:{操作} 示例: - user:add 新增用户 - user:edit 编辑用户 - user:delete 删除用户 - user:view 查看用户 - user:export 导出用户 - order:pay 支付订单 - order:refund 退款订单不推荐的权限粒度:
super_admin | ||
user:add:user | user:add | |
新增用户信息 | user:add |
13.3 Redis 集成配置
生产环境推荐的 Redis 配置:
spring: data: redis: host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6389} password: ${REDIS_PASSWORD:} database: 0 timeout: 3000ms lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 max-wait: 1000mssa-token: timeout: 28800 active-timeout: 1800 is-concurrent: true is-share: false token-style: uuid is-log: false # 生产环境关闭日志以避免泄露 Token13.4 安全加固建议
传输安全:
• 所有请求使用 HTTPS 传输 • Token 不在 URL 中传递(通过 Header 传递)
sa-token: is-read-cookie: false is-read-header: trueToken 管理:
• 设置合理的 Token 有效期和活跃频率 • 敏感操作使用二级认证 • 定期清理过期 Token
// 定期清理过期 Token(可配合定时任务)@Scheduled(cron = "0 0 2 * * ?")public void cleanExpiredTokens() { // Sa-Token 会自动清理过期数据,无需手动处理 // 如需自定义清理逻辑,可通过 SaTokenDao 接口操作}权限管理:
• 遵循最小权限原则 • 定期审计权限分配 • 权限变更及时刷新 Session
防攻击:
• 登录接口增加验证码机制 • 限制登录失败次数 • 异常登录行为监控
@PostMapping("/login")public Result<String> login(@RequestParam String username, @RequestParam String password, @RequestParam(required = false) String captcha) { // 1. 验证码校验 if (!captchaService.verify(captcha)) { return Result.error("验证码错误"); } // 2. 登录失败次数限制 String failKey = "login:fail:" + username; long failCount = SaTokenDao.defaultDao.getTimeout(failKey); if (failCount > 5) { return Result.error("登录失败次数过多,请稍后再试"); } // 3. 验证用户 SysUser user = userService.findByUsername(username); if (user == null || !passwordEncoder.matches(password, user.getPassword())) { // 记录失败次数 SaTokenDao.defaultDao.set(failKey, "1", 1800); // 30 分钟内累计 return Result.error("用户名或密码错误"); } // 4. 登录成功,清除失败记录 SaTokenDao.defaultDao.delete(failKey); StpUtil.login(user.getId()); return Result.success(StpUtil.getTokenValue());}14. 总结
14.1 核心要点速查表
| 一句话定位 | |
| 核心 API | StpUtil.login()StpUtil.checkLogin()、StpUtil.checkPermission() |
| 核心接口 | StpLogic |
| 登录认证 | StpUtil.login(userId) |
| 权限校验 | StpUtil.checkPermission() / 注解方式 @SaCheckPermission |
| 角色校验 | StpUtil.checkRole() / 注解方式 @SaCheckRole |
| Token 存储 | |
| 多端支持 | |
| SSO | |
| OAuth2 | |
| 二级认证 | StpUtil.openSafe()@SaCheckSafe |
| 适合场景 | |
| 学习成本 |
14.2 核心代码速查
// ---- 基础操作 ----StpUtil.login(userId); // 登录StpUtil.logout(); // 登出StpUtil.isLogin(); // 是否登录StpUtil.getLoginId(); // 获取账号IDStpUtil.getTokenValue(); // 获取Token值// ---- 权限操作 ----StpUtil.checkPermission("user:add"); // 校验权限StpUtil.hasPermission("user:add"); // 是否有权限StpUtil.getPermissionList(); // 获取权限列表// ---- 角色操作 ----StpUtil.checkRole("admin"); // 校验角色StpUtil.hasRole("admin"); // 是否有角色StpUtil.getRoleList(); // 获取角色列表// ---- 会话操作 ----StpUtil.getSession().set("key", value); // 设置会话数据StpUtil.getSession().get("key"); // 获取会话数据// ---- 二级认证 ----StpUtil.openSafe(1800); // 打开二级认证StpUtil.closeSafe(); // 关闭二级认证StpUtil.isSafe(); // 是否已通过二级认证// ---- 踢人下线 ----StpUtil.logoutByLoginId(userId); // 踢指定用户下线StpUtil.kickout(device); // 踢出指定设备15. 面试自测
👉 Sa-Token认证框架全解析(面试自测与答案)
16. 参考资料
1. Sa-Token 官方文档 — Sa-Token 官方文档,涵盖入门指南、核心功能、高级特性 2. Sa-Token GitHub 仓库 — 源码与 Issue 跟踪 3. Sa-Token 与 Spring Security 的对比 — 官方对比说明 4. Sa-Token SSO 单点登录文档 — SSO 功能详细说明 5. Sa-Token OAuth2 文档 — OAuth2 协议实现说明 6. Spring Security 官方文档 — Spring Security 参考文档 7. Apache Shiro 官方文档 — Shiro 参考文档 8. RFC 6749 - OAuth 2.0 授权框架 — OAuth2 协议标准 9. Sa-Token 多账号体系示例 — 官方示例仓库