Master Dynamic Data Permissions in Spring Boot 3 with Custom Annotations & AOP

This article demonstrates how to implement flexible, decoupled data permission control in Spring Boot 3 by defining a @DataPermission annotation, creating strategy classes for different scopes, wiring them through an AOP aspect, and integrating the solution with JPA entities, MyBatis mappers, and comprehensive test cases.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Master Dynamic Data Permissions in Spring Boot 3 with Custom Annotations & AOP

Environment: Spring Boot 3.4.2

1. Introduction

In typical business systems, different users have different data visibility. Hard‑coding permission checks in every query leads to duplicated, error‑prone code and makes future changes difficult. To solve this, we combine a custom annotation with AOP to generate dynamic SQL conditions automatically, achieving flexible and decoupled permission management.

2. Practical Cases

2.1 Custom Annotation

@Retention(RetentionPolicy.RUNTIME)</code><code>@Target(ElementType.METHOD)</code><code>public @interface DataPermission {</code><code>    PermissionType type() default PermissionType.ORG;</code><code>    /** custom sql */</code><code>    String sql() default "";</code><code>    /** current SQL org alias */</code><code>    String orgAlias() default "";</code><code>    /** current SQL dept alias */</code><code>    String deptAlias() default "";</code><code>    /** current SQL user alias */</code><code>    String userAlias() default "";</code><code>    public static enum PermissionType {</code><code>        ALL, // all data visible</code><code>        ORG, // organization scope</code><code>        DEPT, // department scope</code><code>        SELF // personal scope</code><code>    }</code><code>}

2.2 Permission Strategy Interface

public interface PermissionStrategy {</code><code>    PermissionType support();</code><code>    String condition(DataPermission dp);
</code><code>}

2.3 AOP Aspect

@Aspect</code><code>@Component</code><code>public class DataPermissionAspect {</code><code>    private final Map<PermissionType, PermissionStrategy> strategies = new ConcurrentHashMap<>();</code><code>    public DataPermissionAspect(List<PermissionStrategy> list) {</code><code>        list.forEach(s -> strategies.put(s.support(), s));</code><code>    }</code><code>    @SuppressWarnings("unchecked")</code><code>    @Around("@annotation(perm)")</code><code>    public Object applyPermission(ProceedingJoinPoint pjp, DataPermission perm) throws Throwable {</code><code>        PermissionStrategy strategy = strategies.get(UserContext.getPermissionType());</code><code>        if (strategy == null) { return pjp.proceed(); }</code><code>        Object[] args = pjp.getArgs();</code><code>        if (args.length == 0) { return pjp.proceed(); }</code><code>        Object arg = args[0];</code><code>        String condition = strategy.condition(perm);</code><code>        String sql = perm.sql();</code><code>        if (StringUtils.hasLength(sql)) { condition = sql; }</code><code>        condition = " AND (" + condition + ")";</code><code>        if (arg instanceof BaseEntity entity) {</code><code>            entity.getParams().put("dataScope", condition);</code><code>        } else if (arg instanceof Map map) {</code><code>            map.put("dataScope", condition);</code><code>        }</code><code>        return pjp.proceed();</code><code>    }</code><code>}

2.4 Base Entity

@MappedSuperclass</code><code>public class BaseEntity {</code><code>    @Id</code><code>    @GeneratedValue(strategy = GenerationType.IDENTITY)</code><code>    protected Long id;</code><code>    @Transient</code><code>    protected Map<String, Object> params = new HashMap<>();</code><code>}

2.5 Mapper Definition

public interface OrderMapper {</code><code>    @DataPermission</code><code>    List<Order> queryOrder(@Param("params") Map<String, Object> params);</code><code></code><code>    @DataPermission(deptAlias = "y")</code><code>    List<Order> queryOrderAlias(@Param("params") Map<String, Object> params);</code><code></code><code>    @DataPermission(sql = "status='COMPLETED'")</code><code>    List<Order> queryOrderSql(Order order);</code><code>}

2.6 XML Mapper Configuration

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

2.7 Test Cases

@Resource</code><code>private OrderMapper orderMapper;</code><code>@Test</code><code>public void testQueryOrder() {</code><code>    Map<String, Object> params = new HashMap<>();</code><code>    List<Order> ret = orderMapper.queryOrder(params);</code><code>    System.err.println(ret);</code><code>}</code><code>@Test</code><code>public void testQueryOrderSql() {</code><code>    Order order = new Order();</code><code>    List<Order> ret = orderMapper.queryOrderSql(order);</code><code>    System.err.println(ret);</code><code>}</code><code>@Test</code><code>public void queryOrderAlias() {</code><code>    Map<String, Object> params = new HashMap<>();</code><code>    List<Order> ret = orderMapper.queryOrderAlias(params);</code><code>    System.err.println(ret);</code><code>}

Running the three test cases produces the following console outputs:

Test output 1
Test output 1
Test output 2
Test output 2
Test output 3
Test output 3

2.8 DDL Statements Used in the Article

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

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaaopSpring BootCustom AnnotationData Permission
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.