RestTemplate踩坑

冷暖自知 提交于 2020-04-05 23:53:44

池化 + 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());
}

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!