剖析 Sa-Token 权限认证:从注解到拦截器的完整调用链路
2026/4/20 23:52:53 网站建设 项目流程

1. Sa-Token权限认证的核心流程

Sa-Token的权限认证流程可以比作机场安检系统。当你走进机场(发起HTTP请求),首先要在入口处出示登机牌(@SaCheckPermission注解),然后安检人员(SaAnnotationInterceptor拦截器)会核对你的身份和权限,最后根据安全策略(SaStrategy)决定是否放行。

整个流程从注解触发开始,就像在Controller方法上贴了一张"仅限VIP通行"的告示。当请求到达时,拦截器会像尽职的保安一样检查这张告示,然后呼叫后台的安全主管(SaStrategy)进行详细核查。

@RestController public class UserController { @SaCheckPermission("user:delete") @DeleteMapping("/user/{id}") public String deleteUser(@PathVariable Long id) { // 删除用户逻辑 return "删除成功"; } }

这段代码就像在说:"只有持有'user:delete'权限卡的人才能进入这个房间"。实际运行时,Sa-Token会完成以下动作:

  1. 拦截器捕获请求并定位到方法注解
  2. 提取注解中的权限标识"user:delete"
  3. 获取当前登录用户的所有权限列表
  4. 进行权限匹配校验
  5. 通过则放行,否则抛出异常

2. 注解驱动的权限声明

2.1 @SaCheckPermission注解详解

这个注解就像给方法贴上的权限标签,支持多种灵活的配置方式:

// 基础用法:必须同时具备user:add和user:edit权限 @SaCheckPermission({"user:add", "user:edit"}) // OR模式:具备任意一个权限即可 @SaCheckPermission(value = {"user:add", "user:edit"}, mode = SaMode.OR) // 权限-角色混合校验:要么有user:delete权限,要么有admin角色 @SaCheckPermission(value = "user:delete", orRole = "admin") // 多账号体系下的权限校验 @SaCheckPermission(value = "user:query", type = "merchant")

注解参数就像组合锁的密码盘:

  • value:核心权限码,支持字符串数组
  • mode:校验模式,AND(默认)表示需全部满足,OR表示满足其一即可
  • type:多账号体系标识,区分不同业务线的权限体系
  • orRole:备选方案,权限不满足时尝试用角色校验

2.2 注解作用域与继承规则

注解可以贴在方法上,也能贴在类级别。就像公司门禁规则:

  • 贴在方法上:仅对该方法生效(如财务室的保险柜)
  • 贴在类上:对该类所有方法生效(如整个财务区域)
  • 存在冲突时:方法级注解覆盖类级注解(特殊保险柜使用独立规则)
@SaCheckPermission("admin") // 类级别:默认需要admin权限 @RestController public class AdminController { @GetMapping("/dashboard") public String dashboard() { // 需要admin权限 } @SaCheckPermission("super-admin") // 方法级别:需要更高权限 @DeleteMapping("/system") public String resetSystem() { // 需要super-admin权限 } }

3. 拦截器的工作机制

3.1 SaAnnotationInterceptor拦截流程

这个拦截器就像公司的前台接待,对每个来访请求进行初筛:

public class SaAnnotationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 1. 检查是否为方法处理器 if (!(handler instanceof HandlerMethod)) { return true; // 放行非方法请求 } // 2. 获取目标方法 Method method = ((HandlerMethod) handler).getMethod(); // 3. 委托策略引擎校验注解 SaStrategy.me.checkMethodAnnotation.accept(method); return true; // 验证通过 } }

关键点在于:

  1. 只拦截Spring MVC的HandlerMethod
  2. 通过方法对象获取所有安全注解
  3. 将实际校验工作委托给SaStrategy

3.2 拦截器的注册与配置

要让这个"保安"上岗,需要在Spring配置中登记:

@Configuration public class SaTokenConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 注册注解拦截器 registry.addInterceptor(new SaAnnotationInterceptor()) .addPathPatterns("/**") // 拦截所有路径 .excludePathPatterns("/login"); // 排除登录接口 } }

实际项目中建议:

