剖析 Sa-Token 权限认证:从注解到拦截器的完整调用链路

张开发
2026/4/20 23:52:57 15 分钟阅读

分享文章

剖析 Sa-Token 权限认证:从注解到拦截器的完整调用链路
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会完成以下动作拦截器捕获请求并定位到方法注解提取注解中的权限标识user:delete获取当前登录用户的所有权限列表进行权限匹配校验通过则放行否则抛出异常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; // 验证通过 } }关键点在于只拦截Spring MVC的HandlerMethod通过方法对象获取所有安全注解将实际校验工作委托给SaStrategy3.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; // 全部校验失败 } }这个流程体现了先严后宽的校验策略首先严格检查声明的权限权限不足时考虑备选角色方案全部不满足才最终拒绝4.2 权限数据的获取链路权限数据的获取就像查档案public ListString 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 ListString getPermissionList(Object loginId, String loginType) { // 实际业务中可能 // 1. 查询数据库 // 2. 调用外部权限服务 // 3. 读取缓存等 return Arrays.asList(user:add, user:edit); } Override public ListString getRoleList(Object loginId, String loginType) { return Arrays.asList(admin, operator); } }开发建议实现类应该标记为Spring组件权限数据建议缓存优化多账号体系可通过loginType区分5. 实战中的常见问题5.1 权限缓存的最佳实践权限数据就像员工工牌应该合理缓存public class RedisStpInterface implements StpInterface { Autowired private RedisTemplateString, Object redisTemplate; Override public ListString getPermissionList(Object loginId, String loginType) { String cacheKey perm: loginType : loginId; ListString permissions (ListString) 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 ListOrder getMerchantOrders() { // 商户端订单查询 } SaCheckPermission(value order:query, type admin) GetMapping(/admin/orders) public ListOrder getAllOrders() { // 管理端订单查询 }实现要点通过type参数区分账号体系自定义StpInterface时根据loginType处理建议不同体系使用不同的权限前缀5.3 权限树的动态校验对于层级权限如menu:sys:user可以这样处理public class TreePermissionCheck { public static boolean hasPermission(String required, ListString 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() { // 仅限财务部门访问 }实现步骤定义注解元素实现对应的校验逻辑注册到SaStrategy6.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也可以组合使用

更多文章