调试钉钉小程序请求的坑-@RequestBody处理form提交数据

问题背景

​ 接口项目本身约定数据都是json格式,使用@RequestBody接收

@RestController
@RequestMapping("/app")
@Transactional(rollbackFor=Exception.class)
public class AppAction {
   
	@PostMapping("/login")
	public LoginResp login(@RequestBody @Valid LoginReq req)throws Exception{
		throw new LogicException(Code.APP_VERSION_LOWER.getCode(),Code
				.APP_VERSION_LOWER.getMessage());
	}
}

​ 但是请求安全需要,请求和响应数据加密解密处理,也就是在http发送post请求的时候,服务端收到的本身是密文,需要解密就才是 json

ShO7s5vmTis0Yp4OmnqHZL+pIP9YkHZ4hw/7XfSK2QAKIY0kWsGMlNG92RVQLf079/D7iFsCNf5yilklH+BhJQo6J7iuEaxvZFQfp+vPW3A=

​ 因此使用RequestBodyAdviceResponseBodyAdvice里面做了解密 和 加密处理,

​ 最近在接钉钉小程序出现了问题,因为钉钉在发送请求的是时候,会校验content-type,因为本身发的密文,不是json 格式,所以content-type=application/json;的时候,发送密文传是失败的,因此,只能按照content-type=application/x-www-form-urlencoded发送,

​ 报错如下:

org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported

​ 服务端肯定失败,因为只会处理 json

解决思路:

​ 既然,发送的请求类型不对,那么我在服务端处理之前包装下requet,让它变成json格式,也就是问题变成,如果包装requst ,把content-type 变成application/json

  • 拦截器

  • Aop

  • 过滤器

    ​ 经过测试,最后选择使用过滤器,用HttpServletRequestWrapper包装request 后往后传递处理。

新建过滤器

@Component
@WebFilter(urlPatterns = "/app/**")
public class MyRequestFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletRequest wrapperRequest = null;

        if (request instanceof HttpServletRequest) {
            wrapperRequest = new MyRequestWrapper((HttpServletRequest) request);
        }

        chain.doFilter(wrapperRequest, response);
    }

    class MyRequestWrapper extends HttpServletRequestWrapper {
        
        public MyRequestWrapper(HttpServletRequest request) {
            super(request);
        }

        @Override
        public Enumeration<String> getHeaders(String name) {
            if (null != name && name.equals("content-type")) {
                return new Enumeration<String>() {
                    private boolean hasGetted = false;

                    @Override
                    public String nextElement() {
                        if (hasGetted) {
                            throw new NoSuchElementException();
                        } else {
                            hasGetted = true;
                            return MediaType.APPLICATION_JSON_VALUE;
                       }
                    }

                    @Override
                    public boolean hasMoreElements() {
                        return !hasGetted;
                    }
                };

            }
            return super.getHeaders(name);
        }
    }

}

​ 调试发现,还是不行

调试分析

调用链

RequestMappingHandlerAdapter#invokeHandlerMethod

ServletInvocableHandlerMethod#invokeAndHandle

InvocableHandlerMethod#invokeForRequest

InvocableHandlerMethod#getMethodArgumentValues

HandlerMethodArgumentResolverComposite#resolveArgument

RequestResponseBodyMethodProcessor#resolveArgument

RequestResponseBodyMethodProcessor#readWithMessageConverters

AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters

调试分析

