调取Https接口遇到的坑及解决方式

目录

踩坑的过程

背景

前提

开始排查问题原因

 结论

关于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 reset
        at 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 同步序列编号

ACKAcknowledge 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
     */
    @Bean
    public 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");
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        try {
            sunJSSEX509TrustManager.checkClientTrusted(chain, authType);
        } catch (CertificateException excep) {
            excep.printStackTrace();
        }
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        try {
            sunJSSEX509TrustManager.checkServerTrusted(chain, authType);
        } catch (CertificateException excep) {
            excep.printStackTrace();
        }
    }

    @Override
    public 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)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值