记录HttpServletRequestWrapper处理multipart/form-data类型包含参数和文件的问题

 项目中的请求需要对参数进行加密传输,设计中使用filter对request进行拦截,并解析request

原本的逻辑是:

  1. 客户端请求:如果参数加密,则在请求头中添加isEncrypt = true,将body的json加密之后用参数encrypt_data接收传递
  2. 服务端:略

问题出在客户端请求到达服务器后,在filter中对request进行处理,原本的逻辑是,从request中获取到url,过滤一遍,不需要对参数解密的放行,在获取request的header中的isEncrypt参数,如果未false,则不需要解密,放行,如果为true,则调用request.getParameter方法获取加密数据encrypt_data,在判断加密数据如果为空,则放行,过滤完前面不需要解密的条件之后,在对encrypt_data进行解密,放入封装的HttpServletRequestWrapper中

原本的自定义filter部分代码如下

String encrypt_data = request.getParameter("encrypt_data");
if (StrUtil.isBlank(encrypt_data)) {
    filterChain.doFilter(request, response);
} else {
    String versionId = request.getHeader("versionId");
    ParameterRequestWrapper wrapperRequest = new ParameterRequestWrapper(request, isNeedBody);
    String decrypt_data = 解密....
        ......
	//将参数添加到wrapperRequest
    String[] parameterArray = decrypt_data.split("&");
    log.info("parameterArray:" + JSONUtil.toJsonStr(parameterArray));
    for (String parameter : parameterArray) {
        String[] keyValue = parameter.split("=");
        if (keyValue.length > 1) {
            wrapperRequest.addParameter(keyValue[0], keyValue[1]);
        } else {
            wrapperRequest.addParameter(keyValue[0], "");
        }
    }
    filterChain.doFilter(wrapperRequest, response);
}

自定义的ParameterRequestWrapper如下

public class ParameterRequestWrapper extends HttpServletRequestWrapper {
    private Map<String, String[]> params = new HashMap<String, String[]>();
    private byte[] body;
    public ParameterRequestWrapper(HttpServletRequest request) {
        // 将request交给父类,以便于调用对应方法的时候,将其输出,其实父亲类的实现方式和第一种new的方式类似
        super(request);
        //将参数表,赋予给当前的Map以便于持有request中的参数
        //由于request并没有提供现成的获取json字符串的方法,所以我们需要将body中的流转为字符串
        String json = getPostStream(request);		//通过获取request的stream获取到body数据
        if (StrUtil.isNotBlank(json)) {
            this.body = getData(json).getBytes();
        }
        this.params.putAll(request.getParameterMap());
    }
    
    //添加addParameter方法
    
    //重写getParameter方法
    @Override
    public String getParameter(String name) {//重写getParameter,代表参数从当前类中的map获取
        String[] values = params.get(name);
        if (values == null || values.length == 0) {
            return null;
        }
        return values[0];
    }
    
    //重写getReader
    
    //重写getInputStream
    
    //通过保存的body字节数组转steam 和 reader返回
    
    private String getPostStream(HttpServletRequest request){
        try{
            //获取stream并处理
        }catch(IOException e){
            return null;
        }
    }
    
}

一开始负责修改这部分代码的时候,我发现getPostData方法中捕获了IOException,在获取stream的时候,其实已经出错了,但是由于代码的逻辑,正常情况下,文件上传在url过滤的时候就已经放行了,不涉及到文件的请求,exception被消费掉了之后,后续代码对参数进行解密,添加到params中,并且使用自定义的request放行,在后续的controller中也能正确的通过重写的getParameter方法获取到参数,不会有任何问题

但是我的需求是,对一些文件上传,并且涉及到参数传递的接口,修改为加密传输,我一开始看代码逻辑,只需要前端将请求头中的isEncrypt 传递true,并且将参数进行加密就可以了,后端不需要做处理,但是在联调的时候,出现了问题,文件获取不到,在排查之后

第一次定位到的问题:

  1. 在封装request之前,也就是如下代码,之前调用了requet.getParameter获取加密数据,导致了springboot消费掉了request中的stream,用于解析body,绑定参数,后面封装request的时候获取不到stream

ParameterRequestWrapper wrapperRequest = new ParameterRequestWrapper(request, isNeedBody);

调整了一下filter中的代码,线封装request,在调用封装的ParameterRequestWrapper获取加密数据

在调整了代码之后,stream被重复消费的报错没有了,但是出现了另外一个问题 ->通过wrapperRequest.getParameter获取不到参数了,也就是springboot解析不到body中的参数,查询各种资料和不断的尝试之后,我发现是因为重写了HttpServletRequestWrapper的getReader和getInputStream方法导致的,springboot处理multipart/form-data类型的body时,会通过getInputStream方法获取到stream之后,对body的每一个part进行解析,每个part都有自己的边界,并且可能包含额外的元数据,整个解析过程是非常复杂的,而在重写后的代码中并没有考虑到对part的处理

最后修改ParameterRequestWrapper类如下

public class ParameterRequestWrapper extends HttpServletRequestWrapper implements MultipartHttpServletRequest {

    private Map<String, String[]> params = new HashMap<>();

    private MultipartHttpServletRequest multReq;

