2025-09-18
场景与实战
0

目录

需求背景
实现思路
权限数据存储
参数解析器改造
注解设计
数据绑定流程
代码实现
自定义参数解析器
配置类注册解析器
注解与DTO定义
控制器使用示例
总结

在现代企业级应用系统中,数据权限隔离是保障业务安全的核心需求之一。随着业务规模的扩大,系统往往需要支持多区域、多项目公司的数据隔离能力,例如某位业务人员只能查看所属区域的订单数据。本文将分享如何通过自定义 Spring MVC 参数解析器与 Redis 缓存机制,实现对分页接口的精细化数据权限控制方案,并附完整代码示例。

需求背景

在现有分页查询接口中,业务人员可能拥有跨区域、跨项目公司的数据访问权限,这可能导致敏感数据泄露或越权操作。例如:

  • 财务人员A仅负责华东区域,却能查询全国所有订单;
  • 项目公司B的业务员可访问其他公司的客户数据。

为解决上述问题,需实现以下目标:

  • 权限动态配置:通过管理后台为用户分配区域/项目公司权限;
  • 数据自动过滤:接口调用时自动注入用户权限范围内的数据标识;
  • 无侵入改造:不修改现有业务代码,仅扩展权限逻辑。

实现思路

传统的权限控制通常在 Service 层或 DAO 层实现,但这样会导致业务代码与权限代码耦合度过高。根据需求内容进行思考,在技术层面上实现方式如下:

实现方式优点缺点
处理器拦截器 HandlerInterceptor可以对请求预处理、后处理、还有结束后处理无法对直接获取到请求参数对象,需要各种反射获取
AOP 拦截,通过反射赋值直观、快捷,更多的拦截方式处理拦截的 controller 扫描接口太多,浪费内存,每个接口都会拦截一次
自定义参数解析器 HandlerMethodArgumentResolver在请求时直接注入参数,无需转换实现复杂度高

最终我选择在 Web 层通过自定义参数解析器实现数据权限的自动注入,具有以下优势:

  • 解耦合:权限逻辑与业务逻辑分离
  • 集中管理:所有权限相关处理集中在一个地方
  • 易于维护:新增或修改权限逻辑只需调整解析器
  • 低侵入性:通过注解方式实现,不影响现有代码

权限数据存储

使用 Redis 的 Set 结构缓存用户权限,Key 格式为:DATA_PERMISSION_CONFIG_KEY:{userId}:{fieldName},例如: data_permission:1001:areaCodes 存储用户 1001 的区域编码集合。

参数解析器改造

通过继承 Spring MVC 的 RequestResponseBodyMethodProcessor,实现自定义参数解析器:

  • 权限注入时机:在请求参数反序列化后,通过反射动态填充权限字段;
  • 字段匹配规则:优先匹配 DTO 类中定义的 companyCodes/areaCodes 字段。

注解设计

创建 @DataPermissionBody 注解标记需要权限注入的参数,其行为与 @RequestBody 类似,但具备权限处理逻辑。

数据绑定流程

在新增财务数据权限中,新增编辑删除项目公司都对应的添加到缓存中,请求接受时会根据 controller 层自定义注解进行参数解析,查询数据权限赋值到现有的查询对象中,实现数据权限的控制,绑定流程:请求到达 → 参数解析器识别注解 → 从Redis加载权限数据 → 反射注入DTO字段 → 业务方法执行

image.png

代码实现

自定义参数解析器

默认的 @ResponseBody 注解参数解析器是 RequestResponseBodyMethodProcessor,我们调整的接口上都是基于此方式,所以对 RequestResponseBodyMethodProcessor 进行重写,来校验数据权限

