大数跨境
0
0

优雅!Spring Boot 一个注解,开启 SQL 数据动态权限控制

优雅!Spring Boot 一个注解,开启 SQL 数据动态权限控制 Spring全家桶实战案例
2025-07-19
1
导读:优雅!Spring Boot 一个注解,开启 SQL 数据动态权限控制
Spring Boot 3实战案例锦集PDF电子书已更新至130篇!
图片

🎉🎉《Spring Boot实战案例合集》目前已更新146个案例,我们将持续不断的更新。文末有电子书目录。

💪💪永久更新承诺

我们郑重承诺,所有订阅合集的粉丝都将享受永久免费的后续更新服务

💌💌如何获取
订阅我们的合集点我订阅,并通过私信联系我们,我们将第一时间将电子书发送给您。

→ 现在就订阅合集

环境:SpringBoot3.4.2



1. 简介

在咱们日常用的各种业务系统里,不同身份的人能看到和操作的数据是不一样的。就好比公司里,普通员工只能看自己的工作信息,领导却能查看整个部门的数据。以前要在每个查看数据的方法里都写一堆判断谁能看谁不能看的代码,又麻烦又容易出错,以后想改权限也特别费劲。

为解决这些问题,我们采用自定义注解与 AOP 技术结合实现动态权限控制。自定义注解标记需权限控制的方法,AOP 切面拦截这些方法,在方法执行前根据当前用户权限及注解配置,自动生成并添加动态 SQL 条件,精准过滤数据,实现灵活、解耦的权限管理,提升系统可维护性与扩展性。

2.实战案例

2.1 自定义注解

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface DataPermission {
  PermissionType type() default PermissionType.ORG ;  /**自定义sql*/  String sql() default "";
  /**当前执行的SQL机构别名*/  String orgAlias() default "" ;
  /**当前执行的SQL科室别名*/  String deptAlias() default "" ;
  /**当前执行的SQL用户别名*/  String userAlias() default "" ;
  public static enum PermissionType {    ALL,    // 全部数据可见    ORG,    // 机构作用域    DEPT,   // 部门作用域    SELF    // 个人作用域  }}

在该注解中,我们明确定义了若干种基本的权限作用域,此外,还可借助 sql 属性来配置自定义条件,以此实现更为精细的权限控制。

2.2 定义权限策略

我们通过定义 PermissionStrategy 接口并针对不同权限作用域实现了不同的策略。它将权限控制逻辑抽象为不同策略,根据用户权限动态调用对应实现,提高了代码的灵活性与可扩展性,便于后续新增或修改权限策略。

public interface PermissionStrategy {  PermissionType support() ;
  String condition(DataPermission dp) ;}

接下来,是针对不同的作用域进行不同的实现(根据上面的枚举类型)。

// 拥有所有权限@Componentpublic class AllPermissionStrategy implements PermissionStrategy {
  @Override  public String condition(DataPermission dp) {    return "1=1";  }  @Override  public PermissionType support() {    return DataPermission.PermissionType.ALL ;  }}// 机构权限@Componentpublic class OrgPermissionStrategy implements PermissionStrategy {
  @Override  public String condition(DataPermission dp) {    String deptIds = String.format("select id from h_dept s where s.org_id = %s"UserContext.getOrgId()) ;    if (StringUtils.hasLength(dp.orgAlias())) {      return String.format("%s.dept_id in (%s)", dp.orgAlias(), deptIds) ;    }    return String.format("dept_id in (%s)", deptIds) ;  }  @Override  public PermissionType support() {    return PermissionType.ORG ;  }}// 部门权限@Componentpublic class DeptPermissionStrategy implements PermissionStrategy {
  @Override  public String condition(DataPermission dp) {    if (StringUtils.hasLength(dp.deptAlias())) {      return String.format("%s.id = %s", dp.deptAlias(), UserContext.getDeptId()) ;    }    return String.format("dept_id = %s"UserContext.getDeptId()) ;  }  @Override  public PermissionType support() {    return PermissionType.DEPT ;  }}// 个人权限@Componentpublic class SelfPermissionStrategy implements PermissionStrategy {
  @Override  public String condition(DataPermission dp) {    if (StringUtils.hasLength(dp.userAlias())) {      return String.format("%s.user_id = %s", dp.userAlias(), UserContext.getUserId()) ;    }    return String.format("user_id = %s"UserContext.getUserId()) ;  }  @Override  public PermissionType support() {    return PermissionType.SELF ;  }}

