1 package com.jadyer.util; 2 3 import java.io.IOException; 4 import java.net.SocketTimeoutException; 5 import java.nio.charset.Charset; 6 import java.security.cert.CertificateException; 7 import java.security.cert.X509Certificate; 8 import java.util.ArrayList; 9 import java.util.List; 10 import java.util.Map; 11 12 import javax.net.ssl.SSLContext; 13 import javax.net.ssl.SSLException; 14 import javax.net.ssl.SSLSession; 15 import javax.net.ssl.SSLSocket; 16 import javax.net.ssl.TrustManager; 17 import javax.net.ssl.X509TrustManager; 18 19 import org.apache.http.Header; 20 import org.apache.http.HttpEntity; 21 import org.apache.http.HttpResponse; 22 import org.apache.http.NameValuePair; 23 import org.apache.http.ParseException; 24 import org.apache.http.client.ClientProtocolException; 25 import org.apache.http.client.HttpClient; 26 import org.apache.http.client.entity.UrlEncodedFormEntity; 27 import org.apache.http.client.methods.HttpGet; 28 import org.apache.http.client.methods.HttpPost; 29 import org.apache.http.conn.ConnectTimeoutException; 30 import org.apache.http.conn.scheme.Scheme; 31 import org.apache.http.conn.ssl.SSLSocketFactory; 32 import org.apache.http.conn.ssl.X509HostnameVerifier; 33 import org.apache.http.entity.ContentType; 34 import org.apache.http.entity.StringEntity; 35 import org.apache.http.impl.client.DefaultHttpClient; 36 import org.apache.http.message.BasicNameValuePair; 37 import org.apache.http.params.CoreConnectionPNames; 38 import org.apache.http.protocol.HTTP; 39 import org.apache.http.util.EntityUtils; 40 41 /** 42 * 封装了采用HttpClient发送HTTP请求的方法 43 * @see 本工具所采用的是HttpComponents-Client-4.2.1 44 * @see =================================================================================================== 45 * @see 开发HTTPS应用时,时常会遇到两种情况 46 * @see 1、测试服务器没有有效的SSL证书,客户端连接时就会抛异常 47 * @see javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated 48 * @see 2、测试服务器有SSL证书,但可能由于各种不知名的原因,它还是会抛一堆烂码七糟的异常,诸如下面这两种 49 * @see javax.net.ssl.SSLException: hostname in certificate didn't match: <123.125.97.66> != <123.125.97.241> 50 * @see javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target 51 * @see =================================================================================================== 52 * @see 这里使用的是HttpComponents-Client-4.2.1创建的连接,所以就要告诉它使用一个不同的TrustManager 53 * @see 由于SSL使用的模式是X.509,对于该模式,Java有一个特定的TrustManager,称为X509TrustManager 54 * @see TrustManager是一个用于检查给定的证书是否有效的类,所以我们自己创建一个X509TrustManager实例 55 * @see 而在X509TrustManager实例中,若证书无效,那么TrustManager在它的checkXXX()方法中将抛出CertificateException 56 * @see 既然我们要接受所有的证书,那么X509TrustManager里面的方法体中不抛出异常就行了 57 * @see 然后创建一个SSLContext并使用X509TrustManager实例来初始化之 58 * @see 接着通过SSLContext创建SSLSocketFactory,最后将SSLSocketFactory注册给HttpClient就可以了 59 * @see =================================================================================================== 60 * @version v1.7 61 * @history v1.0-->新建<code>sendGetRequest()</code>和<code>sendPostRequest()</code>方法 62 * @history v1.1-->新增<code>sendPostSSLRequest()</code>方法,用于发送HTTPS的POST请求 63 * @history v1.2-->新增<code>sendPostRequest()</code>方法,用于发送HTTP协议报文体为任意字符串的POST请求 64 * @history v1.3-->新增<code>java.net.HttpURLConnection</code>实现的<code>sendPostRequestByJava()</code> 65 * @history v1.4-->所有POST方法中增加连接超时限制和读取超时限制 66 * @history v1.5-->重组各方法,并补充自动获取HTTP响应文本编码的方式,移除<code>sendPostRequestByJava()</code> 67 * @history v1.6-->整理GET和POST请求方法,使之更为适用 68 * @history v1.7-->修正<code>sendPostRequest()</code>请求的CONTENT_TYPE头信息,并优化各方法参数及内部处理细节 69 * @create Feb 1, 2012 3:02:27 PM 70 * @update Jul 23, 2013 1:18:35 PM 71 * @author 玄玉<http://blog.csdn.net/jadyer> 72 */ 73 public class HttpClientUtil { 74 private HttpClientUtil(){} 75 76 /** 77 * 发送HTTP_GET请求 78 * @see 1)该方法会自动关闭连接,释放资源 79 * @see 2)方法内设置了连接和读取超时时间,单位为毫秒,超时或发生其它异常时方法会自动返回"通信失败"字符串 80 * @see 3)请求参数含中文时,经测试可直接传入中文,HttpClient会自动编码发给Server,应用时应根据实际效果决定传入前是否转码 81 * @see 4)该方法会自动获取到响应消息头中[Content-Type:text/html; charset=GBK]的charset值作为响应报文的解码字符集 82 * @see 若响应消息头中无Content-Type属性,则会使用HttpClient内部默认的ISO-8859-1作为响应报文的解码字符集 83 * @param requestURL 请求地址(含参数) 84 * @return 远程主机响应正文 85 */ 86 public static String sendGetRequest(String reqURL){ 87 String respContent = "通信失败"; //响应内容 88 HttpClient httpClient = new DefaultHttpClient(); //创建默认的httpClient实例 89 //设置代理服务器 90 //httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, new HttpHost("10.0.0.4", 8080)); 91 httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 10000); //连接超时10s 92 httpClient.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT, 20000); //读取超时20s 93 HttpGet httpGet = new HttpGet(reqURL); //创建org.apache.http.client.methods.HttpGet 94 try{ 95 HttpResponse response = httpClient.execute(httpGet); //执行GET请求 96 HttpEntity entity = response.getEntity(); //获取响应实体 97 if(null != entity){ 98 //respCharset=EntityUtils.getContentCharSet(entity)也可以获取响应编码,但从4.1.3开始不建议使用这种方式 99 Charset respCharset = ContentType.getOrDefault(entity).getCharset(); 100 respContent = EntityUtils.toString(entity, respCharset); 101 //Consume response content 102 EntityUtils.consume(entity); 103 } 104 System.out.println("-------------------------------------------------------------------------------------------"); 105 StringBuilder respHeaderDatas = new StringBuilder(); 106 for(Header header : response.getAllHeaders()){ 107 respHeaderDatas.append(header.toString()).append(" "); 108 } 109 String respStatusLine = response.getStatusLine().toString(); //HTTP应答状态行信息 110 String respHeaderMsg = respHeaderDatas.toString().trim(); //HTTP应答报文头信息 111 String respBodyMsg = respContent; //HTTP应答报文体信息 112 System.out.println("HTTP应答完整报文=[" + respStatusLine + " " + respHeaderMsg + " " + respBodyMsg + "]"); 113 System.out.println("-------------------------------------------------------------------------------------------"); 114 } catch (ConnectTimeoutException cte){ 115 //Should catch ConnectTimeoutException, and don`t catch org.apache.http.conn.HttpHostConnectException 116 LogUtil.getLogger().error("请求通信[" + reqURL + "]时连接超时,堆栈轨迹如下", cte); 117 } catch (SocketTimeoutException ste){ 118 LogUtil.getLogger().error("请求通信[" + reqURL + "]时读取超时,堆栈轨迹如下", ste); 119 }catch(ClientProtocolException cpe){ 120 //该异常通常是协议错误导致:比如构造HttpGet对象时传入协议不对(将'http'写成'htp')or响应内容不符合HTTP协议要求等 121 LogUtil.getLogger().error("请求通信[" + reqURL + "]时协议异常,堆栈轨迹如下", cpe); 122 }catch(ParseException pe){ 123 LogUtil.getLogger().error("请求通信[" + reqURL + "]时解析异常,堆栈轨迹如下", pe); 124 }catch(IOException ioe){ 125 //该异常通常是网络原因引起的,如HTTP服务器未启动等 126 LogUtil.getLogger().error("请求通信[" + reqURL + "]时网络异常,堆栈轨迹如下", ioe); 127 }catch (Exception e){ 128 LogUtil.getLogger().error("请求通信[" + reqURL + "]时偶遇异常,堆栈轨迹如下", e); 129 }finally{ 130 //关闭连接,释放资源 131 httpClient.getConnectionManager().shutdown(); 132 } 133 return respContent; 134 } 135 136 137 /** 138 * 发送HTTP_POST请求 139 * @see 1)该方法允许自定义任何格式和内容的HTTP请求报文体 140 * @see 2)该方法会自动关闭连接,释放资源 141 * @see 3)方法内设置了连接和读取超时时间,单位为毫秒,超时或发生其它异常时方法会自动返回"通信失败"字符串 142 * @see 4)请求参数含中文等特殊字符时,可直接传入本方法,并指明其编码字符集encodeCharset参数,方法内部会自动对其转码 143 * @see 5)该方法在解码响应报文时所采用的编码,取自响应消息头中的[Content-Type:text/html; charset=GBK]的charset值 144 * @see 若响应消息头中未指定Content-Type属性,则会使用HttpClient内部默认的ISO-8859-1 145 * @param reqURL 请求地址 146 * @param reqData 请求参数,若有多个参数则应拼接为param11=value11&22=value22&33=value33的形式 147 * @param encodeCharset 编码字符集,编码请求数据时用之,此参数为必填项(不能为""或null) 148 * @return 远程主机响应正文 149 */ 150 public static String sendPostRequest(String reqURL, String reqData, String encodeCharset){ 151 String reseContent = "通信失败"; 152 HttpClient httpClient = new DefaultHttpClient(); 153 httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 10000); 154 httpClient.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT, 20000); 155 HttpPost httpPost = new HttpPost(reqURL); 156 //由于下面使用的是new StringEntity(....),所以默认发出去的请求报文头中CONTENT_TYPE值为text/plain; charset=ISO-8859-1 157 //这就有可能会导致服务端接收不到POST过去的参数,比如运行在Tomcat6.0.36中的Servlet,所以我们手工指定CONTENT_TYPE头消息 158 httpPost.setHeader(HTTP.CONTENT_TYPE, "application/x-www-form-urlencoded; charset=" + encodeCharset); 159 try{ 160 httpPost.setEntity(new StringEntity(reqData==null?"":reqData, encodeCharset)); 161 HttpResponse response = httpClient.execute(httpPost); 162 HttpEntity entity = response.getEntity(); 163 if (null != entity) { 164 reseContent = EntityUtils.toString(entity, ContentType.getOrDefault(entity).getCharset()); 165 EntityUtils.consume(entity); 166 } 167 } catch (ConnectTimeoutException cte){ 168 LogUtil.getLogger().error("请求通信[" + reqURL + "]时连接超时,堆栈轨迹如下", cte); 169 } catch (SocketTimeoutException ste){ 170 LogUtil.getLogger().error("请求通信[" + reqURL + "]时读取超时,堆栈轨迹如下", ste); 171 }catch(Exception e){ 172 LogUtil.getLogger().error("请求通信[" + reqURL + "]时偶遇异常,堆栈轨迹如下", e); 173 }finally{ 174 httpClient.getConnectionManager().shutdown(); 175 } 176 return reseContent; 177 } 178 179 180 /** 181 * 发送HTTP_POST_SSL请求 182 * @see 1)该方法会自动关闭连接,释放资源 183 * @see 2)该方法亦可处理普通的HTTP_POST请求 184 * @see 3)当处理HTTP_POST_SSL请求时,默认请求的是对方443端口,除非reqURL参数中指明了SSL端口 185 * @see 4)方法内设置了连接和读取超时时间,单位为毫秒,超时或发生其它异常时方法会自动返回"通信失败"字符串 186 * @see 5)请求参数含中文等特殊字符时,可直接传入本方法,并指明其编码字符集encodeCharset参数,方法内部会自动对其转码 187 * @see 6)方法内部会自动注册443作为SSL端口,若实际使用中reqURL指定的SSL端口非443,可自行尝试更改方法内部注册的SSL端口 188 * @see 7)该方法在解码响应报文时所采用的编码,取自响应消息头中的[Content-Type:text/html; charset=GBK]的charset值 189 * @see 若响应消息头中未指定Content-Type属性,则会使用HttpClient内部默认的ISO-8859-1 190 * @param reqURL 请求地址 191 * @param params 请求参数 192 * @param encodeCharset 编码字符集,编码请求数据时用之,当其为null时,则取HttpClient内部默认的ISO-8859-1编码请求参数 193 * @return 远程主机响应正文 194 */ 195 public static String sendPostSSLRequest(String reqURL, Map<String, String> params, String encodeCharset){ 196 String responseContent = "通信失败"; 197 HttpClient httpClient = new DefaultHttpClient(); 198 httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 10000); 199 httpClient.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT, 20000); 200 //创建TrustManager() 201 //用于解决javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated 202 X509TrustManager trustManager = new X509TrustManager(){ 203 @Override 204 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {} 205 @Override 206 public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {} 207 @Override 208 public X509Certificate[] getAcceptedIssuers() {return null;} 209 }; 210 //创建HostnameVerifier 211 //用于解决javax.net.ssl.SSLException: hostname in certificate didn't match: <123.125.97.66> != <123.125.97.241> 212 X509HostnameVerifier hostnameVerifier = new X509HostnameVerifier(){ 213 @Override 214 public void verify(String host, SSLSocket ssl) throws IOException {} 215 @Override 216 public void verify(String host, X509Certificate cert) throws SSLException {} 217 @Override 218 public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException {} 219 @Override 220 public boolean verify(String arg0, SSLSession arg1) {return true;} 221 }; 222 try { 223 //TLS1.0与SSL3.0基本上没有太大的差别,可粗略理解为TLS是SSL的继承者,但它们使用的是相同的SSLContext 224 SSLContext sslContext = SSLContext.getInstance(SSLSocketFactory.TLS); 225 //使用TrustManager来初始化该上下文,TrustManager只是被SSL的Socket所使用 226 sslContext.init(null, new TrustManager[]{trustManager}, null); 227 //创建SSLSocketFactory 228 SSLSocketFactory socketFactory = new SSLSocketFactory(sslContext, hostnameVerifier); 229 //通过SchemeRegistry将SSLSocketFactory注册到HttpClient上 230 httpClient.getConnectionManager().getSchemeRegistry().register(new Scheme("https", 443, socketFactory)); 231 //创建HttpPost 232 HttpPost httpPost = new HttpPost(reqURL); 233 //由于下面使用的是new UrlEncodedFormEntity(....),所以这里不需要手工指定CONTENT_TYPE为application/x-www-form-urlencoded 234 //因为在查看了HttpClient的源码后发现,UrlEncodedFormEntity所采用的默认CONTENT_TYPE就是application/x-www-form-urlencoded 235 //httpPost.setHeader(HTTP.CONTENT_TYPE, "application/x-www-form-urlencoded; charset=" + encodeCharset); 236 //构建POST请求的表单参数 237 if(null != params){ 238 List<NameValuePair> formParams = new ArrayList<NameValuePair>(); 239 for(Map.Entry<String,String> entry : params.entrySet()){ 240 formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); 241 } 242 httpPost.setEntity(new UrlEncodedFormEntity(formParams, encodeCharset)); 243 } 244 HttpResponse response = httpClient.execute(httpPost); 245 HttpEntity entity = response.getEntity(); 246 if (null != entity) { 247 responseContent = EntityUtils.toString(entity, ContentType.getOrDefault(entity).getCharset()); 248 EntityUtils.consume(entity); 249 } 250 } catch (ConnectTimeoutException cte){ 251 LogUtil.getLogger().error("请求通信[" + reqURL + "]时连接超时,堆栈轨迹如下", cte); 252 } catch (SocketTimeoutException ste){ 253 LogUtil.getLogger().error("请求通信[" + reqURL + "]时读取超时,堆栈轨迹如下", ste); 254 } catch (Exception e) { 255 LogUtil.getLogger().error("请求通信[" + reqURL + "]时偶遇异常,堆栈轨迹如下", e); 256 } finally { 257 httpClient.getConnectionManager().shutdown(); 258 } 259 return responseContent; 260 } 261 }
下面是测试代码
public static void main(String[] args) { Map<String, String> params = new HashMap<String, String>(); params.put("merNo", "301100100001630"); params.put("signType", "MD5"); params.put("merBindAgrNo", "00003018007000006450000013866742"); params.put("interfaceVersion", "1.0.0.0"); params.put("amount", "1000"); params.put("orderDate", "20120823"); params.put("orderNo", "UDP1208230917531231111"); params.put("merReqTime", "20120823091802"); params.put("goodsDesc", "为号码交费充值元"); params.put("goodsName", "中国联通交费充值"); params.put("userIdeMark", "3"); params.put("bankAgrMode", "9"); params.put("signMsg", "3ced24a118461043901d47815e6905a9"); System.out.println(HttpClientUtil.sendPostSSLRequest("https://123.125.97.66:8085/pay/servlet/CreditPayReqServlte", params, "UTF-8")); }