Spring MVC中内容类型application / x-www-form-urlencoded的请求参数顺序

时间:2013-08-23 15:07:30

标签: java spring spring-mvc digital-signature

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

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。

2 个答案:

答案 0 :(得分: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 normalization或某些available APIs like Twitter's中的一些想法。

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

答案 1 :(得分:0)

以下是我如何解决这个问题:

我已延长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>