UniHttp/Jsoup Https SSL证书验证失败:SunCertPathBuilderException解决方案详解

目录

前言

一、场景再现

1、要做什么事

2、使用Java调用接口

二、Jsoup的实现与问题解决

1、使用Jsoup来访问第三方接口

2、Jsoup解决访问Https问题的方法

三、UniHttp的实现与问题解决

1、UniHttp定义第三方接口

2、UniHttp调用接口实战

3、UniHttp解决Https的访问问题

四、总结


前言

在当今微服务架构与数据驱动的开发范式中,HTTP客户端已成为Java应用不可或缺的基础设施。无论是构建高性能的RESTful API调用,还是开发数据采集与内容解析工具,我们习惯依赖UniHttp(及其背后的OkHttp/Spring RestTemplate技术栈)和Jsoup这类成熟库来简化网络通信的复杂度。然而,当请求的目标URL从http://切换到https://时,一个看似简单的协议升级却常常成为开发旅程中的"暗礁"------SunCertPathBuilderException这个晦涩的异常信息,就像一道无形的数字证书壁垒,将无数开发者挡在安全的HTTPS世界之外。

这个异常最令人生畏之处,在于它的出现往往伴随着项目进度的戛然而止。想象一下这样的场景:你的爬虫程序在测试环境中对目标网站解析完美,一旦部署到生产服务器,Jsoup.connect()却抛出javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target;或者你的微服务在调用第三方支付接口时,UniHttp客户端突然报告证书路径构建失败。

本文的目标读者是正在与SSL证书问题搏斗的Java开发者------无论你是需要快速修复本地开发环境阻塞,还是负责设计生产级安全通信架构,都能在这里找到清晰的技术路径。后续章节将从异常堆栈的深度解读开始,逐步展开证书链验证的底层原理。让我们一同揭开HTTPS证书验证的神秘面纱,将这个令人沮丧的异常转化为深入理解Java安全体系的机会。

一、场景再现

本节我们将讲述一下我们需要使用Java来实现一个什么需求。我们以国家统计局为例,我们需要使用互联网接口来获取查询相应的数据,而网站的协议是Https,那么如何获取Https的接口数据呢,下面来看看调用https接口时,可能会遇到什么问题。

1、要做什么事

统计数据相信大家都会遇到,比如如下官网网站:

可以在浏览器的网络栏中,看到以下链接:

bash 复制代码
https://data.stats.gov.cn/easyquery.htm?m=QueryData&dbcode=fsyd&rowcode=zb&colcode=sj&wds=%5B%7B%22wdcode%22%3A%22reg%22%2C%22valuecode%22%3A%22110000%22%7D%5D&dfwds=%5B%7B%22wdcode%22%3A%22zb%22%2C%22valuecode%22%3A%22A030102%22%7D%5D&k1=1692174996701&h=1

将以上连接复制到浏览器中,可以看到以下输出:

如果我们想实现动态的参数传入和数据获取,使用Java语言可以有哪些方式来获取Https接口返回的数据呢?且看下文说明。

2、使用Java调用接口

在Java中,要实现第三方接口的调用,有很多种实现方式,比如使用原生的网络接口,也可以使用HttpClient这样的组件,与HttpClient相类似还有Jsoup和UniHttp,Jsoup也是一个非常常见的第三方接口访问组件;而UniHttp则是方便快速的进行接口集成的组件。以Jsoup为例,当我们使用Jsoup编写完了代码之后,在访问Https接口时,就很容易发生以下异常:

报错异常信息如下:

bash 复制代码
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(Unknown Source)
	at sun.security.validator.PKIXValidator.engineValidate(Unknown Source)
	at sun.security.validator.Validator.validate(Unknown Source)
	at sun.security.ssl.X509TrustManagerImpl.validate(Unknown Source)
	at sun.security.ssl.X509TrustManagerImpl.checkTrusted(Unknown Source)
	at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(Unknown Source)
	... 16 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at sun.security.provider.certpath.SunCertPathBuilder.build(Unknown Source)
	at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(Unknown Source)
	at java.security.cert.CertPathBuilder.build(Unknown Source)
	... 22 more

从技术本质上看,SunCertPathBuilderException并非某个库的缺陷,而是Java安全模型正确运作的体现。当客户端发起HTTPS请求时,JVM会触发一套严格的证书链验证机制:它不仅要验证服务器证书的数字签名是否有效、域名是否匹配,更要构建一条从服务器证书到可信根证书颁发机构(CA)的完整信任链。这个过程中,如果任何一个中间证书缺失、根证书不在JVM的信任库(cacerts)中、或者证书已过期失效,Sun security provider就会抛出该异常。在Java 8之后,虽然sun包下的类被标记为内部API,但这个异常类名依然保留,成为开发者诊断SSL问题的关键线索。问题的复杂性还在于应用场景的多样性。

