在现代企业级应用系统中,数据权限隔离是保障业务安全的核心需求之一。随着业务规模的扩大,系统往往需要支持多区域、多项目公司的数据隔离能力,例如某位业务人员只能查看所属区域的订单数据。本文将分享如何通过自定义 Spring MVC 参数解析器与 Redis 缓存机制,实现对分页接口的精细化数据权限控制方案,并附完整代码示例。
在现有分页查询接口中,业务人员可能拥有跨区域、跨项目公司的数据访问权限,这可能导致敏感数据泄露或越权操作。例如:
为解决上述问题,需实现以下目标:
传统的权限控制通常在 Service 层或 DAO 层实现,但这样会导致业务代码与权限代码耦合度过高。根据需求内容进行思考,在技术层面上实现方式如下:
| 实现方式 | 优点 | 缺点 |
|---|---|---|
处理器拦截器 HandlerInterceptor | 可以对请求预处理、后处理、还有结束后处理 | 无法对直接获取到请求参数对象,需要各种反射获取 |
| AOP 拦截,通过反射赋值 | 直观、快捷,更多的拦截方式处理 | 拦截的 controller 扫描接口太多,浪费内存,每个接口都会拦截一次 |
自定义参数解析器 HandlerMethodArgumentResolver | 在请求时直接注入参数,无需转换 | 实现复杂度高 |
最终我选择在 Web 层通过自定义参数解析器实现数据权限的自动注入,具有以下优势:
使用 Redis 的 Set 结构缓存用户权限,Key 格式为:DATA_PERMISSION_CONFIG_KEY:{userId}:{fieldName},例如: data_permission:1001:areaCodes 存储用户 1001 的区域编码集合。
通过继承 Spring MVC 的 RequestResponseBodyMethodProcessor,实现自定义参数解析器:
创建 @DataPermissionBody 注解标记需要权限注入的参数,其行为与 @RequestBody 类似,但具备权限处理逻辑。
在新增财务数据权限中,新增编辑删除项目公司都对应的添加到缓存中,请求接受时会根据 controller 层自定义注解进行参数解析,查询数据权限赋值到现有的查询对象中,实现数据权限的控制,绑定流程:请求到达 → 参数解析器识别注解 → 从Redis加载权限数据 → 反射注入DTO字段 → 业务方法执行

默认的 @ResponseBody 注解参数解析器是 RequestResponseBodyMethodProcessor,我们调整的接口上都是基于此方式,所以对 RequestResponseBodyMethodProcessor 进行重写,来校验数据权限
javapublic 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));
}
}
定义数据权限的注解 @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 许可协议。转载请注明出处!