在现代企业级应用系统中,数据权限隔离是保障业务安全的核心需求之一。随着业务规模的扩大,系统往往需要支持多区域、多项目公司的数据隔离能力,例如某位业务人员只能查看所属区域的订单数据。本文将分享如何通过自定义 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 进行重写,来校验数据权限
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)); } }
定义数据权限的注解 @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 参数解析器实现数据权限控制,我们成功构建了一个优雅、高效的解决方案。该方案具有以下优点:
在实际应用中,该方案能够有效支持多区域、多项目公司的数据隔离需求,为业务人员提供精确的数据访问控制。未来还可以进一步优化,如增加权限缓存、支持更复杂的权限规则等。
这种基于参数解析器的权限控制方案不仅适用于当前场景,也可以为其他类似的数据过滤需求提供借鉴,是 Spring MVC 高级应用的典型实践。
本文作者:张豪
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!