
springboot使用拦截器时遇到的坑记录
问题出现在request.getInputStream()这一行代码上,http请求的输入流为网络传输的流,jdk在设计的流的时候就假设了流只能读取一次,读取流的时候有一个指针pos、读取后指针往后移动一位,读取完后指向-1。(付一张deepseek回答的截图)项目中需要对某一个controller层的部分接口进行权限控制,如新增、删除、修改、提交、上报等接口,request请求中携带了当前的账户
背景
项目中需要对某一个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请求拷贝缓存一份,这样就可以重复读取了。
更多推荐
所有评论(0)