池化 + SSL 的 RestTemplate :
public RestTemplate init() throws Exception{
SSLConnectionSocketFactory connectionSocketFactory = initSSL(jks_path, jks_pwd, jks_path, jks_pwd);
PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager(
RegistryBuilder.<ConnectionSocketFactory>create().register("https", connectionSocketFactory)
.build());
int availableProcessors = Runtime.getRuntime().availableProcessors();
poolingConnectionManager.setMaxTotal(2 * availableProcessors + 3); // 连接池最大连接数
poolingConnectionManager.setDefaultMaxPerRoute(2 * availableProcessors); // 每个主机的并发
CloseableHttpClient httpclient = HttpClients.custom().setConnectionManager(poolingConnectionManager)
.disableAutomaticRetries().build();
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(
httpclient);
clientHttpRequestFactory.setReadTimeout(anxinsignConfig.getReadTimeout());
clientHttpRequestFactory.setConnectionRequestTimeout(anxinsignConfig.getConnectionRequestTimeout());
clientHttpRequestFactory.setConnectTimeout(anxinsignConfig.getConnectTimeout());
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
StringHttpMessageConverter httpMessageConverter = new StringHttpMessageConverter();
List<MediaType> list = Lists.newArrayList();
list.addAll(httpMessageConverter.getSupportedMediaTypes());
list.add(MediaType.TEXT_PLAIN);
httpMessageConverter.setSupportedMediaTypes(httpMessageConverter.getSupportedMediaTypes());
restTemplate.getMessageConverters().add(httpMessageConverter);
return restTemplate;
}
/**
* SSL的证书配置
*
* @param keyStorePath 证书地址
* @param keyStorePassword 证书密码
* @param trustStorePath 可以与keyStorePath相同
* @param trustStorePassword 可以与keyStorePassword相同
*/
public SSLConnectionSocketFactory initSSL(String keyStorePath, char[] keyStorePassword, String trustStorePath,
char[] trustStorePassword) throws Exception {
KeyManagerFactory keyManagerFactory = null;
KeyStore keyStore = null;
if (CommonUtil.isEmpty(sslConfig.keyProvider)) {
keyManagerFactory = KeyManagerFactory.getInstance(sslConfig.keyAlgorithm);
if (CommonUtil.isNotEmpty(sslConfig.keyStoreType)) {
keyStore = KeyStore.getInstance(sslConfig.keyStoreType);
}
} else {
keyManagerFactory = KeyManagerFactory.getInstance(sslConfig.keyAlgorithm, sslConfig.keyProvider);
if (CommonUtil.isNotEmpty(sslConfig.keyStoreType)) {
keyStore = KeyStore.getInstance(sslConfig.keyStoreType, sslConfig.keyProvider);
}
}
if (CommonUtil.isEmpty(keyStorePath)) {
keyManagerFactory.init(keyStore, keyStorePassword);
} else {
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(keyStorePath);
keyStore.load(fileInputStream, keyStorePassword);
keyManagerFactory.init(keyStore, keyStorePassword);
} finally {
if (fileInputStream != null) {
fileInputStream.close();
}
}
}
TrustManagerFactory trustManagerFactory = null;
KeyStore trustStore = null;
if (CommonUtil.isEmpty(sslConfig.trustProvider)) {
trustManagerFactory = TrustManagerFactory.getInstance(sslConfig.trustAlgorithm);
if (CommonUtil.isNotEmpty(sslConfig.trustStoreType)) {
trustStore = KeyStore.getInstance(sslConfig.trustStoreType);
}
} else {
trustManagerFactory = TrustManagerFactory.getInstance(sslConfig.trustAlgorithm, sslConfig.trustProvider);
if (CommonUtil.isNotEmpty(sslConfig.trustStoreType)) {
trustStore = KeyStore.getInstance(sslConfig.trustStoreType, sslConfig.trustProvider);
}
}
if (CommonUtil.isEmpty(trustStorePath)) {
trustManagerFactory.init(trustStore);
} else {
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(trustStorePath);
trustStore.load(fileInputStream, trustStorePassword);
trustManagerFactory.init(trustStore);
fileInputStream.close();
} finally {
if (fileInputStream != null) {
fileInputStream.close();
}
}
}
SSLContext sslContext = null;
if (CommonUtil.isEmpty(sslConfig.sslProvider)) {
sslContext = SSLContext.getInstance(sslConfig.sslProtocol);
} else {
sslContext = SSLContext.getInstance(sslConfig.sslProtocol, sslConfig.sslProvider);
}
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
return new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
}
public static class SSLConfig {
public String sslProvider = null;
public String sslProtocol = "TLSv1.1";
public String keyProvider = null;
public String keyAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
public String keyStoreType = KeyStore.getDefaultType();
public String trustProvider = null;
public String trustAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
public String trustStoreType = KeyStore.getDefaultType();
public boolean ignoreHostname = true;
}
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.11</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.8</version>
</dependency>
踩坑
HttpClient构建注入SSLSocketFactory无效
org.apache.http.impl.client.HttpClients.custom().build() -> org.apache.http.impl.client.HttpClientBuilder.build() 时注意: 对于org.apache.http.conn.ssl.SSLConnectionSocketFactory 如下图中的设置方式是失效的。
private SSLConnectionSocketFactory connectionSocketFactory;
PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager();
int availableProcessors = Runtime.getRuntime().availableProcessors();
poolingConnectionManager.setMaxTotal(2 * availableProcessors + 3); // 连接池最大连接数
poolingConnectionManager.setDefaultMaxPerRoute(2 * availableProcessors); // 每个主机的并发
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(connectionSocketFactory).setConnectionManager(poolingConnectionManager).disableAutomaticRetries().build();
优先使用外部设置的HttpClientConnectionManager配置。 否则内部初始化新的PoolingHttpClientConnectionManager对象,这才会去使用外部设置的SSLSocketFactory(setSSLSocketFactory方法)
需要改写为:由外部指定的PoolingHttpClientConnectionManager直接注册SSLSocketFactory
PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager(
RegistryBuilder.<ConnectionSocketFactory>create().register("https", connectionSocketFactory)
.build());
CloseableHttpClient httpclient = HttpClients.custom()
.setConnectionManager(poolingConnectionManager).disableAutomaticRetries().build();
源码如下:
都是优先使用外部指定的配置,但需要注意使用的前提条件!!!
接着,在内部初始化PoolingHttpClientConnectionManager上注入支持https的SSL对象
SSL证书失败
在使用SSLConnectionSocketFactory的过程中有可能会报错: “RSA premaster secret error” | “SunTlsRsaPremasterSecret KeyGenerator not available”
javax.net.ssl.SSLKeyException: RSA premaster secret error
res:RSA premaster secret error
at sun.security.ssl.RSAClientKeyExchange.<init>(RSAClientKeyExchange.java:87)
at sun.security.ssl.ClientHandshaker.serverHelloDone(ClientHandshaker.java:972)
at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:369)
at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1037)
at sun.security.ssl.Handshaker.process_record(Handshaker.java:965)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1064)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379)
at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:162)
at cfca.trustsign.demo.connector.HttpClient.send(HttpClient.java:155)
at cfca.trustsign.demo.connector.HttpConnector.deal(HttpConnector.java:111)
at cfca.trustsign.demo.connector.HttpConnector.post(HttpConnector.java:70)
at cfca.trustsign.demo.test.Test3401.main(Test3401.java:84)
Caused by: java.security.NoSuchAlgorithmException: SunTlsRsaPremasterSecret KeyGenerator not available
at javax.crypto.KeyGenerator.<init>(KeyGenerator.java:169)
at javax.crypto.KeyGenerator.getInstance(KeyGenerator.java:223)
at sun.security.ssl.JsseJce.getKeyGenerator(JsseJce.java:251)
at sun.security.ssl.RSAClientKeyExchange.<init>(RSAClientKeyExchange.java:78)
... 15 more
还可能报错: “Could not generate secret” | "ECDH key agreement requires ECPrivateKey for initialisation"
Caused by: javax.net.ssl.SSLHandshakeException: Could not generate secret
at sun.security.ssl.ECDHCrypt.getAgreedSecret(ECDHCrypt.java:104)
at sun.security.ssl.ClientHandshaker.serverHelloDone(ClientHandshaker.java:1122)
at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:369)
at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1037)
at sun.security.ssl.Handshaker.process_record(Handshaker.java:965)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1064)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379)
at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:396)
at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:355)
at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:142)
at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:359)
at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:381)
at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:237)
at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:185)
at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:111)
at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56)
at org.springframework.http.client.HttpComponentsClientHttpRequest.executeInternal(HttpComponentsClientHttpRequest.java:87)
at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:53)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:735)
... 5 common frames omitted
Caused by: java.security.InvalidKeyException: ECDH key agreement requires ECPrivateKey for initialisation
at cfca.sadk.org.bouncycastle.jcajce.provider.asymmetric.ec.KeyAgreementSpi.initFromKey(KeyAgreementSpi.java:194)
at cfca.sadk.org.bouncycastle.jcajce.provider.asymmetric.ec.KeyAgreementSpi.engineInit(KeyAgreementSpi.java:168)
at javax.crypto.KeyAgreement.implInit(KeyAgreement.java:346)
at javax.crypto.KeyAgreement.chooseProvider(KeyAgreement.java:378)
at javax.crypto.KeyAgreement.init(KeyAgreement.java:470)
at javax.crypto.KeyAgreement.init(KeyAgreement.java:441)
at sun.security.ssl.ECDHCrypt.getAgreedSecret(ECDHCrypt.java:100)
... 28 common frames omitted
将项目中jdk或jre改成自定义安装的就好,不要用eclipse自带的。
还有:“unable to find valid certification path to requested target” ----------- 这个是RestTemplate构建时要指定能够注入带 SSLConnectionSocket 的HttpClient 的工厂类 HttpComponentsClientHttpRequestFactory
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:397)
at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:302)
at sun.security.validator.Validator.validate(Validator.java:262)
at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:324)
at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:229)
at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:124)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1621)
... 19 common frames omitted
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141)
at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126)
at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280)
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:392)
... 25 common frames omitted
org.springframework.http.HttpMessageConverter 需匹配Content-Type和对象Class
restTemplate使用时: 设置的org.springframework.http.converter.HttpMessageConverter需要与传递参数的Class匹配及org.springframework.http.HttpHeaders设置"Content-Type"的值MediaType匹配。
在 抽象类 org.springframework.http.converter.AbstractHttpMessageConverter<T> implements HttpMessageConverter<T> 中 定义的canRead | canWrite 方法会判定supportedMediaTypes 及 supports的Class
给默认已经装载的HttpMessageConverter增加MediaType 需要如下文处理:
RestTemplate restTemplate = new RestTemplate();
StringHttpMessageConverter httpMessageConverter = new StringHttpMessageConverter();
List<MediaType> list = Lists.newArrayList();
list.addAll(httpMessageConverter.getSupportedMediaTypes());
list.add(MediaType.TEXT_PLAIN);
httpMessageConverter.setSupportedMediaTypes(httpMessageConverter.getSupportedMediaTypes());
restTemplate.getMessageConverters().add(httpMessageConverter);
因为:getSupportedMediaTypes返回的是个不可变集合
postForObject 使用 org.springframework.util.MultiValueMap 传值
4. org.springframework.web.client.RestTemplate.postForObject 传参 一定要使用 org.springframework.util.MultiValueMap 来封装参数,否则 无法传递参数。(应该与服务端接收请求的处理方式有关)
org.apache.http.client.methods.HttpPost 传参使用 org.apache.http.message.BasicNameValuePair
下文两种方式都可以成功传递
private String deal(String remoteUrl, HttpMethod method, String data, String signature) {
try {
MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<String, Object>();
paramMap.add("data", data);
paramMap.add("signature", signature);
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.setCacheControl("no-cache");
requestHeaders.setAccept(Arrays.asList(MediaType.valueOf("text/plain")));
requestHeaders.setAcceptCharset((Arrays.asList(Charset.forName("UTF-8"))));
requestHeaders.add("User-Agent", "client");
requestHeaders.add("Content-Type", "text/plain;charset=UTF-8");
return restTemplate.postForObject(remoteUrl, paramMap, String.class);
} catch (Exception e) {
if (e instanceof HttpStatusCodeException) {
HttpStatusCodeException ex = (HttpStatusCodeException) e;
log.info(ex.getResponseBodyAsString());
}
throw e;
}
}
public void doPost(String remoteUrl, String data, String signature) {
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslConnectionSocketFactory)
.disableAutomaticRetries().build();
HttpPost post = new HttpPost(remoteUrl);
try {
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("data", data));
params.add(new BasicNameValuePair("signature", signature));
post.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));
HttpResponse response = httpclient.execute(post);
System.out.println(EntityUtils.toString(response.getEntity()));
} catch (Exception e) {
e.printStackTrace();
}
}
如果直接传MultiValueMap,用 Content-Type = “ text/plain” 或 “application/x-www-form-urlencoded” 都没问题。 但如果使用:HttpEntity包装MultiValueMap ,则需要使用“application/x-www-form-urlencoded” 。
HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<MultiValueMap<String, Object>>(paramMap, requestHeaders);
String response = restTemplate.postForObject(remoteUrl, httpEntity, String.class);
否则报错:
org.springframework.web.client.RestClientException: No HttpMessageConverter for org.springframework.util.LinkedMultiValueMap and content type "text/plain;charset=UTF-8"
at org.springframework.web.client.RestTemplate$HttpEntityRequestCallback.doWithRequest(RestTemplate.java:957)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:733)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:670)
at org.springframework.web.client.RestTemplate.postForObject(RestTemplate.java:414)
restTemplate对HttpStatus非200的处理
org.springframework.web.client.RestTemplate.handleResponse -> org.springframework.web.client.DefaultResponseErrorHandler.handleError :
restTemplate 对Response的处理会先判定httpStatus的值,非200默认报错。 所以如果对于这种类型的响应还需要拿ResponseBody里的值需要额外扩展
第一种: 自定义ResponseErrorHandler。 绑定: restTemplate.setErrorHandler(new SelfResponseErrorHandler());
这种方式从ClientHttpResponse读出流内数据。 重写的handleError方法虽然屏蔽了父类方法去抛出异常,但后续其他方法内会再次从ClientHttpResponse读流报错: nested exception is java.io.IOException: Attempted read from closed stream.
@Slf4j
static class SelfResponseErrorHandler extends DefaultResponseErrorHandler {
public SelfResponseErrorHandler() {
super();
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
String failMsg = new String(receive(response), AnxinSignConst.DEFAULT_CHARSET);
log.info(failMsg);
}
public byte[] receive(ClientHttpResponse response) throws IOException {
InputStream inputStream = response.getBody();
if (inputStream != null) {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(2);
byte[] buffer = new byte[1024];
int read = -1;
while ((read = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, read);
}
return byteArrayOutputStream.toByteArray();
} catch (Exception e) {
log.error("dealErrorResponse fail", e);
}
}
return null;
}
}
第二种:从 HttpStatusCodeException中直接获取响应
if(e instanceof HttpStatusCodeException) {
HttpStatusCodeException ex = (HttpStatusCodeException)e;
log.info(ex.getResponseBodyAsString());
}
来源:oschina
链接:https://my.oschina.net/u/3434392/blog/3217971