    public ParameterRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        CommonsMultipartResolver resolver = new CommonsMultipartResolver();
        this.multReq = resolver.resolveMultipart(request);
        this.params.putAll(multReq.getParameterMap());
    }

    @Override
    public Map<String, MultipartFile> getFileMap() {
        return this.multReq.getFileMap();
    }

    @Override
    public MultiValueMap<String, MultipartFile> getMultiFileMap() {
        return this.multReq.getMultiFileMap();
    }

    @Override
    public String getMultipartContentType(String paramOrFileName) {
        return this.multReq.getMultipartContentType(paramOrFileName);
    }

    @Override
    public Iterator<String> getFileNames() {
        return this.multReq.getFileNames();
    }

    @Override
    public MultipartFile getFile(String name) {
        return this.multReq.getFile(name);
    }

    @Override
    public List<MultipartFile> getFiles(String name) {
        return this.multReq.getFiles(name);
    }

    @Override
    public String[] getParameterValues(String name) {
        // 如果文件参数,返回null或者空数组
        if (this.multReq.getFile(name) != null) {
            return null;
        }
        return params.get(name);
    }

    @Override
    public Enumeration<String> getParameterNames() {
        // 合并普通参数和文件参数的名称
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.putAll(super.getParameterMap());
        paramMap.putAll(getFileMap());
        return Collections.enumeration(paramMap.keySet());
    }


    @Override
    public String getParameter(String name) {//重写getParameter,代表参数从当前类中的map获取
        String[] values = params.get(name);
        if (values == null || values.length == 0) {
            return null;
        }
        return values[0];
    }


    public void addParameter(String name, Object value) {//增加参数
        if (value != null) {
            if (value instanceof String[]) {
                params.put(name, (String[]) value);
            } else if (value instanceof String) {
                params.put(name, new String[]{(String) value});
            } else {
                params.put(name, new String[]{String.valueOf(value)});
            }
        }
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return this.multReq.getReader();
    }

    /**
     * 在使用@RequestBody注解的时候,其实框架是调用了getInputStream()方法,所以我们要重写这个方法
     *
     * @return
     * @throws IOException
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        return this.multReq.getInputStream();
    }

    @Override
    public HttpMethod getRequestMethod() {
        return this.multReq.getRequestMethod();
    }

    @Override
    public HttpHeaders getRequestHeaders() {
        return this.multReq.getRequestHeaders();
    }

    @Override
    public HttpHeaders getMultipartHeaders(String paramOrFileName) {
        return this.multReq.getMultipartHeaders(paramOrFileName);
    }
}

使用Apache Commons FileUpload库手动解析multipart/form-data中的part,解析文件和参数在上述代码的构造方法中

CommonsMultipartResolver resolver = new CommonsMultipartResolver();
this.multReq = resolver.resolveMultipart(request);

resolver.resolveMultipart返回的也是一个HttpServletRequestWrapper,放到属性中,这样就解析出了body中的参数和文件,通过multReq.getParameterMap就可以获取到所有的参数,然后需要适配后续的文件操作,所以实现了MultipartHttpServletRequest接口,并重写其中对文件处理的方法(multReq中已经有文件的处理结果)

工具包依赖坐标

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.5</version> <!-- 使用实际的版本号替换 -->
</dependency>

问题解决end

(filter,intecepter->preHandler,参数解析) 执行顺序

  • 22
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
您提到了一个 `HttpServletRequestWrapper` 与 `multipart/form-data` 相关的问题。`HttpServletRequestWrapper` 是一个 Java 类,它可以用来包装原始的 `HttpServletRequest` 对象,以便在请求处理过程中对其进行自定义的操作和修改。而 `multipart/form-data` 是一种 HTTP 请求的内容类型,常用于上传文件和提交表单数据等操作。 当使用 `multipart/form-data` 类型提交请求时,通常是为了支持上传文件或者非文本类型的数据。它会将请求体中的数据分割成多个部分,每个部分包含一个表单字段或者上传的文件。在处理这种类型的请求时,可以使用 `HttpServletRequestWrapper` 类进行封装,以便在处理过程中访问和修改请求参数。 以下是一个示例代码,演示了如何使用 `HttpServletRequestWrapper` 来处理 `multipart/form-data` 类型的请求: ```java import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.Part; import java.util.Collection; public class MyHttpServletRequestWrapper extends HttpServletRequestWrapper { public MyHttpServletRequestWrapper(HttpServletRequest request) { super(request); } @Override public Collection<Part> getParts() { // 在这里可以对上传的文件进行特定操作 return super.getParts(); } @Override public Part getPart(String name) { // 在这里可以对特定的表单字段或者文件进行操作 return super.getPart(name); } } ``` 在上述示例中,我们创建了一个自定义的 `HttpServletRequestWrapper` 类 `MyHttpServletRequestWrapper`,并重写了其中的 `getParts()` 和 `getPart(String name)` 方法,以便在处理 `multipart/form-data` 请求时对上传的文件或者表单字段进行特定的操作。 请注意,这只是一个简单的示例,实际应用中可能需要根据具体需求进行更多的自定义操作。希望能对您有所帮助!如果还有其他问题,请随时提问。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值