java
public class DataPermissionMethodArgumentResolver extends RequestResponseBodyMethodProcessor { /** * Basic constructor with converters only. Suitable for resolving * {@code @RequestBody}. For handling {@code @ResponseBody} consider also * providing a {@code ContentNegotiationManager}. * * @param converters */ public DataPermissionMethodArgumentResolver(List<HttpMessageConverter<?>> converters) { super(converters); } @Override public boolean supportsParameter(MethodParameter parameter) { // 判断是否包含数据权限类 return parameter.hasParameterAnnotation(DataPermissionBody.class) && parameter.getParameterType().getSuperclass().isAssignableFrom(DataPermissionDTO.class); } /** * Throws MethodArgumentNotValidException if validation fails. * * @param parameter * @param mavContainer * @param webRequest * @param binderFactory * @throws HttpMessageNotReadableException if {@link RequestBody#required()} * is {@code true} and there is no body content or if there is no suitable * converter to read the content with. */ @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { // 获取参数 Object resolveArgument = super.resolveArgument(parameter, mavContainer, webRequest, binderFactory); try { // 反射设置给对象的父类属性 Class<?> superclass = parameter.getParameterType().getSuperclass(); for (Field declaredField : superclass.getDeclaredFields()) { // 获取数据权限 Set<String> members = RedisTemplateHolder.redisTemplate().opsForSet().members(Constant.DATA_PERMISSION_CONFIG_KEY + CurrentUserContext.getUserId() + ":" + declaredField.getName()); logger.info("数据权限:" + members); if (members != null && declaredField.getType().isAssignableFrom(List.class)) { // 设置字段值 ReflectUtil.setAccessible(declaredField); ReflectUtil.setFieldValue(resolveArgument, declaredField, new ArrayList<>(members)); } } } catch (Exception e) { logger.error("数据权限参数解析异常", e); } return resolveArgument; } @Override protected boolean checkRequired(MethodParameter parameter) { DataPermissionBody requestBody = parameter.getParameterAnnotation(DataPermissionBody.class); return (requestBody != null && requestBody.required() && !parameter.isOptional()); } }

配置类注册解析器

将参数解析器添加到 MVC 配置中,实现 addArgumentResolvers 方法

java
@Configuration public class BusinessWebMvcConfigurer implements WebMvcConfigurer { private List<HttpMessageConverter<?>> messageConverters; @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { WebMvcConfigurer.super.extendMessageConverters(converters); this.messageConverters = converters; } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(new DataPermissionMethodArgumentResolver(messageConverters)); } }

注解与DTO定义

定义数据权限的注解 @DataPermissionBody,其本质也就是换名的 @ResponseBody

java
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataPermissionBody { /** * Whether body content is required. * <p>Default is {@code true}, leading to an exception thrown in case * there is no body content. Switch this to {@code false} if you prefer * {@code null} to be passed when the body content is {@code null}. * @since 3.2 */ boolean required() default true; }
java
@Data public class DataPermissionDTO extends PageReqDTO { /** * 项目公司编码 */ private List<String> companyCodes; /** * 区域编码 */ private List<String> areaCodes; }

控制器使用示例

在使用的过程中,需要将接口的入参标记注解 @DataPermissionBody,并继承 extends DataPermissionDTO,这样入参对象中通过反射就被扩展了数据权限的参数

java
// 业务请求的 DTO public class OrderRequestDTO extends DataPermissionDTO { // 业务字段定义... } @PostMapping(value = "/page") public XEnergyResponse<PageResDTO<OrderPageRes>> page(@DataPermissionBody OrderRequestDTO req) { // 业务逻辑处理 }

总结

通过自定义 Spring MVC 参数解析器实现数据权限控制,我们成功构建了一个优雅、高效的解决方案。该方案具有以下优点:

  • 解耦性强:权限逻辑与业务逻辑完全分离,符合单一职责原则

  • 扩展性好:新增权限维度只需在 DTO 基类中添加字段并在 Redis 中配置

  • 维护简单:权限逻辑集中处理,便于统一管理和优化

  • 侵入性低:通过注解方式实现,对现有代码影响极小

在实际应用中,该方案能够有效支持多区域、多项目公司的数据隔离需求,为业务人员提供精确的数据访问控制。未来还可以进一步优化,如增加权限缓存、支持更复杂的权限规则等。

这种基于参数解析器的权限控制方案不仅适用于当前场景,也可以为其他类似的数据过滤需求提供借鉴,是 Spring MVC 高级应用的典型实践。

本文作者:柳始恭

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!