目录
踩坑的过程
背景
前提
开始排查问题原因
结论
关于Https协议的接口
Java实现调取Https接口方式
1.RestTemplate跳过验证
2.校验方式
3.Forest框架调取接口
参考文档
踩坑的过程
背景
现在有一个平台提供的接口,是Https形式的,但是呢,提供的文档上写的是Http,他们还好心付上了代码样例,你以为这样就可以CV了吗,想简单了,年轻人。
但在实际调取的时候发现,接口不通,负责人告诉我,这就是简单的https接口,你们调就行,直到把缺少证书的报错发给他,才给我一个server.pem,一个server.key,使用这两个文件调取,还是出现问题;
后来又给了一个pem文件,使用这个最新的文件调接口,还是报错,更换了各种调取接口的代码后,依旧是一样的错误;
前提
1.因为服务端禁止ping,采用telnet命令(没有的话需要先安装)
2.这个台服务器上hosts文件也做了IP对域名的映射
3.服务器上的DNS设置也做了
开始排查问题原因
1.先使用keytool命令,将最新的pem证书文件导入客户端服务器的JDK中
keytool -import -v -trustcacerts -alias mycacerts -file radiance-com-cn.pem -storepass changeit -keystore JDK/jre/lib/security/cacerts
2.查看导入的证书
keytool -list -keystore "JDKfile/jdk1.8.0_131/jre/lib/security/cacerts" -storepass changeit
3.使用curl命令调取接口并查看结果
curl -v https://xxxxxxxx
curl命令需要支持https协议
若curl不支持https协议,再看下服务器上是否存在openssl主件
4.尝试使用代码调接口,也是不行的,一直是报错,connect reset
java.net.SocketException: Connection resetat java.net.SocketInputStream.read(SocketInputStream.java:209) ~[?:1.8.0_102]at java.net.SocketInputStream.read(SocketInputStream.java:141) ~[?:1.8.0_102]
5.抓包
在https客户端这边抓包,结果是TCP三次握手之后,客户端开始SSL验证,向服务端发送了client hello,服务端返回了reset,将这次连接拆掉了,后边的SSL/TSL校验也不存在了;
在服务端抓包看,是43600发送来的reset,这个和上面服务端上对不上的!
结论
所以,看到这里,只能证明,在客户端服务器和服务端服务器之间还有其他的网络设备,导致的网络问题,进而导致的客户端和服务端抓包不一致的
将这个抓包和现象给平台的负责人反馈后,也不知道他们改了什么东西,就可以调通接口了,下面的事情就好办了,照着抓包进行调接口就行;
高呼:抓包yyds!
实际接口入参、返回值和原文档对不上这种事情太常见了...
关于Https协议的接口
Https接口是和Http接口不同的,在Http的上面加了一层SSL/TSL证书校验;
一般调取的方式为跳过证书的验证,直接访问接口,跳过的方式也又很多种,我一开始采用的是RestTemplate的方式;
SYN:Synchronize Sequence Numbers 同步序列编号
ACK:Acknowledge character 确认字符
RST:reset
一次正常的双向验证如下图所示
先进行TCP的三次握手,握手成功后
1.客户端向服务端发送client hello,携带客户端这边支持的加密套件,32位随机数发送给服务端
2.服务端收到client hello后,回复ACK
3.服务端接着回复Server Hello,并且生成随机数、Ciper Suite使用的加密算法,tls的版本这里我们使用的是TLS v1.2;
4.客户端收到Server Hello后,回复ACK
5.接着,服务端将生成公钥和私钥,私钥保留将公钥发送给客户端,以认证身份,并且发送Server Hello Done表示Server Hello结束;
6.客户端收到服务端的证书回复ACK
7.客户端将证书发送给服务端,进行证书的交换Client key Exchange,生成一对公钥和私钥,公钥发给服务端;
8.服务端收到之后,回复ACK
9.客户端和服务端进行交换,开始使用最开始协商好了的密钥,服务端收到后回复ACK
10.客户端根据交互过程中交换的信息,以及服务端的密码套件,生成了相对应的密匙
encrypted handshake message:客户端通过发送一段加密后的finished的信息给服务端,是为了证明刚才握手建立起来的加密通信;
Java实现调取Https接口方式
java代码层面实现调取Https方式有很多种,下面简单介绍几种,都是CV的网上的代码,直接拿来用即可,不需要重复造轮子
1.RestTemplate跳过验证
@Configuration
public class RestTemplateConfig {/*** restTemplate跳过Https接口 SSL认证** @return*/@Beanpublic RestTemplate restTemplate() {RestTemplate restTemplate = new RestTemplate();restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));return new RestTemplate(generateHttpRequestFactory());}private HttpComponentsClientHttpRequestFactory generateHttpRequestFactory() {TrustStrategy acceptingTrustStrategy = (x509Certificates, authType) -> true;SSLContext sslContext = null;try {sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();} catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {e.printStackTrace();}SSLConnectionSocketFactory connectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());HttpClientBuilder httpClientBuilder = HttpClients.custom();httpClientBuilder.setSSLSocketFactory(connectionSocketFactory);CloseableHttpClient httpClient = httpClientBuilder.build();HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();factory.setHttpClient(httpClient);return factory;}
}
在http工具类中直接使用restTemplate调取接口即可
2.校验方式
进行校验的方式有很多种,在上述案例踩坑的过程中,在网上找了很多使用证书验证的方式,也写了很多测试的方法尝试着去调取他们的接口,但是都是报错了;
这块有很多种方式,但是由于平台方给的证书不符合要求就不一一列举了,chatgpt确实帮了大忙,嘎嘎一顿写
现在先写这一个,之后慢慢研究
以下代码通过chatgpt生成,并未通过实际验证,慎用!!!
/*** 使用pem进行SSL校验,调取https接口,post请求** @param httpsUrl 接口URL* @param body 入参* @param keystoreFile pem文件路径* @param keystorePass pem文件密码* @param sslType SSL的类型* @return 调取接口的返回*/public String sendPostByHttps(String httpsUrl, JSONObject body, String keystoreFile, String keystorePass, String sslType) {try {//设置可通过ip地址访问https请求HttpsURLConnection.setDefaultHostnameVerifier(new NullHostNameVerifier());URL url = new URL(httpsUrl);HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();// 在连接之前先设置SSL上下文SSLContext sslContext = SSLContext.getInstance(sslType);// 创建SSLContext对象,并使用我们指定的信任管理器初始化TrustManager[] tm = {new MyX509TrustManager(keystoreFile, keystorePass)};sslContext.init(null, tm, new java.security.SecureRandom());conn.setSSLSocketFactory(sslContext.getSocketFactory());// 后续操作以及可以正常进行 HTTPS 请求了 conn.setRequestMethod("POST");conn.setDoOutput(true);// 打开输出流,以便向服务器提交数据conn.setDoInput(true); // 打开输入流,以便从服务器获取数据PrintWriter out = new PrintWriter(new OutputStreamWriter(conn.getOutputStream(), StandardCharsets.UTF_8));out.print(body.toJSONString());out.flush();out.close();int responseCode = conn.getResponseCode();log.info("调取接口,HTTPS返回状态码为:{}", responseCode);if (responseCode == HttpURLConnection.HTTP_OK) {InputStream inputStreamRes = conn.getInputStream();BufferedReader reader = new BufferedReader(new InputStreamReader(inputStreamRes));String line;StringBuilder result = new StringBuilder();while ((line = reader.readLine()) != null) {result.append(line);}reader.close();inputStreamRes.close();return result.toString(); //即为接口的返回}} catch (Exception e) {log.error("请求HTTPS接口,请求失败 | e = {}", e.getMessage(), e);}return null;}
public class MyX509TrustManager implements X509TrustManager {private X509TrustManager sunJSSEX509TrustManager;MyX509TrustManager(String keystoreFile, String pass) throws Exception {KeyStore ks = KeyStore.getInstance("JKS");ks.load(new FileInputStream(keystoreFile), pass.toCharArray());TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509", "SunJSSE");tmf.init(ks);TrustManager[] tms = tmf.getTrustManagers();for (TrustManager tm : tms) {if (tm instanceof X509TrustManager) {sunJSSEX509TrustManager = (X509TrustManager) tm;return;}}throw new Exception("Couldn't initialize");}@Overridepublic void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {try {sunJSSEX509TrustManager.checkClientTrusted(chain, authType);} catch (CertificateException excep) {excep.printStackTrace();}}@Overridepublic void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {try {sunJSSEX509TrustManager.checkServerTrusted(chain, authType);} catch (CertificateException excep) {excep.printStackTrace();}}@Overridepublic X509Certificate[] getAcceptedIssuers() {return sunJSSEX509TrustManager.getAcceptedIssuers();}}
3.Forest框架调取接口
此方法经过验证是可以的,也是上手最快的;
🎁 新手介绍 | Forest (dtflyx.com)
pom依赖
<dependency><groupId>com.dtflys.forest</groupId><artifactId>forest-spring-boot-starter</artifactId><version>1.5.0</version></dependency>
properties文件中配置项
#配置后端HTTP-API为okhttp3
forest.backend=okhttp3
#forest组件中是否开启日志打印总开关
forest.log-enabled=false
#forest组件中id值
forest.ssl-key-stores.id=myhttpstest
#forest组件中证书的路径
forest.ssl-key-stores.file=
#forest组件中密匙没有空着
forest.ssl-key-stores.keystore-pass=
#forest组件中密匙没有空着
forest.ssl-key-stores.cert-pass=
#forest组件中加密的版本
forest.ssl-key-stores.protocols=TLSv1.2
public interface HttpsClient {/*** Https接口测试** @param url 接口名* @param param 入参* @param headerMap 接口的Header* @return*/@Post(url = "${url}", keyStore = "myhttpstest")ForestResponse<String> getHttpsTest(@DataVariable("url") String url,@JSONBody String param,@Header Map<String, Object> headerMap);
在Application启动类中加上如下注解
@ForestScan(basePackages = "com.test.forest")
@SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}
直接将HttpsClient注入调取接口的业务方法中,通过方法调用的形式即可实现接口的调用,具体的原理呢,框架已经帮我们实现了,不用重复的写那么多的代码了;
....ForestResponse<String> resultEntity = httpClient.getIdmPersonInfo(url, param.toString(), headerMap);
参考文档
https://blog.csdn.net/qq78442761/article/details/120149824
wireshark抓包分析HTTPS - 知乎 (zhihu.com)