🎉🎉《Spring Boot实战案例合集》目前已更新146个案例,我们将持续不断的更新。文末有电子书目录。
💪💪永久更新承诺
我们郑重承诺,所有订阅合集的粉丝都将享受永久免费的后续更新服务。
💌💌如何获取
订阅我们的合集《点我订阅》,并通过私信联系我们,我们将第一时间将电子书发送给您。
环境:SpringBoot3.4.2
1. 简介
在咱们日常用的各种业务系统里,不同身份的人能看到和操作的数据是不一样的。就好比公司里,普通员工只能看自己的工作信息,领导却能查看整个部门的数据。以前要在每个查看数据的方法里都写一堆判断谁能看谁不能看的代码,又麻烦又容易出错,以后想改权限也特别费劲。
为解决这些问题,我们采用自定义注解与 AOP 技术结合实现动态权限控制。自定义注解标记需权限控制的方法,AOP 切面拦截这些方法,在方法执行前根据当前用户权限及注解配置,自动生成并添加动态 SQL 条件,精准过滤数据,实现灵活、解耦的权限管理,提升系统可维护性与扩展性。
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) ;}
接下来,是针对不同的作用域进行不同的实现(根据上面的枚举类型)。
// 拥有所有权限public class AllPermissionStrategy implements PermissionStrategy {public String condition(DataPermission dp) {return "1=1";}public PermissionType support() {return DataPermission.PermissionType.ALL ;}}// 机构权限public class OrgPermissionStrategy implements PermissionStrategy {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) ;}public PermissionType support() {return PermissionType.ORG ;}}// 部门权限public class DeptPermissionStrategy implements PermissionStrategy {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()) ;}public PermissionType support() {return PermissionType.DEPT ;}}// 个人权限public class SelfPermissionStrategy implements PermissionStrategy {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()) ;}public PermissionType support() {return PermissionType.SELF ;}}
以上针对不同的作用域分别实现了不同的测试。(文末提供了当前实现所有的DDL语句)。
2.3 定义AOP切面
接下来,我们将定义一个 Aspect 切面,其核心作用是对那些使用了 @DataPermission 注解的方法实施拦截操作。该切面主要针对两种特定参数类型,自动为其添加动态 SQL 条件,以此实现精细化的权限控制。
public class DataPermissionAspect {private final Map<PermissionType, PermissionStrategy> strategies = new ConcurrentHashMap<>() ;public DataPermissionAspect(List<PermissionStrategy> list) {list.forEach(strategy -> {strategies.put(strategy.support(), strategy) ;}) ;}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,在该实体类中定义了公共的属性专门用来处理动态参数情况。
public class BaseEntity {(strategy = GenerationType.IDENTITY)protected Long id;protected Map<String, Object> params = new HashMap<>() ;}
需要注意的是,若某个属性使用了 @Transient 注解,那么该属性在实际的数据库表结构中,将不会创建与之对应的字段。如下其它实体类定义:
public class Org extends BaseEntity{}public class Dept extends BaseEntity {}public class User extends BaseEntity {}public class Order extends BaseEntity {}
本人非常推荐使用jpa来管理我们的表结构,非常方便,尤其是Spring Data Jpa基本的CURD使用太简单了一行代码搞定。
2.5 定义Mapper
如下为了测试不同情况,我们定义了3个方法。
public interface OrderMapper {List<Order> queryOrder(("params") Map<String, Object> params) ;(deptAlias = "y")List<Order> queryOrderAlias(("params") Map<String, Object> params) ;// 指定查询条件(sql = "status='COMPLETED'")List<Order> queryOrderSql(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 ;}}
测试用例:
private OrderMapper orderMapper ;public void testQueryOrder() {Map<String, Object> params = new HashMap<>() ;List<Order> ret = this.orderMapper.queryOrder(params) ;System.err.println(ret) ;}public void testQueryOrderSql() {Order order = new Order() ;List<Order> ret = this.orderMapper.queryOrderSql(order) ;System.err.println(ret) ;}public void queryOrderAlias() {Map<String, Object> params = new HashMap<>() ;List<Order> ret = this.orderMapper.queryOrderAlias(params) ;System.err.println(ret) ;}
最终运行上面3个测试用例,控制台输出如下:
本篇文章用到的DDL语句
CREATE TABLE `h_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`username` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,`password` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,`dept_id` bigint(20) DEFAULT NULL,PRIMARY KEY (`id`),) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;CREATE TABLE `h_org` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`parent_id` bigint(20) DEFAULT NULL,`org_code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,`org_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,`path` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,PRIMARY KEY (`id`),) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;CREATE TABLE `h_dept` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,`org_id` bigint(20) DEFAULT NULL,PRIMARY KEY (`id`),) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;CREATE TABLE `h_order` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`order_no` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,`user_id` bigint(20) NOT NULL,`dept_id` bigint(20) NOT NULL,`amount` decimal(15,2) NOT NULL,`status` varchar(20) COLLATE 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(20) NOT NULL AUTO_INCREMENT,`amount` decimal(15,2) NOT NULL,`order_no` varchar(50) NOT NULL,`order_time` datetime(6) NOT NULL,`status` int(11) NOT NULL DEFAULT '0',PRIMARY KEY (`id`),) ENGINE=InnoDB DEFAULT CHARSET=utf8;
推荐文章
新选择!基于Spring Boot监听MySQL日志Binlog实现数据实时同步
高级开发!手撕Controller底层调用链,深度功能扩展、性能优化
高级开发!Spring Boot + JsonNode 动态处理请求/响应属性值
Jackson 5 大逆天神技,搞定双向关联输出 JSON 问题
请不要自己写!Spring Boot 一个注解搞定逻辑删除,支持JPA/MyBatis
Spring Boot3新特性@RSocketExchange轻松实现消息实时推送
SpringBoot冷门但逆天的5个神级注解,老司机都在偷偷用!


