Spring Boot之 RestTemplate 如何调用 HTTPS 请求
1 具体使用
如何构造 RestTemplate,以便可以调用 HTTPS,直接上源码,如下:
@Configuration
public class RestTemplateManager {
@Bean
public RestTemplate httpsRestTemplate(HttpComponentsClientHttpRequestFactory httpsFactory) {
RestTemplate restTemplate = new RestTemplate(httpsFactory);
restTemplate.setErrorHandler(new ResponseErrorHandler() {
@Override
public boolean hasError(ClientHttpResponse clientHttpResponse) {
return false;
}
@Override
public void handleError(ClientHttpResponse clientHttpResponse) {
}
});
return restTemplate;
}
@Bean(name = "httpsFactory")
public HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory() throws Exception {
CloseableHttpClient httpClient = acceptsUntrustedCertsHttpClient();
HttpComponentsClientHttpRequestFactory httpsFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
httpsFactory.setReadTimeout(SemanticConfig.INSTANCE.getSocketTimeout());
httpsFactory.setConnectTimeout(SemanticConfig.INSTANCE.getConnectTimeout());
return httpsFactory;
}
public static CloseableHttpClient acceptsUntrustedCertsHttpClient() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
HttpClientBuilder b = HttpClientBuilder.create();
// setup a Trust Strategy that allows all certificates.
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
return true;
}
}).build();
b.setSSLContext(sslContext);
// don't check Host names, either.
HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
// create a SSL Socket Factory, to use our weakened "trust strategy" and create a Registry, to register it.
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", sslSocketFactory)
.build();
// create connection-manager using registry allows multi-threaded use
PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
connMgr.setMaxTotal(200);
connMgr.setDefaultMaxPerRoute(100);
b.setConnectionManager(connMgr);
return b.build();
}
}
然后在需要使用的地方,直接注入即可
@Autowired
private RestTemplate restTemplate;
2 踩坑
说点踩坑的题外话
2.1 背景
历史原因,服务A需要调用服务A自身的API,此时出现来Bug,但在出现问题前只有一个commit,就是修改了RestTemplate的构造方法(以便可以调用HTTPS格式的URL)。
2.2 排查
排查初期一直很奇怪,调用请求是单例,但是每次debug发现调用的用户都会由用户a变为用户b。
但是用户属性在请求初期是正常的,待到逻辑处理中间才发现是通过请求中的session中变更的。
因为内部调用API是使用RestTemplate,此前没仔细看源码,此时怀疑可能是其复用了链接请求。
可以验证是否请求连接复用:
查看发过来的请求地址和端口是否相等,一般情况下 request 每次的请求端口不同。
String addr = request.getRemoteAddr() + ":" + request.getRemotePort();
很明显两次是一致的,确定了HTTP请求连接的复用导致session内包含旧的用户名,导致请求的用户和session的用户不同,而出现权限不同,导致创建数据集时出现项目下无可用数据集的报错提示。
2.3 总结
后面也去查看了一下 RestTemplate 源码,确定了猜想,具体信息如下:
RestTemplate 的无参构造方法使用的 ClientHttpRequestFactory 类型实例为:SimpleClientHttpRequestFactory 的createRequest方法内openConnection是每次都新建一个新的连接。具体可见openConnection源码的注释说明。
* <P>A new instance of {@linkplain java.net.URLConnection URLConnection} is
* created every time when invoking the
* {@linkplain java.net.URLStreamHandler#openConnection(URL)
* URLStreamHandler.openConnection(URL)} method of the protocol handler for
* this URL.</P>
但为了支持 HTTPS,此处需要RestTemplate(ClientHttpRequestFactory requestFactory) 去构造对HTTPS的兼容处理,而此处需要 引入httpClient,对于此时的RestTemplate,其内部的连接由线程池管理,每次发起的请求会最大化复用闲置的线程,而不是新建一个连接。