以上针对不同的作用域分别实现了不同的测试。(文末提供了当前实现所有的DDL语句)。

2.3 定义AOP切面

接下来,我们将定义一个 Aspect 切面,其核心作用是对那些使用了 @DataPermission 注解的方法实施拦截操作。该切面主要针对两种特定参数类型,自动为其添加动态 SQL 条件,以此实现精细化的权限控制。

@Aspect@Componentpublic class DataPermissionAspect {
  private final Map<PermissionType, PermissionStrategy> strategies = new ConcurrentHashMap<>() ;  public DataPermissionAspect(List<PermissionStrategy> list) {    list.forEach(strategy -> {      strategies.put(strategy.support(), strategy) ;    }) ;  }  @SuppressWarnings("unchecked")  @Around("@annotation(perm)")  public Object applyPermission(ProceedingJoinPoint pjp, DataPermission perm) throws Throwable {    PermissionStrategy strategy = strategies.get(UserContext.getPermissionType());    if (strategy == null) {      return pjp.proceed() ;    }    Object[] args = pjp.getArgs();    // 1.如果方法上使用了注解,但是实际方法并没有参数不进行任何处理    if (args.length == 0) {      return pjp.proceed() ;    }    Object arg = args[0] ;    String condition = strategy.condition(perm) ;    // 2.如果注解中定义了sql属性,那么直接使用该属性的配置,不考虑其它作用域情况    String sql = perm.sql();    if (StringUtils.hasLength(sql)) {      condition = sql ;    }    // 3.最终我们使用AND将与实际的SQL进行了拼接    condition = " AND (" + condition  + ")";    // 4.判断两种类型的参数    if (arg instanceof BaseEntity entity) {      entity.getParams().put("dataScope", condition) ;    } else if (arg instanceof Map map) {      map.put("dataScope", condition) ;    }    return pjp.proceed() ;  }}

注意,这里我们并没有考虑一个用户有多种权限时的情况;这种情况我们应该用for循环然后将所有的权限进行 OR 操作即可。

2.4 实体对象基类

我们的所有实体类都会继承 BaseEntity,在该实体类中定义了公共的属性专门用来处理动态参数情况。

@MappedSuperclasspublic class BaseEntity {  @Id  @GeneratedValue(strategy = GenerationType.IDENTITY)  protected Long id;  @Transient  protected Map<StringObject> params = new HashMap<>() ;}

需要注意的是,若某个属性使用了 @Transient 注解,那么该属性在实际的数据库表结构中,将不会创建与之对应的字段。如下其它实体类定义:

@Entity@Table(name = "h_org")public class Org  extends BaseEntity{}@Entity@Table(name = "h_dept")public class Dept extends BaseEntity {}@Entity@Table(name = "h_user")public class User extends BaseEntity {}@Entity@Table(name = "h_order")public class Order extends BaseEntity {}

本人非常推荐使用jpa来管理我们的表结构,非常方便,尤其是Spring Data Jpa基本的CURD使用太简单了一行代码搞定。

2.5 定义Mapper 

如下为了测试不同情况,我们定义了3个方法。

public interface OrderMapper {  @DataPermission  List<OrderqueryOrder(@Param("params"Map<StringObject> params) ;
  @DataPermission(deptAlias = "y")  List<OrderqueryOrderAlias(@Param("params"Map<StringObject> params) ;  // 指定查询条件  @DataPermission(sql = "status='COMPLETED'")  List<OrderqueryOrderSql(Order order) ;}

下面是3个方法对应的XML配置

<mapper namespace="com.pack.perm.mapper.OrderMapper">  <select id="queryOrder" parameterType="hashmap" resultType="hashmap">    select * from h_order    <where>      <!-- 数据范围过滤 -->      ${params.dataScope}			    </where>  </select>  <select id="queryOrderSql" parameterType="hashmap" resultType="hashmap">    select * from h_order    <where>      <!-- 数据范围过滤 -->      ${params.dataScope}			    </where>  </select>  <select id="queryOrderAlias" parameterType="hashmap" resultType="hashmap">    select x.*,y.name from h_order x left join h_dept y on (x.dept_id = y.id)     <where>      <!-- 数据范围过滤 -->      ${params.dataScope}			    </where>  </select></mapper> 

2.6 测试

为了测试,我们模拟了当前用户信息

public class UserContext {  public static Long getUserId() {    return 1L ;  }  public static Long getDeptId() {    return 2L ;  }  public static Long getOrgId() {    return 1L ;  }  public static PermissionType getPermissionType() {    return PermissionType.DEPT ;  }}

测试用例:

@Resourceprivate OrderMapper orderMapper ;@Testpublic void testQueryOrder() {  Map<StringObject> params = new HashMap<>() ;  List<Order> ret = this.orderMapper.queryOrder(params) ;  System.err.println(ret) ;}@Testpublic void testQueryOrderSql() {  Order order = new Order() ;  List<Order> ret = this.orderMapper.queryOrderSql(order) ;  System.err.println(ret) ;}@Testpublic void queryOrderAlias() {  Map<StringObject> params = new HashMap<>() ;  List<Order> ret = this.orderMapper.queryOrderAlias(params) ;  System.err.println(ret) ;}

最终运行上面3个测试用例,控制台输出如下:

本篇文章用到的DDL语句

CREATE TABLE `h_user` (  `id` bigint(20NOT NULL AUTO_INCREMENT,  `username` varchar(50COLLATE utf8mb4_unicode_ci NOT NULL,  `password` varchar(100COLLATE utf8mb4_unicode_ci NOT NULL,  `dept_id` bigint(20DEFAULT NULL,  PRIMARY KEY (`id`),) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `h_org` (  `id` bigint(20NOT NULL AUTO_INCREMENT,  `parent_id` bigint(20DEFAULT NULL,  `org_code` varchar(50COLLATE utf8mb4_unicode_ci NOT NULL,  `org_name` varchar(100COLLATE utf8mb4_unicode_ci NOT NULL,  `path` varchar(255COLLATE utf8mb4_unicode_ci DEFAULT NULL,  PRIMARY KEY (`id`),) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `h_dept` (  `id` bigint(20NOT NULL AUTO_INCREMENT,  `name` varchar(255COLLATE utf8mb4_unicode_ci DEFAULT NULL,  `org_id` bigint(20DEFAULT NULL,  PRIMARY KEY (`id`),) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `h_order` (  `id` bigint(20NOT NULL AUTO_INCREMENT,  `order_no` varchar(50COLLATE utf8mb4_unicode_ci NOT NULL,  `user_id` bigint(20NOT NULL,  `dept_id` bigint(20NOT NULL,  `amount` decimal(15,2NOT NULL,  `status` varchar(20COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'PENDING',  PRIMARY KEY (`id`),) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `x_order` (  `id` bigint(20NOT NULL AUTO_INCREMENT,  `amount` decimal(15,2NOT NULL,  `order_no` varchar(50NOT NULL,  `order_time` datetime(6NOT NULL,  `status` int(11NOT NULL DEFAULT '0',  PRIMARY KEY (`id`),) ENGINE=InnoDB DEFAULT CHARSET=utf8;


以上是本篇文章的全部内容,如对你有帮助帮忙点赞+转发+收藏

推荐文章

新选择!基于Spring Boot监听MySQL日志Binlog实现数据实时同步

杜绝重复启动!Spring Boot 单实例运行4种方案

高级开发!手撕Controller底层调用链,深度功能扩展、性能优化

高级开发!Spring Boot + JsonNode 动态处理请求/响应属性值

高级开发!高性能Java并发编程技巧

Jackson 5 大逆天神技,搞定双向关联输出 JSON 问题

提升性能:Java工程师必备的20条SQL最佳实践

请不要自己写!Spring Boot 一个注解搞定逻辑删除,支持JPA/MyBatis

Spring Boot3新特性@RSocketExchange轻松实现消息实时推送

3个经典案例,详解Spring Boot实时推送技术

SpringBoot冷门但逆天的5个神级注解,老司机都在偷偷用!

图片
图片
图片
图片
图片
图片
图片
图片
图片

【声明】内容源于网络
0
0
Spring全家桶实战案例
Java全栈开发,前端Vue2/3全家桶;Spring, SpringBoot 2/3, Spring Cloud各种实战案例及源码解读
内容 832
粉丝 0
Spring全家桶实战案例 Java全栈开发,前端Vue2/3全家桶;Spring, SpringBoot 2/3, Spring Cloud各种实战案例及源码解读
总阅读195
粉丝0
内容832