二、Jsoup的实现与问题解决

对于Jsoup而言,其典型场景是网络爬虫与HTML解析,目标网站参差不齐------既有使用Let's Encrypt免费证书的正规站点,也有配置错误缺失中间证书的小众网站,甚至还有使用自签名证书的内部系统或测试环境。本节主要分享如何使用Jsoup来访问第三方接口,以及如何解决访问Https协议的问题。

1、使用Jsoup来访问第三方接口

首先我们来看一下正常情况下要使用Jsoup来访问第三方接口的正确方式,核心代码如下:

java 复制代码
package com.yelang.project.jsoupcase;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
public class RawDataHomePage {
	public static void main(String[] args) {
        try {
            Connection.Response response = Jsoup.connect("http://data.stats.gov.cn/easyquery.htm?m=QueryData&dbcode=fsyd&rowcode=zb&colcode=sj&wds=%5B%7B%22wdcode%22%3A%22reg%22%2C%22valuecode%22%3A%22110000%22%7D%5D&dfwds=%5B%7B%22wdcode%22%3A%22zb%22%2C%22valuecode%22%3A%22A030102%22%7D%5D&k1=1692174996701&h=1")
                    .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.54")
                    .ignoreContentType(true)
                    .execute();
            String body = response.body();
            System.out.println(body);
        } catch (IOException e) {
            e.printStackTrace();
        }
	}
}

以上代码编写完成后,运行程序后,遇到以上问题怎么解决呢?如果大家有验证证书的话,可以直接在Jsoup中带上验证证书即可,这样也不会出现问题。如果我们什么都没有,却因为项目需要,也需要接入数据,那么如何解决呢?

2、Jsoup解决访问Https问题的方法

解决问题的关键就是,我们在程序访问时使用忽略的方式来避免Https验证。设置忽略的核心代码如下:

java 复制代码
package com.yelang.project.jsoupcase;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
public class RawDataHomePage {
	public static void main(String[] args) {
		// 创建自定义 TrustManager
        TrustManager[] trustManagers = new TrustManager[]{new TrustAllCertManager()};
        // 获取默认的 SSLContext
        SSLContext sslContext = null;
        try {
            sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, trustManagers, new java.security.SecureRandom());
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            e.printStackTrace();
        }
        // 应用自定义的 SSLContext
        HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
        try {
            Connection.Response response = Jsoup.connect("http://data.stats.gov.cn/easyquery.htm?m=QueryData&dbcode=fsyd&rowcode=zb&colcode=sj&wds=%5B%7B%22wdcode%22%3A%22reg%22%2C%22valuecode%22%3A%22110000%22%7D%5D&dfwds=%5B%7B%22wdcode%22%3A%22zb%22%2C%22valuecode%22%3A%22A030102%22%7D%5D&k1=1692174996701&h=1")
                    .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.54")
                    .ignoreContentType(true)
                    .execute();
            String body = response.body();
            System.out.println(body);
        } catch (IOException e) {
            e.printStackTrace();
        }
	}
}

这里通过创建自定义 TrustManager的方式类来进行设置,避免输入证书。设置完毕后,再次运行数据,可以在控制台看到后台服务有接口返回了。

三、UniHttp的实现与问题解决

除了可以使用Jsoup的方式来进行接口集成,我们也可以试试 UniHttp(作为现代HTTP客户端的封装)来进行企业级微服务调用。看看使用UniHttp会遇到什么样的问题?本节我们将详细介绍如何使用UniHttp来进行第三方接口定义与调用以及如何解决Https的访问问题。

1、UniHttp定义第三方接口

首先为了演示方便,我们将前面的访问接口使用UniHttp来进行创建引用定义。定义的核心代码如下:

java 复制代码
package com.yelang.project.thridinterface.freeapi;
import com.burukeyou.uniapi.http.annotation.HttpApi;
import com.burukeyou.uniapi.http.annotation.SslCfg;
import com.burukeyou.uniapi.http.annotation.param.QueryPar;
import com.burukeyou.uniapi.http.annotation.request.GetHttpInterface;
import com.burukeyou.uniapi.http.core.response.HttpResponse;
@HttpApi(url = "https://data.stats.gov.cn")
public interface GovStatService {

	@GetHttpInterface("/easyquery.htm?m=QueryData&dbcode=fsyd&rowcode=zb&colcode=sj&wds=%5B%7B%22wdcode%22%3A%22reg%22%2C%22valuecode%22%3A%22110000%22%7D%5D&dfwds=%5B%7B%22wdcode%22%3A%22zb%22%2C%22valuecode%22%3A%22A030102%22%7D%5D&k1=1692174996701&h=1")
	public HttpResponse<String> easyqueryOne();

