已解决:ZuulFilter丢失请求体参数问题

前言:

        问题现象: 调用spring cloud netflix微服务时,如果请求的Content-type为"x-www-form-urlencoded"格式时,部分请求体的参数如果key与query参数重合时,该请求体参数会丢失.

        问题分析: 当x-www-form-urlencoded格式的表单提交的请求体参数的key存在和query参数重合时,请求体参数会被zuul自动合并到一个request的一个隐藏的query参数中,并删除请求体相应参数,导致请求体参数丢失.

        解决方式:创建自定义ZuulFilter子类,自动解析被隐藏的请求体参数并重新写入请求体中,问题解决。

自定义ZuulFilter子类如下所示:

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.netflix.zuul.http.HttpServletRequestWrapper;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequestWrapper;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;


@Component
public class CustomRequestFilter extends ZuulFilter {
    @Autowired
    private ApplicationContext applicationContext;

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE; // 在请求之前执行
    }

    @Override
    public int filterOrder() {
        return -1; // 过滤器的执行顺序
    }
    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        return request.getMethod().equalsIgnoreCase(HttpMethod.POST.name()) || request.getMethod().equalsIgnoreCase(HttpMethod.PUT.name());
    }
    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        try {
            //解决表单方式(application/x-www-form-urlencoded)提交参数,参数名称和query参数同名时,
            // 参数混入query参数中,而请求体中该同名参数丢失的问题
            if (request.getContentType() != null && request.getContentType().contains("application/x-www-form-urlencoded")) {
                // 读取请求体
                MultiValueMap<String, String> requestBody = readFormUrlEncoded(request.getInputStream());
                //该数据中混入了请求体的表单参数
                Map<String, String[]> parameterMap = ((ServletRequestWrapper) request).getRequest().getParameterMap();
                //query参数中存在这些已知参数,需要校验请求体是否存在这些参数
                List<String> tongtechParam = Arrays.asList("signature", "timestamp", "appkey");
                for (String paramKey : parameterMap.keySet()) {
                    if (!tongtechParam.contains(paramKey)) {
                        continue;
                    }
                    String[] values = parameterMap.get(paramKey);
                    if (values != null && values.length > 1) {
                        //实际的query参数,需要和requestBody中同名参数的值进行对比
                        String queryValue = request.getParameterMap().get(paramKey)[0];
                        //循环将业务混入param的参数回填入requestBody中
                        for (String value : values) {
                            if (StringUtils.isNotBlank(queryValue) && !StringUtils.equals(queryValue, value)) {
                                requestBody.add(paramKey, value);
                            }
                        }
                    }
                }
                // 重新设置请求体
                setRequestBody(ctx, request, requestBody);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    private MultiValueMap<String, String> readFormUrlEncoded(java.io.InputStream inputStream) throws IOException {
        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
        String line;
        StringBuilder content = new StringBuilder();
        while ((line = reader.readLine()) != null) {
            content.append(line);
        }
        String[] parts = content.toString().split("&");
        for (String part : parts) {
            String[] kv = part.split("=");
            if (kv.length == 2) {
                map.add(kv[0], kv[1]);
            }
        }
        return map;
    }

    private void setRequestBody(RequestContext ctx, HttpServletRequest request, MultiValueMap<String, String> requestBody) {
        // 创建一个新的请求体
        StringBuilder newRequestBody = new StringBuilder();
        for (String key : requestBody.keySet()) {
            for (String value : requestBody.get(key)) {
                newRequestBody.append(key).append("=").append(value).append("&");
            }
        }
        if (newRequestBody.length() > 0) {
            newRequestBody.deleteCharAt(newRequestBody.length() - 1); // 移除最后一个 &
        }

        // 设置新的请求体
        ctx.setRequest(new HttpServletRequestWrapper(request) {
            @Override
            public String getCharacterEncoding() {
                return StandardCharsets.UTF_8.name();
            }

            @Override
            public String getContentType() {
                return MediaType.APPLICATION_FORM_URLENCODED_VALUE;
            }

            @Override
            public int getContentLength() {
                return newRequestBody.toString().getBytes(StandardCharsets.UTF_8).length;
            }

            @Override
            public long getContentLengthLong() {
                return getContentLength();
            }

            @Override
            public ServletInputStream getInputStream() {
                byte[] bytes = newRequestBody.toString().getBytes(StandardCharsets.UTF_8);
                return new ByteArrayServletInputStream(bytes);
            }
        });
    }
}
import javax.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;

public class ByteArrayServletInputStream extends ServletInputStream {
    private final ByteArrayInputStream bais;

    public ByteArrayServletInputStream(byte[] byteArray) {
        this.bais = new ByteArrayInputStream(byteArray);
    }

    @Override
    public int read() {
        return bais.read();
    }

    @Override
    public boolean isFinished() {
        return bais.available() <= 0;
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void setReadListener(javax.servlet.ReadListener listener) {
        // 不需要实现
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值