【问题标题】:Order of request parameters for content-type application/x-www-form-urlencoded in Spring MVCSpring MVC 中 content-type application/x-www-form-urlencoded 的请求参数顺序
【发布时间】:2013-08-23 15:07:30
【问题描述】:

我正在开发使用 application/x-www-form-urlencoded 类型请求的 Web 服务。以下是此类请求正文的示例:

loginId=tester&action=add&requestId=987654321&data=somedata

请求由客户端签名(SHA1withRSA),签名作为 HTTP 标头发送。

问题是我总是以不同的顺序获取参数,例如:

action=add&loginId=tester&requestId=987654321&data=somedata

因此验证签名总是失败。

有趣的是,只有当 Content-Typeapplication/x-www-form-urlencoded 并且我将 Content-Type 切换到 text/plain 时才会发生这种情况,然后一切正常。

我使用了不同类型的客户端(甚至使用 TCP 监视器监控流量),我确信问题不是由客户端应用引起的。

这是我的自定义消息转换器的一部分(注意我直接将传入的请求打印到控制台):

@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException,
        HttpMessageNotReadableException {

    log.debug("> readInternal - message to Object");
    InputStream inputStream = inputMessage.getBody();
    byte[] bytes = IOUtils.toByteArray(inputStream);
    String body = new String(bytes, charset);

    log.debug("Body: {}", body);
}

还有我的 Spring MVC 配置:

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="messageConverters">
        <array>
            <bean class="converter.NvpHttpMessageConverter">
                <property name="charset" value="UTF-8" />
                <property name="nvpConverter" ref="nvpConverter" />
            </bean>
        </array>
    </property>

</bean>

我相信 Spring 以某种方式检测到内容类型为 application/x-www-form-urlencoded 并使用某种预处理。我的假设正确吗?可以关掉吗?

我正在使用 Tomcat 7。

【问题讨论】:

    标签: java spring spring-mvc digital-signature


    【解决方案1】:

    您的消息转换器不适用于本机请求,而是使用 HttpInputMessage 参数。那是一个Spring类。

    inputMessage.getBody() 是您遇到的问题。默认情况下,使用 ServletServerHttpRequest(另一个 Spring 类),在其 getBody() 方法中有类似的内容:

    public InputStream getBody() throws IOException {
        if (isFormSubmittal(this.servletRequest)) {
            return getFormBody(this.servletRequest);
        }
        else {
            return this.servletRequest.getInputStream();
        }
    }
    

    它委托给这样的私有实现:

    private InputStream getFormBody(HttpServletRequest request) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Writer writer = new OutputStreamWriter(bos, FORM_CHARSET);
    
        Map<String, String[]> form = request.getParameterMap();
        for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
            String name = nameIterator.next();
            List<String> values = Arrays.asList(form.get(name));
            for (Iterator<String> valueIterator = values.iterator(); valueIterator.hasNext();) {
                String value = valueIterator.next();
                writer.write(URLEncoder.encode(name, FORM_CHARSET));
                if (value != null) {
                    writer.write('=');
                    writer.write(URLEncoder.encode(value, FORM_CHARSET));
                    if (valueIterator.hasNext()) {
                        writer.write('&');
                    }
                }
            }
            if (nameIterator.hasNext()) {
                writer.append('&');
            }
        }
        writer.flush();
    
        return new ByteArrayInputStream(bos.toByteArray());
    }
    

    这是您的问题发生的原因:

    ...
    Map<String, String[]> form = request.getParameterMap();
    ...
    

    您提到您使用的是 Tomcat 7,因此在这种情况下,request.getParameterMap() 返回一个 org.apache.catalina.util.ParameterMap,它只是一个 HashMap,它对其内容顺序没有任何具体保证。因此,遍历参数并重新组合请求正文会打乱参数的原始顺序。

    Spring 是一个灵活的框架,您可能会尝试对它做点什么,但您会修复结果,而不是原因。原因是您的签名方法很脆弱

    例如,这些会导致不同的签名吗?

    aaa=1&bbb=2
    bbb=2&aaa=1
    

    还是这些?

    aaa=Hello+world
    aaa=Hello%20world
    

    服务器通常不关心参数的顺序,并且编码值的不同方式最终会得到相同的解码值。因此,在签署某项内容时,您通常会先执行规范化。你可以从URL normalizationavailable APIs like Twitter's 那里借一些想法。

    您会注意到,在签名之前对参数进行排序是一个重要的步骤,它将消除您当前的问题。

    【讨论】:

    • +1。请注意,PayPal 在他们的 IPN 通知中犯了同样的错误:有一个签名步骤假定了一个固定的参数顺序。 OP 并不孤单。
    【解决方案2】:

    这是我解决问题的方法:

    我已扩展 AnnotationMethodHandlerAdapter 并覆盖 AnnotationMethodHandlerAdapter#createHttpInputMessage

    public class MyCustomAnnotationMethodHandlerAdapter extends AnnotationMethodHandlerAdapter {
    
        @Override
        protected HttpInputMessage createHttpInputMessage(HttpServletRequest servletRequest) throws Exception {
            return new ServletServerHttpRequest(servletRequest) {
    
                @Override
                public InputStream getBody() throws IOException {
                    return getServletRequest().getInputStream();
                }
            };
        }
    }
    

    那么你只需要注册你的自定义 AnnotationMethodHandlerAdapter 而不是原来的:

    <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
    
    <bean class="webservice.handler.MyCustomAnnotationMethodHandlerAdapter">
        <property name="messageConverters">
            <array>
                <bean class="converter.NvpHttpMessageConverter">
                    <property name="charset" value="UTF-8" />
                    <property name="nvpConverter" ref="nvpConverter" />
                </bean>
            </array>
        </property>
    
    </bean>
    

    【讨论】:

      猜你喜欢
      • 2019-06-14
      • 2019-02-04
      • 1970-01-01
      • 1970-01-01
      • 2013-11-10
      • 1970-01-01
      • 2011-09-12
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多