@SuppressWarnings("unchecked")
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, 				MethodParameter parameter,Type targetType) 
    throws IOException, HttpMediaTypeNotSupportedException,         								HttpMessageNotReadableException {

    MediaType contentType;
    boolean noContentType = false;
    try {
        // 这里的确是从 header 里面获取的,我们包装类在就是在这里修改了content-type
        contentType = inputMessage.getHeaders().getContentType();
    }
    catch (InvalidMediaTypeException ex) {
        throw new HttpMediaTypeNotSupportedException(ex.getMessage());
    }
    if (contentType == null) {
        noContentType = true;
        // 没有conent-type, 默认是 字节流处理  application/octet-stream
        contentType = MediaType.APPLICATION_OCTET_STREAM;
    }

    ......
    HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
    Object body = NO_VALUE;

    EmptyBodyCheckingHttpInputMessage message;
    try {
        // 这里是关键,待会分析
        message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
		// 下面就是 各种 HttpMessageConverter 调用,什么json string 都是在这里处理
        // json ---MappingJackson2HttpMessageConverter
        // 自定义的 faston ---- FastonHttpMessageConverter
        // string ----StringHttpMessageConverter
        // form ----FormHttpMessageConverter
        for (HttpMessageConverter<?> converter : this.messageConverters) {
            Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
            GenericHttpMessageConverter<?> genericConverter =
                (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
            if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
                (targetClass != null && converter.canRead(targetClass, contentType))) {
                // 处理各种  RequestBodyAdvice  
                if (message.hasBody()) {
                    // 前置处理  beforeBodyRead
                    HttpInputMessage msgToUse =
                        getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
                    body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
                            ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
                    // 后置处理  afterBodyRead
                    body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
                }
                else {
                    body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
                }
                break;
            }
        }
    }
    catch (IOException ex) {
        throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
    }

    // body == null 经常报错  Request body  is missing 就是在这里了
    if (body == NO_VALUE) {
        if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
            (noContentType && !message.hasBody())) {
            return null;
        }
        // 开始的那个报错 Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported  就是这里抛出的
        throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
    }

    // 以上步骤都对
    MediaType selectedContentType = contentType;
    Object theBody = body;
    // 日志输出 请求封装结果
    LogFormatUtils.traceDebug(logger, traceOn -> {
        String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
      //Read "application/json;charset=UTF-8" to [LoginReq{userName='dura-sale-1', password='96e79218965eb72c92a549dd5a330112', lon=null, lat=null, ci (truncated)...]
        return "Read \"" + selectedContentType + "\" to [" + formatted + "]";
    });

    return body;
}

​ 继续分析EmptyBodyCheckingHttpInputMessage

// 把HttpInputMessage 包装为  EmptyBodyCheckingHttpInputMessage
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);

​ 所以,其实,各种请求的信息,还是从HttpInputMessage里面获取的

​ 查看构造函数

public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
    this.headers = inputMessage.getHeaders();
    // 这里很关键,直接去获取请求的 inputStream了
    InputStream inputStream = inputMessage.getBody();
    ......
}

​ 继续查看getBody

//ServletServerHttpRequest#getBody
@Override
public InputStream getBody() throws IOException {
    // 判断是不是 form 表单,
    //也就是content-type=application/x-www-form-urlencoded;charset=UTF-8
    if (isFormPost(this.servletRequest)) {
        // 是的话,获取的 request.getParameterMaps()
        return getBodyFromServletRequestParameters(this.servletRequest);
    }
    else {
        // 不是的话,获取 request.getInputStream();
        return this.servletRequest.getInputStream();
    }
}

​ 但是很坑的是 isFormPost的判断

//ServletServerHttpRequest#isFormPost
private static boolean isFormPost(HttpServletRequest request) {
    // 直接获取的是 request.getContentType(),而不是 request.getHeader("content-type")
    String contentType = request.getContentType();
    return (contentType != null && contentType.contains(FORM_CONTENT_TYPE) &&
            HttpMethod.POST.matches(request.getMethod()));
}

​ 这就很坑了啊,说明,我们在HttpServletRequestWrapper修改的header里面的conent-type没起到作用啊 ,所以肯定走到逻辑request.getInputStream();去处理了

​ 这里的处理,后续也可以,但是仍然有个很坑的问题,参数处理中直接把特殊字符转义了,导致解密逻辑失败

​ 比如密文是:

ShO7s5vmTis0Yp4OmnqHZL+pIP9YkHZ4hw/7XfSK2QAKIY0kWsGMlNG92RVQLf079/D7iFsCNf5yilklH+BhJQo6J7iuEaxvZFQfp+vPW3A=
//被转义的密文
ShO7s5vmTis0Yp4OmnqHZL+pIP9YkHZ4hw%2F7XfSK2QAKIY0kWsGMlNG92RVQLf079%2FD7iFsCNf5yilklH+BhJQo6J7iuEaxvZFQfp+vPW3A=

​ 这还是只是/被转义了,其他没有用到的,还不知道多少坑呢,所以换个角度处理

​ 上面分析,既然判断逻辑是request.getContentType(),所以,覆盖一下嘛,修改MyRequestWrapper,添加如下:

public String getContentType() {
    return MediaType.APPLICATION_JSON_UTF8_VALUE;
}

​ 继续调试,报错如下:

org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing:

​ 为什么呢,因为刚才判断isFormPost=false,是从inputStream里面获取输入流,所以,也要覆写一下,继续修改,添加如下:

@Override
public ServletInputStream getInputStream() throws IOException {
    return getRequest().getInputStream();
}

​ 但是,还是提示 Required request body is missing,继续调试分析

​ 奇怪,getBody()返回的是inputStream,但是上面已经添加了getInputStream(),到底几个意思啊,也就是说,我们getRequest().getInputStream();返回的输入流不对?

public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
    this.headers = inputMessage.getHeaders();
    InputStream inputStream = inputMessage.getBody();
    // 输入流,是否支持?
    if (inputStream.markSupported()) {
        inputStream.mark(1);
        this.body = (inputStream.read() != -1 ? inputStream : null);
        inputStream.reset();
    }
    else {
        // 走到这个逻辑了
        PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
        int b = pushbackInputStream.read();
        if (b == -1) {
            // 所以 报错 Required request body is missing
            this.body = null;
        }
        else {
            this.body = pushbackInputStream;
            pushbackInputStream.unread(b);
        }
    }
}

​ 最开始的的cotent-type=application/x-www-form-urlencoded;charset=UTF-8,属于表单提交数据,所以是用request.getParamMaps()去获取参数处理,所以request.getInputStream.markSupported()=false,而我们只是强制修改了content-type=application-json;,可以去获取request.getInputStream(),但是确是不支持的,也就是不可read

​ 思考,在包装类MyRequestWrapper里面,自己获取一下内容,然后转换为inputStream返回,继续修改,完整代码如下:

class MyRequestWrapper extends HttpServletRequestWrapper {

    private String body;

    public MyRequestWrapper(HttpServletRequest request) {
        super(request);
        try {
            String encrypt = IOUtils.toString(request.getInputStream());
            log.debug("请求密文:" + encrypt);
            this.body = encrypt;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public Enumeration<String> getHeaders(String name) {
        if (null != name && name.equals("content-type")) {
            return new Enumeration<String>() {
                private boolean hasGetted = false;

                @Override
                public String nextElement() {
                    if (hasGetted) {
                        throw new NoSuchElementException();
                    } else {
                        hasGetted = true;
                        return MediaType.APPLICATION_JSON_VALUE;
                    }
                }
                @Override
                public boolean hasMoreElements() {
                    return !hasGetted;
                }
            };

        }
        return super.getHeaders(name);
    }

    public String getContentType() {
        return MediaType.APPLICATION_JSON_UTF8_VALUE;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        InputStream inputStream = IOUtils.toInputStream(this.getBody());
        ServletInputStream servletInputStream = new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

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

            @Override
            public void setReadListener(ReadListener readListener) {
            }

            @Override
            public int read() throws IOException {
                return inputStream.read();
            }
        };
        return servletInputStream;

    }

    public String getBody() {
        return this.body;
    }

}

​ 再次调试,果然可以了,

​ 另外,虽然判断isFormPost的逻辑是使用request.getContentType来判断,但是判断支持的content-ype类型,却的确是从header里面获取判断的,略坑,所以我们最初包装类里面设置header的代码还必须保留,不然报错

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值