  • 排除公开接口(如登录、注册)
  • 注意拦截器顺序(安全拦截器通常靠前)
  • 可配合@SaCheckIgnore实现方法级排除

4. 策略引擎的校验逻辑

4.1 SaStrategy的核心校验流程

SaStrategy就像公司的HR系统,掌握着所有员工的权限档案:

public void checkByAnnotation(SaCheckPermission at) { // 1. 获取权限码数组 String[] permissionArray = at.value(); try { // 2. 按模式校验权限 if(at.mode() == SaMode.AND) { checkPermissionAnd(permissionArray); } else { checkPermissionOr(permissionArray); } } catch (NotPermissionException e) { // 3. 权限不通过时尝试角色校验 if(at.orRole().length > 0) { for (String role : at.orRole()) { String[] roles = SaFoxUtil.convertStringToArray(role); if(hasRoleAnd(roles)) { return; // 角色校验通过 } } } throw e; // 全部校验失败 } }

这个流程体现了"先严后宽"的校验策略:

  1. 首先严格检查声明的权限
  2. 权限不足时考虑备选角色方案
  3. 全部不满足才最终拒绝

4.2 权限数据的获取链路

权限数据的获取就像查档案:

public List<String> getPermissionList(Object loginId) { // 1. 获取StpInterface实现 StpInterface stpInterface = SaManager.getStpInterface(); // 2. 委托实现类查询权限列表 return stpInterface.getPermissionList(loginId, loginType); }

关键设计点:

  • 通过SaManager获取全局StpInterface实例
  • 实际数据查询委托给具体实现类
  • 支持自定义实现(数据库、Redis等)

4.3 自定义权限实现方案

实现自己的权限系统就像定制公司门禁卡:

@Component public class CustomStpInterface implements StpInterface { @Override public List<String> getPermissionList(Object loginId, String loginType) { // 实际业务中可能: // 1. 查询数据库 // 2. 调用外部权限服务 // 3. 读取缓存等 return Arrays.asList("user:add", "user:edit"); } @Override public List<String> getRoleList(Object loginId, String loginType) { return Arrays.asList("admin", "operator"); } }

开发建议:

  • 实现类应该标记为Spring组件
  • 权限数据建议缓存优化
  • 多账号体系可通过loginType区分

5. 实战中的常见问题

5.1 权限缓存的最佳实践

权限数据就像员工工牌,应该合理缓存:

public class RedisStpInterface implements StpInterface { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public List<String> getPermissionList(Object loginId, String loginType) { String cacheKey = "perm:" + loginType + ":" + loginId; List<String> permissions = (List<String>) redisTemplate.opsForValue().get(cacheKey); if(permissions == null) { permissions = userService.queryPermissions(loginId); redisTemplate.opsForValue().set(cacheKey, permissions, 2, TimeUnit.HOURS); } return permissions; } }

缓存策略建议:

  • 设置合理过期时间(如2小时)
  • 权限变更时主动清除缓存
  • 考虑使用Hash结构存储

5.2 多账号体系的权限隔离

就像集团不同子公司使用独立的门禁系统:

@SaCheckPermission(value = "order:query", type = "merchant") @GetMapping("/merchant/orders") public List<Order> getMerchantOrders() { // 商户端订单查询 } @SaCheckPermission(value = "order:query", type = "admin") @GetMapping("/admin/orders") public List<Order> getAllOrders() { // 管理端订单查询 }

实现要点:

  • 通过type参数区分账号体系
  • 自定义StpInterface时根据loginType处理
  • 建议不同体系使用不同的权限前缀

5.3 权限树的动态校验

对于层级权限(如menu:sys:user),可以这样处理:

public class TreePermissionCheck { public static boolean hasPermission(String required, List<String> owned) { // 将要求的权限拆分为层级 String[] levels = required.split(":"); // 逐级检查 StringBuilder current = new StringBuilder(); for (String level : levels) { current.append(level); if (!owned.contains(current.toString())) { return false; } current.append(":"); } return true; } }

这种设计适合:

  • 菜单权限控制
  • 数据范围权限
  • 多级资源访问控制

6. 深度定制与扩展

6.1 自定义注解开发

就像设计新型门禁卡:

@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface SaCheckDepartment { String value(); // 部门ID boolean includeChildren() default false; // 是否包含子部门 } // 使用示例 @SaCheckDepartment("dept:finance") public void financialOperation() { // 仅限财务部门访问 }

实现步骤:

  1. 定义注解元素
  2. 实现对应的校验逻辑
  3. 注册到SaStrategy

6.2 动态权限的实时生效

权限热更新就像实时调整门禁权限:

@Aspect @Component public class PermissionRefreshAspect { @Autowired private PermissionCache permissionCache; @AfterReturning("execution(* com..PermissionService.update*(..))") public void refreshCache(JoinPoint jp) { Object loginId = jp.getArgs()[0]; permissionCache.evict(loginId); // 清除缓存 } }

关键点:

  • 使用Spring AOP监听权限变更
  • 及时清除相关缓存
  • 考虑分布式环境下的缓存一致性

6.3 与Spring Security的对比

Sa-Token与Spring Security就像两套不同的安保系统:

特性Sa-TokenSpring Security
学习曲线平缓陡峭
配置方式注解为主配置类+DSL
权限模型RBACACL/RBAC/ABAC
分布式支持内置需额外配置
定制灵活性中等极高
社区生态活跃非常成熟

选择建议:

  • 快速开发选Sa-Token
  • 复杂场景选Spring Security
  • 也可以组合使用

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询