背景

项目中需要对某一个controller层的部分接口进行权限控制,如新增、删除、修改、提交、上报等接口,request请求中携带了当前的账户id:accoutId,每个接口含有请求参数信息(body、param、path)。

实现思路

首先考虑使用拦截器实现该需求,拦截指定的请求,在拦截器中获取请求中的参数,判断当前账户是否有该接口的操作权限。拦截器代码如下:

@Component
public class PermissionInterceptor implements HandlerInterceptor {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private PermissionConfigUserService permissionConfigUserService;

    @Autowired
    private ReportCategoryMapper reportCategoryMapper;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        Long accountId = Long.parseLong(HttpUtil.getHeader(Const.HTTP_HEADER_USER_ACCOUNT_ID));
        List<ReportRoleVo> reportRoleList = permissionConfigUserService.permission(accountId);

        // 根据请求路径获取操作类型
        String requestPath = request.getRequestURI();
        String requestMethod = request.getMethod();
        logger.info("=========request path: 【{}】, method: 【{}】==========", requestPath, requestMethod);

        String operationType = parseOperationType(requestPath, requestMethod);
        Long categoryId = parseCategoryId(request);
        if (Objects.isNull(categoryId)) {
            throw new GeneralException(ErrorEnum.REPORT_RECORD_NOT_EXIST);
        }

        // 检查权限
        if (!hasPermission(reportRoleList, operationType, categoryId)) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            response.setContentType("application/json;charset=UTF-8");  // 新增内容类型
            try {
                new ObjectMapper().writeValue(
                        response.getWriter(),
                        ResponseMessage.error(ErrorEnum.PERMISSION_DENY)
                );
            } catch (IOException e) {
                logger.error("权限拦截器响应写入失败", e);
            }
            return false;
        }
        return true;
    }

    private boolean hasPermission(List<ReportRoleVo> reportRoleList, String operation, Long categoryId) {
        return reportRoleList.stream()
                .anyMatch(role -> {
                    // 提前终止:无数据分类或操作权限时直接跳过
                    if (CollectionUtils.isEmpty(role.getDataCategories())
                            || CollectionUtils.isEmpty(role.getOperates())
                            || CollectionUtils.isEmpty(role.getDataCategoryIds())) {
                        return false;
                    }

                    // 并行处理分类和操作权限校验
                    List<Long> dataCategoryIds = role.getDataCategoryIds();
                    logger.info("selected category ids : {}", dataCategoryIds);
                    boolean hasCategory = dataCategoryIds.contains(categoryId);

                    boolean hasOperation = role.getOperates().stream()
                            .anyMatch(op -> matchesOperation(op, operation));

                    return hasCategory && hasOperation;
                });
    }


    private boolean matchesOperation(ReportOperateVo operate, String targetOp) {
        return ReportDataConst.API_MANUAL_OPERATE_RULE_NAME.equals(operate.getLabel())
                && operate.getChildren().stream()
                .anyMatch(child -> targetOp.equals(child.getLabel()) && child.isSelected());
    }

    private Long parseCategoryId(HttpServletRequest request) {
        PermissionMappingEnum mapping = PermissionMappingEnum.matchMapping(
                request.getRequestURI(),
                request.getMethod()
        );

        if (mapping == null) return null;

        try {
            switch (mapping.getParamType()) {
                case REQUEST_BODY:
                    Object body = parseRequestBody(request, Object.class);
                    String paramKey = mapping.getParamKey();
                    Long id = extractValueFromBody(body, mapping.getParamKey());
                    if (Objects.equals(paramKey, "id")) {
                        // id表示记录id,需要进一步富化出分类id
                        return reportCategoryMapper.findCategoryIdByRecordId(id);
                    }
                    return id;
                case PATH_VARIABLE:
                    String pathValue = extractPathVariable(request, mapping.getParamKey());
                    if (Objects.equals(mapping.getParamKey(), "id")) {
                        return reportCategoryMapper.findCategoryIdByRecordId(Long.parseLong(pathValue));
                    }
                    return Long.parseLong(pathValue);
                case REQUEST_PARAM:
                    if (Objects.equals(mapping.getParamKey(), "id")) {
                        return reportCategoryMapper.findCategoryIdByRecordId(Long.parseLong(request.getParameter(mapping.getParamKey())));
                    }
                    return Long.parseLong(request.getParameter(mapping.getParamKey()));
                default:
                    return null;
            }
        } catch (Exception e) {
            throw new RuntimeException("分类ID解析失败: " + e.getMessage());
        }
    }

    private Long extractValueFromBody(Object body, String key) {
        if (body instanceof Map) {
            Object value = ((Map<?,?>) body).get(key);
            return value != null ? ((Number) value).longValue() : null;
        }
        try {
            Field field = body.getClass().getDeclaredField(key);
            field.setAccessible(true);
            return ((Number) field.get(body)).longValue();
        } catch (Exception e) {
            throw new RuntimeException("从请求体提取字段失败: " + key);
        }
    }

    @SuppressWarnings("unchecked")
    private String extractPathVariable(HttpServletRequest request, String paramName) {
        Map<String, String> pathVariables = (Map<String, String>) request.getAttribute(
                HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
        return pathVariables.get(paramName);
    }

    private <T> T parseRequestBody(HttpServletRequest request, Class<T> clazz) {
        try {
            return new ObjectMapper().readValue(request.getInputStream(), clazz);
        } catch (IOException e) {
            logger.error("请求体解析错误", e);
        }
        return null;
    }

    private String parseOperationType(String path, String method) {
        return PermissionMappingEnum.matchOperation(path, method);
    }
}

注册拦截器:

@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private GlobalLogInterceptor globalLogInterceptor;

    @Autowired
    private PermissionInterceptor permissionInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
        registry.addInterceptor(permissionInterceptor)
                .addPathPatterns(Arrays.stream(PermissionMappingEnum.values())
                        .map(PermissionMappingEnum::getPathPattern)
                        .collect(Collectors.toList()));
    }
}

大工告成,运行代码后,postman调用接口报错,查看日志错误信息如下:HttpMessageNotReadableException: Required request body is missing:

问题排查

问题出现在request.getInputStream()这一行代码上,http请求的输入流为网络传输的流,jdk在设计的流的时候就假设了流只能读取一次,读取流的时候有一个指针pos、读取后指针往后移动一位,读取完后指向-1。而拦截器在controller的前一层,因此当拦截器读取流后,controller再次读就会出现读取不到的问题。(付一张deepseek回答的截图)
在这里插入图片描述

解决方法

参考https://blog.csdn.net/qqqqqqhhhhhh/article/details/118862081的文章,在过滤层拦截所有的请求,把request请求拷贝缓存一份,这样就可以重复读取了。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