背景:
最近花了一天在处理一个生产环境问题,
客户端(发送数据):通过HTTP的GET请求,传输参数中带有“+”加号。
服务端(接收数据):“+”加号变为空格。
因为是签名数据,导致服务端验证签名不通过,算比较严重的问题。
解决问题示例(多个解决方案):
示例1(请求url的参数采用直接拼装的方式)(失败):
package com.qhfax.test;
import com.qhfax.common.util.HttpClientUtil;
public class HttpGetTest1 {
public static void main(String[] args) throws Exception {
//签名
String sign = "abcde+fghij";
//请求的服务地址
String serviceUrl = "http://localhost:8080/qhfaxWeb/httpGetTest/paramTest";//请求路径
//请求参数
String queryString = "?sign="+sign;
//URI编码处理
String getUrl = serviceUrl + queryString;
String response = HttpClientUtil.get(getUrl);
System.out.println(response);
}
}
服务端接收的的值为:abcde fghij
服务端返回:false
示例2(使用URIUtil.encodeQuery方法对请求参数进行编码)(失败)
package com.qhfax.test;
import org.apache.commons.httpclient.util.URIUtil;
import com.qhfax.common.util.HttpClientUtil;
public class HttpGetTest2 {
public static void main(String[] args) throws Exception {
//签名
String sign = "abcde+fghij";
//请求的服务地址
String serviceUrl = "http://localhost:8080/qhfaxWeb/httpGetTest/paramTest";//请求路径
//请求参数
String queryString = "?sign="+sign;
//URI编码处理
String getUrl = serviceUrl + URIUtil.encodeQuery(queryString);
String response = HttpClientUtil.get(getUrl);
System.out.println(response);
}
}
服务端接收的的值为:abcde fghij
服务端返回:false
示例3(对URL保留字符进行ASCII码转换,把“+”替换为“%2B”)(成功)
package com.qhfax.test;
import com.qhfax.common.util.HttpClientUtil;
public class HttpGetTest3 {
public static void main(String[] args) throws Exception {
//签名
String sign = "abcde+fghij";
//把“+”替换为“%2B”
sign = sign.replaceAll("\\+", "%2B");
//请求的服务地址
String serviceUrl = "http://localhost:8080/qhfaxWeb/httpGetTest/paramTest";//请求路径
//请求参数
String queryString = "?sign="+sign;
//URI编码处理
String getUrl = serviceUrl + queryString;
String response = HttpClientUtil.get(getUrl);
System.out.println(response);
}
}
服务端接收的的值为:abcde+fghij
服务端返回:true
示例4(基于示例3,使用java自带的URLEncoder.encode方法对参数进行编码)(成功)
package com.qhfax.test;
import com.qhfax.common.util.HttpClientUtil;
public class HttpGetTest4 {
public static void main(String[] args) throws Exception {
//签名
String sign = "abcde+fghij";
//使用java自带的URLEncoder.encode方法对参数进行编码
sign = java.net.URLEncoder.encode(sign);
//请求的服务地址
String serviceUrl = "http://localhost:8080/qhfaxWeb/httpGetTest/paramTest";//请求路径
//请求参数
String queryString = "?sign="+sign;
//URI编码处理
String getUrl = serviceUrl + queryString;
String response = HttpClientUtil.get(getUrl);
System.out.println(response);
}
}
服务端接收的的值为:abcde+fghij
服务端返回:true
示例5(基于示例3、4,使用httpClient包中的UrlEncodedFormEntity类,详见上面的HttpClientUtil工具类)(最终版本)(成功)
package com.qhfax.test; import java.util.HashMap; import java.util.Map; import com.qhfax.common.util.HttpClientUtil; public class HttpGetTest5 { public static void main(String[] args) throws Exception { //签名 String sign = "abcde+fghij"; //请求的服务地址 String serviceUrl = "http://localhost:8080/qhfaxWeb/httpGetTest/paramTest";//请求路径 //请求参数 Mapparams = new HashMap (); params.put("sign", sign); String response = HttpClientUtil.get(serviceUrl, params); System.out.println(response); } }
服务端接收的的值为:abcde+fghij
服务端返回:true
示例6(换一种方式,也是Httpclientjar包中的类,使用Request.Get)(成功)
package com.qhfax.test;
import org.apache.http.client.fluent.Request;
import org.apache.http.client.utils.URIBuilder;
public class HttpGetTest6 {
public static void main(String[] args) throws Exception {
//签名
String sign = "abcde+fghij";
//请求的服务地址
String serviceUrl = "http://localhost:8080/qhfaxWeb/httpGetTest/paramTest";//请求路径
String result = Request.Get(
new URIBuilder(serviceUrl)
.addParameter("sign", sign)
.build())
.connectTimeout(5000)
.socketTimeout(5000).execute()
.returnContent().asString();
System.out.println(result);
}
}
服务端接收的的值为:abcde+fghij
服务端返回:true
总结:
1、找到问题:
根据示例1中问题,HTTP的GET请求URL中有包括“! * ' ( ) ; : @ & =+ $ , / ? # [ ]”的保留字符,需要对它们进行转码,这是解决问题思路的第一步,找到原因。
2、验证解决方案:
根据示例3中的处理方式,将把“+”替换为“%2B”,验证处理方法是正确的。这是解决问题思路的第二步,尝试方案,验证能否解决问题。
3、完善解决方案:
示例4、5、6是基于示例3的完善,利用现有的工具类,更好更全面的处理问题。这是解决问题思路的第三步,完善解决方案。
(备注:示例2中的URIUtil.encodeQuery是有编码效果的,只是好像对“+”不会编码,这个点在我处理时有点误导了我的思路)
演示准备前需准备的代码(注:SpringMVC框架环境需自己搭建,httpclient的jar版本为4.4.1):
客户端:
HttpClientUtil工具类代码如下:
package com.qhfax.common.util; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.SocketTimeoutException; import java.nio.charset.Charset; import java.security.GeneralSecurityException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.http.Consts; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.ParseException; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.config.RequestConfig.Builder; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ConnectTimeoutException; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.SSLContextBuilder; import org.apache.http.conn.ssl.TrustStrategy; import org.apache.http.conn.ssl.X509HostnameVerifier; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.HTTP; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @Description: 使用httpclient4.0以上组件 * @author : huangaming * @date : 2017年4月6日 上午11:49:48 */ @SuppressWarnings("deprecation") public class HttpClientUtil { private static Logger logger = LoggerFactory.getLogger(HttpClientUtil.class); public static String postJsonString(String uri, String jsonStr) { String result = ""; Charset charset = Charset.forName("UTF-8"); // 实例化http客户端 HttpClient httpClient = HttpClientBuilder.create().build(); HttpPost post = null; try { post = new HttpPost(uri); StringEntity stringEntity = new StringEntity(jsonStr, ContentType.create("application/json", charset)); // 实例化post提交方式 post.addHeader(HTTP.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString()); // 将参数加入post请求体中 post.setEntity(stringEntity); // 执行post请求并得到返回对象 [ 到这一步我们的请求就开始了 ] HttpResponse resp = httpClient.execute(post); // 解析返回请求结果 HttpEntity entity = resp.getEntity(); result = IOUtils.toString(entity.getContent(), charset); logger.info("[postJsonString response:{}]", result); // 输出结果 } catch (Exception exception) { logger.error("postJsonString exception", exception); } finally { if (post != null) { post.releaseConnection(); } } return result; } public static final int connTimeout=10000;//连接超时参数 public static final int readTimeout=10000;//读取超时参数 public static final String charset="UTF-8";//字符编码 private static HttpClient client = null; static { PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); cm.setMaxTotal(128); cm.setDefaultMaxPerRoute(128); client = HttpClients.custom().setConnectionManager(cm).build(); } public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception{ return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout); } public static String postParameters(String url, String parameterStr,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception{ return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout); } public static String postParameters(String url, Mapparams) throws ConnectTimeoutException, SocketTimeoutException, Exception { return postForm(url, params, null, connTimeout, readTimeout); } public static String postParameters(String url, Map params, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception { return postForm(url, params, null, connTimeout, readTimeout); } public static String get(String url) throws Exception { return get(url, charset, connTimeout, readTimeout); } public static String get(String url, String charset) throws Exception { return get(url, charset, connTimeout, readTimeout); } public static String get(String url, Map params) throws Exception { return get(url, params, charset, connTimeout, readTimeout); } /** * 发送一个 Post 请求, 使用指定的字符集编码. * * @param url * @param body RequestBody * @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3 * @param charset 编码 * @param connTimeout 建立链接超时时间,毫秒. * @param readTimeout 响应超时时间,毫秒. * @return ResponseBody, 使用指定的字符集编码. * @throws ConnectTimeoutException 建立链接超时异常 * @throws SocketTimeoutException 响应超时 * @throws Exception */ public static String post(String url, String body, String mimeType,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception { HttpClient client = null; HttpPost post = new HttpPost(url); String result = ""; try { if (StringUtils.isNotBlank(body)) { HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset)); post.setEntity(entity); } // 设置参数 Builder customReqConf = RequestConfig.custom(); if (connTimeout != null) { customReqConf.setConnectTimeout(connTimeout); } if (readTimeout != null) { customReqConf.setSocketTimeout(readTimeout); } post.setConfig(customReqConf.build()); HttpResponse res; if (url.startsWith("https")) { // 执行 Https 请求. client = createSSLInsecureClient(); res = client.execute(post); } else { // 执行 Http 请求. client = HttpClientUtil.client; res = client.execute(post); } result = IOUtils.toString(res.getEntity().getContent(), charset); } finally { post.releaseConnection(); if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) { ((CloseableHttpClient) client).close(); } } return result; } /** * 提交form表单 * * @param url * @param params * @param connTimeout * @param readTimeout * @return * @throws ConnectTimeoutException * @throws SocketTimeoutException * @throws Exception */ public static String postForm(String url, Map params, Map headers, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception { HttpClient client = null; HttpPost post = new HttpPost(url); try { if (params != null && !params.isEmpty()) { List formParams = new ArrayList (); Set > entrySet = params.entrySet(); for (Entry entry : entrySet) { formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); } UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8); post.setEntity(entity); } if (headers != null && !headers.isEmpty()) { for (Entry entry : headers.entrySet()) { post.addHeader(entry.getKey(), entry.getValue()); } } // 设置参数 Builder customReqConf = RequestConfig.custom(); if (connTimeout != null) { customReqConf.setConnectTimeout(connTimeout); } if (readTimeout != null) { customReqConf.setSocketTimeout(readTimeout); } post.setConfig(customReqConf.build()); HttpResponse res = null; if (url.startsWith("https")) { // 执行 Https 请求. client = createSSLInsecureClient(); res = client.execute(post); } else { // 执行 Http 请求. client = HttpClientUtil.client; res = client.execute(post); } return IOUtils.toString(res.getEntity().getContent(), "UTF-8"); } finally { post.releaseConnection(); if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) { ((CloseableHttpClient) client).close(); } } } /** * 发送一个 GET 请求 * * @param url * @param charset * @param connTimeout 建立链接超时时间,毫秒. * @param readTimeout 响应超时时间,毫秒. * @return * @throws ConnectTimeoutException 建立链接超时 * @throws SocketTimeoutException 响应超时 * @throws Exception */ public static String get(String url, String charset, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,SocketTimeoutException, Exception { HttpClient client = null; HttpGet get = new HttpGet(url); String result = ""; try { // 设置参数 Builder customReqConf = RequestConfig.custom(); if (connTimeout != null) { customReqConf.setConnectTimeout(connTimeout); } if (readTimeout != null) { customReqConf.setSocketTimeout(readTimeout); } get.setConfig(customReqConf.build()); HttpResponse res = null; if (url.startsWith("https")) { logger.info("httpClientUtil|get|执行https的get请求|开始"); // 执行 Https 请求. client = createSSLInsecureClient(); res = client.execute(get); logger.info("httpClientUtil|get|执行https的get请求|结束"); } else { logger.info("httpClientUtil|get|执行http的get请求|开始"); // 执行 Http 请求. client = HttpClientUtil.client; res = client.execute(get); logger.info("httpClientUtil|get|执行http的get请求|结束"); } result = IOUtils.toString(res.getEntity().getContent(), charset); } finally { get.releaseConnection(); if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) { ((CloseableHttpClient) client).close(); } } return result; } /** * 从 response 里获取 charset * * @param ressponse * @return */ @SuppressWarnings("unused") private static String getCharsetFromResponse(HttpResponse ressponse) { // Content-Type:text/html; charset=GBK if (ressponse.getEntity() != null && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) { String contentType = ressponse.getEntity().getContentType().getValue(); if (contentType.contains("charset=")) { return contentType.substring(contentType.indexOf("charset=") + 8); } } return null; } /** * 创建 SSL连接 * @return * @throws GeneralSecurityException */ private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException { try { SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() { public boolean isTrusted(X509Certificate[] chain,String authType) throws CertificateException { return true; } }).build(); SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() { @Override public boolean verify(String arg0, SSLSession arg1) { return true; } @Override public void verify(String host, SSLSocket ssl) throws IOException { } @Override public void verify(String host, X509Certificate cert) throws SSLException { } @Override public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException { } }); return HttpClients.custom().setSSLSocketFactory(sslsf).build(); } catch (GeneralSecurityException e) { throw e; } } /** * @param url http://taobao.com/test.action * @param params 参数,编码之前的参数 * @return * @throws IOException * @throws UnsupportedEncodingException * @throws ParseException * @throws GeneralSecurityException */ public static String get(String url, Map params,String charset, Integer connTimeout,Integer readTimeout) throws ParseException, UnsupportedEncodingException, IOException, GeneralSecurityException { HttpClient client = null; if(StringUtils.isBlank(url)){ return null; } if(params != null && !params.isEmpty()){ List pairs = new ArrayList (params.size()); for(Map.Entry entry : params.entrySet()){ String value = entry.getValue(); if(value != null){ pairs.add(new BasicNameValuePair(entry.getKey(),value)); } } url += "?" + EntityUtils.toString(new UrlEncodedFormEntity(pairs, charset)); } HttpGet httpget = new HttpGet(url); CloseableHttpResponse response = null; // 设置参数 Builder customReqConf = RequestConfig.custom(); if (connTimeout != null) { customReqConf.setConnectTimeout(connTimeout); } if (readTimeout != null) { customReqConf.setSocketTimeout(readTimeout); } httpget.setConfig(customReqConf.build()); if (url.startsWith("https")) { logger.info("httpClientUtil|get|执行https的get请求|开始"); // 执行 Https 请求. client = createSSLInsecureClient(); response = (CloseableHttpResponse) client.execute(httpget); logger.info("httpClientUtil|get|执行https的get请求|结束"); } else { logger.info("httpClientUtil|get|执行http的get请求|开始"); // 执行 Http 请求. client = HttpClientUtil.client; response = (CloseableHttpResponse) client.execute(httpget); logger.info("httpClientUtil|get|执行http的get请求|结束"); } int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != 200) { httpget.abort(); throw new RuntimeException("HttpClient,error status code :" + statusCode); } HttpEntity entity = response.getEntity(); String result = null; if (entity != null) { result = EntityUtils.toString(entity, "utf-8"); } EntityUtils.consume(entity); response.close(); return result; } }
服务端
HttpGetTestController代码如下:
package com.qhfax.controller.test; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; /** * * HttpGet测试Controller * * @author zhanghaitao * */ @Controller @RequestMapping(value = "/httpGetTest") public class HttpGetTestController { /** * 参数测试 * * @param request 请求 * @param session 会话 * @return */ @RequestMapping(value = "/paramTest", method = RequestMethod.GET) @ResponseBody public String paramTest(HttpServletRequest request, HttpSession session) { //获取签名 String sign = request.getParameter("sign"); System.out.println(sign); //原签名 String oldSign = "abcde+fghij"; //验证服务端接收到的签名是否与客户端一致 boolean isEqual = oldSign.equals(sign); return isEqual+""; } }