我正在处理使用application/x-www-form-urlencoded
类型请求的网络服务。以下是此类请求正文的示例:
loginId=tester&action=add&requestId=987654321&data=somedata
请求已由客户端签名(使用SHA1withRSA
),签名将作为HTTP标头发送。
问题是我总是以不同的顺序获取参数,例如:
action=add&loginId=tester&requestId=987654321&data=somedata
因此验证签名总是失败。
有趣的是,仅当Content-Type
为application/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。
答案 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>