Ruoyi-vue-plus-5.x第二篇Sa-Token权限认证实战:1.4 自定义注解与动态权限控制

张开发
2026/4/18 3:25:02 15 分钟阅读

分享文章

Ruoyi-vue-plus-5.x第二篇Sa-Token权限认证实战:1.4 自定义注解与动态权限控制
1. 自定义注解在Sa-Token中的核心价值在Ruoyi-vue-plus这类企业级开发框架中权限控制往往需要应对复杂的业务场景。系统内置的SaCheckLogin、SaCheckRole等注解虽然能解决基础需求但面对动态权限、数据权限等特殊场景时就需要我们扩展自定义注解。这就像给工具箱添加专属工具——标准螺丝刀能应付大多数情况但遇到特殊螺丝时就需要定制批头。实际开发中我遇到过这样的案例某电商系统要求不同地区的管理员只能操作本地区数据。如果只用标准权限注解我们不得不在每个方法里写重复的区域校验代码。而通过自定义RegionCheck注解只需在方法上简单标注就能自动完成校验代码量减少60%以上。这种扩展能力正是Sa-Token设计精妙之处——它提供了完整的权限认证骨架同时留出了充足的定制空间。自定义注解的核心优势体现在三个方面语义化封装将复杂的权限判断逻辑隐藏在注解背后业务代码只需声明需要什么权限动态组合通过注解属性实现条件判断比如DataPermission(typedept)集中管理所有权限规则在切面中统一处理避免校验逻辑分散在各处2. 自定义注解开发全流程2.1 定义注解接口我们先从最简单的案例开始——创建一个检查数据所有者权限的注解。这个注解将验证当前用户是否是被操作数据的拥有者。/** * 数据所有者校验注解 */ Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface DataOwnerCheck { /** * 数据ID在方法参数中的位置 * 例如0表示第一个参数 */ int dataIdPosition() default 0; /** * 数据ID字段名当参数是对象时使用 */ String idField() default id; /** * 权限前缀 */ String prefix() default data; }这个设计考虑了三种常见场景直接传递ID值的情况如deleteById(Long id)传递DTO对象的情况如update(UserDTO dto)需要特定权限前缀的情况如order:delete2.2 实现切面逻辑注解定义好后我们需要通过切面来实现具体校验逻辑Aspect Component public class DataOwnerCheckAspect { Autowired private DataPermissionService dataService; Around(annotation(check)) public Object checkDataOwner(ProceedingJoinPoint joinPoint, DataOwnerCheck check) throws Throwable { // 1. 获取当前登录用户 Long currentUserId Long.valueOf(StpUtil.getLoginId().toString()); // 2. 解析目标数据ID Object[] args joinPoint.getArgs(); Object dataId resolveDataId(args, check); // 3. 验证数据所有权 if(!dataService.isOwner(dataId, currentUserId)) { throw new SaTokenException(无权限操作该数据); } // 4. 放行原方法 return joinPoint.proceed(); } private Object resolveDataId(Object[] args, DataOwnerCheck check) { Object targetParam args[check.dataIdPosition()]; // 如果是简单类型直接返回 if(targetParam instanceof Long || targetParam instanceof String) { return targetParam; } // 如果是对象则反射获取字段值 try { Field field targetParam.getClass().getDeclaredField(check.idField()); field.setAccessible(true); return field.get(targetParam); } catch (Exception e) { throw new SaTokenException(解析数据ID失败, e); } } }这个切面处理了以下关键点与Sa-Token的登录体系无缝集成支持灵活的参数位置配置同时处理基本类型和对象类型的参数统一的权限异常处理2.3 在Ruoyi-vue-plus中集成在Ruoyi-vue-plus项目中我们需要确保自定义注解能被Spring管理在application.yml中开启注解扫描sa-token: # 启用注解拦截器 is-annotation: true将切面类加入IoC容器Configuration public class AopConfig { Bean public DataOwnerCheckAspect dataOwnerCheckAspect() { return new DataOwnerCheckAspect(); } }在Controller中使用注解RestController RequestMapping(/order) public class OrderController { DataOwnerCheck(dataIdPosition0, prefixorder) DeleteMapping(/{orderId}) public R deleteOrder(PathVariable Long orderId) { // 业务逻辑 } DataOwnerCheck(dataIdPosition0, idFieldorderId) PutMapping public R updateOrder(RequestBody OrderDTO dto) { // 业务逻辑 } }3. 动态权限控制实战3.1 基于业务的动态权限很多场景下用户权限需要根据业务状态动态变化。比如审批流中当前处理人需要临时获得特定权限。我们在Ruoyi-vue-plus中可以这样实现/** * 动态权限注解 */ public interface DynamicPermission { /** * 权限表达式SpEL * 可以引用方法参数和Spring Bean */ String value(); } Aspect Component public class DynamicPermissionAspect { Autowired private ExpressionParser parser; Around(annotation(dynamic)) public Object checkDynamicPermission(ProceedingJoinPoint joinPoint, DynamicPermission dynamic) throws Throwable { // 解析SpEL表达式 EvaluationContext context new StandardEvaluationContext(); context.setVariable(args, joinPoint.getArgs()); String[] requiredPermissions parser.parseExpression(dynamic.value()) .getValue(context, String[].class); // 校验权限 StpUtil.checkPermissionAnd(requiredPermissions); return joinPoint.proceed(); } } // 使用示例 RestController RequestMapping(/approval) public class ApprovalController { DynamicPermission(permService.getApprovalPerms(#approval.type)) PostMapping public R approve(RequestBody ApprovalVO approval) { // 审批逻辑 } }这种实现方式的特点是使用SpEL表达式实现灵活配置支持从Spring容器调用Bean方法可以访问方法参数作为变量与Sa-Token原生权限检查无缝结合3.2 数据行级权限控制行级权限是企业管理系统的常见需求比如部门经理只能查看本部门数据。我们可以扩展Sa-Token的StpInterfaceComponent public class CustomStpInterface implements StpInterface { Autowired private DeptService deptService; Override public ListString getPermissionList(Object loginId, String loginType) { ListString perms new ArrayList(); // 1. 获取基础权限 perms.addAll(getBasicPermissions(loginId)); // 2. 添加动态数据权限 Long userId Long.valueOf(loginId.toString()); perms.addAll(getDataPermissions(userId)); return perms; } private ListString getDataPermissions(Long userId) { ListString perms new ArrayList(); // 获取用户部门 Long deptId deptService.getUserDeptId(userId); // 添加部门数据权限 perms.add(data:dept: deptId); // 如果是部门负责人添加管理权限 if(deptService.isDeptManager(userId)) { perms.add(data:dept:manage: deptId); } return perms; } }然后在Repository层实现数据过滤Repository public class UserRepositoryImpl implements UserRepositoryCustom { PersistenceContext private EntityManager em; public ListUser findFilteredUsers(SpecificationUser spec) { CriteriaBuilder cb em.getCriteriaBuilder(); CriteriaQueryUser query cb.createQuery(User.class); RootUser root query.from(User.class); // 1. 业务条件 Predicate businessPredicate spec.toPredicate(root, query, cb); // 2. 数据权限条件 Predicate dataPredicate buildDataPredicate(cb, root); // 组合条件 query.where(cb.and(businessPredicate, dataPredicate)); return em.createQuery(query).getResultList(); } private Predicate buildDataPredicate(CriteriaBuilder cb, RootUser root) { // 获取当前用户的数据权限 ListString dataPerms StpUtil.getPermissionList() .stream() .filter(p - p.startsWith(data:dept:)) .collect(Collectors.toList()); // 构建查询条件 ListPredicate predicates new ArrayList(); for(String perm : dataPerms) { String[] parts perm.split(:); Long deptId Long.valueOf(parts[2]); predicates.add(cb.equal(root.get(dept).get(id), deptId)); } return cb.or(predicates.toArray(new Predicate[0])); } }4. 最佳实践与避坑指南4.1 性能优化建议在权限系统设计中性能是需要重点考虑的因素。根据我的实战经验推荐以下优化策略缓存权限数据Sa-Token默认会缓存权限列表但要特别注意自定义权限的缓存策略// 自定义缓存实现示例 Component public class RedisPermissionCache implements SaPermissionCache { Autowired private RedisTemplateString, Object redisTemplate; Override public ListString getPermissionList(Object loginId, String loginType) { String key perm: loginType : loginId; return (ListString) redisTemplate.opsForValue().get(key); } Override public void setPermissionList(Object loginId, String loginType, ListString permissionList) { String key perm: loginType : loginId; redisTemplate.opsForValue().set(key, permissionList, 30, TimeUnit.MINUTES); } }批量权限检查避免在循环中进行多次权限检查// 不推荐 for(Data data : dataList) { StpUtil.checkPermission(data:view: data.getId()); } // 推荐 ListLong accessibleIds dataService.filterAccessibleIds( dataList.stream().map(Data::getId).collect(Collectors.toList()) ); ListData result dataList.stream() .filter(d - accessibleIds.contains(d.getId())) .collect(Collectors.toList());延迟加载对于复杂的权限计算可以采用懒加载策略Getter public class LazyPermissionCheck { private final SupplierBoolean checkResult; public LazyPermissionCheck(String permission) { this.checkResult () - StpUtil.hasPermission(permission); } }4.2 常见问题排查在实际项目中我遇到过几个典型问题值得分享注解不生效检查是否开启了注解拦截sa-token.is-annotationtrue确认切面类被Spring管理有Component或Aspect注解检查切入点表达式是否正确匹配权限缓存不一致当用户权限变更时需要手动清除缓存public void updateUserRoles(Long userId, ListLong roleIds) { // 更新数据库 userRoleService.updateRoles(userId, roleIds); // 清除权限缓存 StpUtil.logout(userId); }SpEL表达式性能问题复杂表达式建议预编译private final Expression permExpression parser.parseExpression( permService.getDynamicPerms(#root.args[0]) ); public Object checkPermission(ProceedingJoinPoint joinPoint) { String[] perms permExpression.getValue( new StandardEvaluationContext(joinPoint.getArgs()), String[].class ); // ... }在Ruoyi-vue-plus项目中集成Sa-Token权限系统时建议先从简单的注解开始逐步扩展到自定义注解和动态权限。每次迭代后都要进行全面的权限测试确保不会出现越权访问。对于复杂的业务场景可以考虑使用权限决策树来管理各种判断逻辑。

更多文章