	@GetHttpInterface("/easyquery.htm")
	public HttpResponse<String> easyQueryTwo(@QueryPar("m") String m, @QueryPar("dbcode") String dbcode,
			@QueryPar("rowcode") String rowcode, @QueryPar("colcode") String colcode, @QueryPar("wds") String wds,
			@QueryPar("dfwds") String dfwds, @QueryPar("k1") String k1, @QueryPar("h") String h);
}

以上这两个方法都是一个方法,不一样的区别就是第一个查询接口的查询参数是不能修改的,第二个查询接口的参数是可以动态传入。

2、UniHttp调用接口实战

然后在Junit组件中来调用这个接口,并且调用官网的服务接口,核心代码如下:

java 复制代码
package com.yelang.project.unihttp;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.burukeyou.uniapi.http.core.response.HttpResponse;
import com.yelang.project.thridinterface.freeapi.GovStatService;
@SpringBootTest
@RunWith(SpringRunner.class)
public class GovStatServiceCase {
	@Autowired
	private GovStatService govStatService;
	@Test
	public void testOne() {
		try {
			HttpResponse<String> result = govStatService.easyqueryOne();
			System.out.println(result.getBodyResult());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	@Test
	public void testTwo() {
		String m = "QueryData";
		String dbcode = "fsyd";	
		String rowcode = "zb";
		String colcode = "sj";
		String wds = "[{\"wdcode\":\"reg\",\"valuecode\":\"110000\"}]";
		String dfwds = "[{\"wdcode\":\"zb\",\"valuecode\":\"A030102\"}]";
		String k1 = "1692174996701";
		String h = "1";
				
		HttpResponse<String> result = govStatService.easyQueryTwo(m, dbcode, rowcode, colcode, wds, dfwds, k1, h);
		System.out.println(result);
		System.out.println(result.getBodyResult());
	}
}

运行之后,你会发现,两个方法都报错了,都是跟Jsoup差不多的错误信息。

在异常的信息可以看到最原始的异常如下:

3、UniHttp解决Https的访问问题

首先来看看在UniHttp中有没有相关的技术点,在其官方文档上可以看到以下的章节说明:

可以看到,在UniHttp中,对于https的访问是有控制的,接着往下看,在UniHttp中,其支持单向和双向认证。单向认证支持直接配置 证书certificate 或者 keysotre。 并且支持配置成文件路径、或者文件base64内容 , 会动态识别和取值。而单向认证又有三种不同的设置方法。

配法1、使用证书文件配置信任证书:

java 复制代码
// 方式1) 配置证书文件的内容
@SslCfg(trustCertificate = "信任证书文件的base64内容")

// 方式2) 配置证书文件的路径
@SslCfg(trustCertificate = "classpath:ssl/server.crt")

// 方式3)配置证书的环境变量,从环境变量取值. 
// 请确保配置了该环境变量为证书文路径件或者证书内容
@SslCfg(trustCertificate = "${channel.mtuan.ssl.cert}")

配法2、没有证书文件,也可以使用keystore文件配置信任证书.

java 复制代码
@SslCfg(
    // 配置keystore文件路径 (当前同上也可配置文件base64内容、或者环境变量)
	trustStore = "classpath:ssl/server01.p12",
	// keyspre文件类型。 在jdk8不配默认是 jks . 而在jdk11默认是 PKCS12
	trustStoreType="PKCS12",
	// keystore文件密码
	trustStorePassword = "文件密码"
)

配法3、如果你啥证书也没有,也可以配置直接关闭SSL验证。

java 复制代码
@SslCfg(
    // 关闭信任证书校验、会信任所有证书  
	closeCertificateTrustVerify=true,
	// 关闭域名校验,信任所有域名。  否则会校验证书SAN或者CN
	closeHostnameVerify=true
)

双向认证相比单向认证需要多提供一个keystore文件并且里面包含客户端自己私钥和公钥证书, 所以信任证书的配置此处不再描述,同单向认证一致。

配法1、分别配置 证书和私钥

java 复制代码
@SslCfg(
        // 客户端的公钥证书    (当前同上也可配置文件base64内容、或者环境变量)
        certificate = "classpath:ssl/client.crt",
        // 客户端的私钥文件      (当前同上也可配置文件base64内容、或者环境变量)
        certificatePrivateKey = "classpath:ssl/client.key"
)

配法2、如果已经放到keysotre文件里也可以直接配置keystore文件

java 复制代码
@SslCfg(
        // keystore文件路径    (当前同上也可配置文件base64内容、或者环境变量)
        keyStore = "classpath:ssl2/ca_client.pkcs12",
        // keyspre文件类型。 在jdk8不配默认是 jks . 而在jdk11默认是 PKCS12
        keyStoreType = "PKCS12",
        // keystore文件密码
        keyStorePassword = "文件密码",
        // 使用的key条目, 如果不配置默认使用第一个密钥条目。
        keyAlias = "key条目别名",
        // key条目密码
        keyPassword = "key条目密码"
)

这里我们使用单向配置中的最后一条,首先配置关闭验证吧,核心代码如下:

java 复制代码
package com.yelang.project.thridinterface.freeapi;
import com.burukeyou.uniapi.http.annotation.HttpApi;
import com.burukeyou.uniapi.http.annotation.SslCfg;
import com.burukeyou.uniapi.http.annotation.param.QueryPar;
import com.burukeyou.uniapi.http.annotation.request.GetHttpInterface;
import com.burukeyou.uniapi.http.core.response.HttpResponse;
@HttpApi(url = "https://data.stats.gov.cn")
public interface GovStatService {

	@GetHttpInterface("/easyquery.htm?m=QueryData&dbcode=fsyd&rowcode=zb&colcode=sj&wds=%5B%7B%22wdcode%22%3A%22reg%22%2C%22valuecode%22%3A%22110000%22%7D%5D&dfwds=%5B%7B%22wdcode%22%3A%22zb%22%2C%22valuecode%22%3A%22A030102%22%7D%5D&k1=1692174996701&h=1")

	@SslCfg( // 关闭信任证书校验、会信任所有证书
			closeCertificateTrustVerify = true,
			// 关闭域名校验,信任所有域名。 否则会校验证书SAN或者CN
			closeHostnameVerify = true)
	public HttpResponse<String> easyqueryOne();

	@GetHttpInterface("/easyquery.htm")
	@SslCfg( // 关闭信任证书校验、会信任所有证书
			closeCertificateTrustVerify = true,
			// 关闭域名校验,信任所有域名。 否则会校验证书SAN或者CN
			closeHostnameVerify = true)
	public HttpResponse<String> easyQueryTwo(@QueryPar("m") String m, @QueryPar("dbcode") String dbcode,
			@QueryPar("rowcode") String rowcode, @QueryPar("colcode") String colcode, @QueryPar("wds") String wds,
			@QueryPar("dfwds") String dfwds, @QueryPar("k1") String k1, @QueryPar("h") String h);
}

在此运行之前的测试程序,则可以正常获取数据,并进行输出,如下图所示:

四、总结

以上就是本文的主要内容,本文详细的介绍了在开发中遇到的需要获取Http是网站的服务的场景,以及在Java中使用Jsoup和UniHttp来分别进行接口服务集成的具体实现与遇到的问题。通过实例的讲解,不仅让大家明白如何在Java中如何具体实现,也同时使用不同的方法说明Jsoup和Unihttp如何解决Https网站接口的问题,希望通过本文对开发者有所帮助。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。

相关推荐
才聚PMP9 小时前
关于开启NPDP项目2025年第二次续证工作的通知
网络协议·https·ssl
toooooop820 小时前
Nginx 反向代理 HTTPS CDN 配置检查清单(避坑版)
运维·nginx·https·cdn
尽兴-1 天前
[特殊字符] 微前端部署实战:Nginx 配置 HTTPS 与 CORS 跨域解决方案(示例版)
前端·nginx·https·跨域·cors·chrom
李少兄1 天前
IDEA / DataGrip 连接 SQL Server 提示“驱动程序无法通过 SSL 加密建立安全连接”的解决方法
安全·intellij-idea·ssl
2501_916008891 天前
HTTPS 请求抓包,从原理到落地排查的工程化指南(Charles / tcpdump / Wireshark / Sniffmaster)
ios·小程序·https·uni-app·wireshark·iphone·tcpdump
尽兴-1 天前
macOS 系统下 Chrome 浏览器安装 HTTPS 证书完整指南
chrome·macos·https·证书·ssl·pem·crt
2501_915909062 天前
WebView 调试工具全解析,解决“看不见的移动端问题”
android·ios·小程序·https·uni-app·iphone·webview
2501_915921432 天前
iOS 抓不到包怎么办?工程化排查与替代抓包方案(抓包/HTTPS/Charles代理/tcpdump)
android·ios·小程序·https·uni-app·iphone·tcpdump
Jerry2505092 天前
怎么才能实现网站HTTPS访问?
网络协议·http·网络安全·https·ssl