ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CERTIFICATE_VERIFY_FAILED报错全解析:从SSL证书链到生产环境配置

CERTIFICATE_VERIFY_FAILED报错全解析:从SSL证书链到生产环境配置 说实话任何一个和HTTP接口打过交道的开发大概率都被这么一行红字折磨过——requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate。我第一次碰到这个CERTIFICATE_VERIFY_FAILED报错时刚从爬虫转后端第一反应就是把requests的verify参数直接设成False接口虽然通了心里却一直不踏实。直到后来接手一个对接第三方支付的项目生产环境不允许关闭验证我才不得不把SSL证书验证失败的整套原理从头啃了一遍。这篇文章我就把这个坑的来龙去脉讲透。从报错原理到自签名证书再到生产环境的免费证书申请、Nginx配置和Nacos这类中间件的HTTPS化整个链路串起来讲。以后你无论在Python、Java、Node还是Nginx里撞上这个报错都能知道从哪儿下手排查而不是靠临时关闭验证糊弄过去。1. CERTIFICATE_VERIFY_FAILED报错到底在验证什么1.1 HTTPS证书链的信任模型一次握手的完整过程很多人对HTTPS的理解停留在“加了密所以安全”这个层面但CERTIFICATE_VERIFY_FAILED这个报错的本质不是加密的问题而是身份信任的问题。当你的客户端通过HTTPS访问一个服务器时除了协商加密算法还会做一件很关键的事验证对方出示的证书是不是可信的。这个验证动作不是简单看一眼证书有没有过期而是完整走一遍证书链。所谓证书链就是一个自上而下的信任结构。最顶层是根证书Root CA它由浏览器或操作系统内置属于“天生被信任”的权威。根证书下面会签发中间证书Intermediate CA中间证书再签发你的服务器证书Leaf Certificate。服务器在TLS握手时把自己的证书连同中间的证书一起发给客户端客户端拿着这个链条一路往上找直到找到自己信任的根证书为止。用生活里的例子说这就像你入职时出示身份证人事专员不是只看你这张证有多像真的而是要通过证件号去公安系统里查底档。如果公安系统里没有你的记录那你这张身份证做得再精美也没用。CERTIFICATE_VERIFY_FAILED就是客户端在“查底档”这一步失败了它没能把你收到的证书链接到任何一条可信的根上于是直接拒绝连接。这个机制到了Python里表现得尤为“较真”。Python的requests库底层依赖urllib3再往下走是标准库ssl模块而ssl模块依赖的是OpenSSL。OpenSSL校验证书的方式比较严格它会调用本地的CA证书库来验证服务器发来的证书链。如果本地CA证书库缺失、过期或者证书链本身不完整OpenSSL就会毫不犹豫地抛出一个CERTIFICATE_VERIFY_FAILED。1.2 触发验证失败的四个核心原因从我处理过的实际案例来看CERTIFICATE_VERIFY_FAILED这个报错表面上长得一样但背后原因可以归成四类。第一类是证书本身有问题。证书过期、域名不匹配、证书链不完整这些都属于“服务器端发的证书就不合格”。比如有些运维配置Nginx时只放了cert.pem没放中间证书chain.pem客户端解析到一半链条断了自然验证失败。第二类是本地CA证书库的问题。这种情况和服务器无关是你的客户端环境缺少必要的根证书。Python 3.4以后开始自带ssl模块的严格校验但不少操作系统或者精简版的Python环境并没有把CA证书库装全。你最常遇到的“unable to get local issuer certificate”就是这个原因。第三类是自签名证书。公司内网、测试环境、临时搭建的服务经常直接用openssl生成一个自签名证书。这种证书没有任何CA给它背书客户端默认不信任于是报CERTIFICATE_VERIFY_FAILED。这类问题的解法不是去“修”什么而是要告诉客户端“请信任这个证书”。第四类比较隐晦就是系统时间不对。证书有效期校验依赖客户端本地时间如果服务器或者开发机的时间差了几个月哪怕证书本身完全正常客户端也会判定它“不在有效期内”。这四类原因我后面都会展开讲。处理SSL证书验证失败最关键的一步就是先判断你遇到的是哪一类判断错了方向后面的努力全白费。2. 什么时候最容易碰到这个报错2.1 Python爬虫与脚本对接里的常见翻车Python生态是CERTIFICATE_VERIFY_FAILED报错的重灾区尤其在写爬虫和脚本的人手里。原因很直接requests库流行到几乎成了Python的网络标配而很多人的requests运行环境从来没打理过CA证书库。最典型的场景是一个爬虫脚本请求某个HTTPS接口前一段时间还好好的突然某一天开始报HTTPSConnectionPool(host..., port443): Max retries exceeded ... SSLError(SSLCertVerificationError ...)。这种“突然坏了”的情况多半不是目标网站的证书坏了而是你本机的CA证书库出了问题。比如你升级了Python版本新版本自带的OpenSSL换了默认的证书搜索路径导致它找不到原来的根证书。还有一种情况在Windows上特别常见。Windows系统自带的证书管理方式和OpenSSL的证书库并不是一回事。Python在Windows上经常读取不到系统根证书所以同一段爬虫代码在Mac上跑得好好的一到Windows上就开始报CERTIFICATE_VERIFY_FAILED。很多人遇到这个情况后直接选择verifyFalse问题看似解决了但实际上是在给自己埋雷——你永远不知道哪一天这个脚本要放到服务器上跑而服务器环境的校验比本地严格得多。除了requestsPython标准库里的urllib.request也会踩同样的坑。不管用哪个库根因都一样OpenSSL在校验证书时找不到可信任的根证书。2.2 Java、Node.js和curl也会碰上同样的问题很多人以为CERTIFICATE_VERIFY_FAILED是Python专属其实不是。任何做TLS握手校验的客户端都可能触发区别只是报错文案不太一样。Java环境里报错通常是PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target。这个报错和Python的CERTIFICATE_VERIFY_FAILED完全同一个性质只是Java用的是自己的cacerts证书库。解决办法是把目标服务器的CA证书导入到Java的cacerts里或者用keytool -importcert手动导入。Java里的-Djavax.net.ssl.trustStore参数也能指定信任库但在生产环境里改变JVM参数需要重启服务涉及面大需要先评估影响。Node.js里有NODE_TLS_REJECT_UNAUTHORIZED0这种环境变量可以关掉校验但这个变量一设整个进程的所有HTTPS请求都不校验证书了危险程度比Python的verifyFalse还要高。我更建议用ca选项指定自定义CA文件const fs require(fs); const https require(https); https.request(url, { ca: fs.readFileSync(/path/to/your-ca.crt) }, (res) { // ... });curl命令行工具呢报错更直接curl: (60) SSL certificate problem: unable to get local issuer certificate网上大量教程会让加-k参数跳过验证。如果你只是临时下载一个文件-k还能接受。但如果这个curl命令出现在自动化脚本里建议把--cacert参数指向一个明确的CA文件或者直接用--resolve指定IP和域名的映射关系至少别让整条链路处于“不设防”的状态。2.3 内网环境里被忽略的细节内网环境是自签名证书和私有CA证书的高发区。很多公司内部系统、测试环境、中间件控制台都没有申请正规证书直接用一个自签名证书顶着。平时大家用浏览器访问时弹出一个“证书不受信任”的警告点一下“继续访问”就进去了没人当回事。但一旦你要用代码去调这些系统的接口麻烦就来了。比如你的内部数据平台提供了一个API浏览器访问是好的但Python脚本一调就CERTIFICATE_VERIFY_FAILED。这个问题的本质是浏览器和Python走的是完全不同的信任路径。浏览器允许用户手动点击信任证书而Python脚本没有交互界面它只能机械化地去证书库里比对。这个场景下动不动就verifyFalse是最偷懒但最不推荐的做法。正确做法是把内网的自签名证书或公司自建的CA证书加入开发机和运行机的系统信任库或者至少让代码通过verify参数明确指定要信任的证书文件。这一步做完内网开发的体感会舒服很多也不用担心哪天因为校验问题突然跑不通。3. 自签名证书的处理全流程从生成到配置到信任3.1 Windows下如何快速生成自签名证书自签名证书最大的价值在于开发调试阶段。你起了个本地服务想在HTTPS协议下联调接口又不想为这个临时环境去申请域名证书自签名证书就是最快的方案。Windows下生成自签名证书路径不止一条。最通用的是装一个OpenSSL。很多从Git官网安装过Git for Windows的同学其实已经自带OpenSSL了安装目录下的usr/bin文件夹里就有openssl.exe。如果你用的是PowerShell也可以走系统自带的路子New-SelfSignedCertificate -DnsName localhost -CertStoreLocation Cert:\LocalMachine\My -FriendlyName LocalHostDevCert这个命令生成证书的速度非常快但它生成出来的证书格式是受Windows证书体系管理的不是你直接能拿来给Nginx用的PEM格式文件。所以如果你要部署到Nginx我建议还是在Windows上装一个OpenSSL然后用命令行方式生成格式上不会踩坑。无论是Windows还是Linux自签名证书的生成命令核心就一行openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout server.key -out server.crt \ -subj /CCN/STBeijing/LBeijing/ODev/CNlocalhost这里几个参数对应关系是-keyout生成私钥-out生成证书文件-newkey rsa:2048一次性生成新的RSA 2048位密钥-x509表示直接输出自签名证书-days 365设置有效期一年。-nodes这个参数很多新手看不懂它表示“不加密私钥文件”这样Nginx加载私钥时不需要输入密码属于开发环境的省事写法。生成完之后同目录下会多出server.key和server.crt两个文件。前者是私钥后者是证书。这两个文件就是你配置HTTPS服务的基础。3.2 Nginx自签名证书的交互式配置方法网上很多教程在讲自签名证书时习惯于用-subj把信息一次性填完看起来确实很高效。但实际操作中很多初学者在交互式环境下更容易理解每个字段的含义。“交互式方法”说白了就是不加-subj参数让openssl一步一步问你。整个对话大概长这样$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout server.key -out server.crt Generating a RSA private key ...... ................... writing new private key to server.key ----- You are about to be asked to enter information that will be incorporated into your certificate request. Country Name (2 letter code) [AU]:CN State or Province Name (full name) [Some-State]:Beijing Locality Name (e.g., city) []:Beijing Organization Name (e.g., company) [Internet Widgits Pty Ltd]:Example Inc Organizational Unit Name (e.g., section) []:IT Common Name (e.g. server FQDN or YOUR name) []:localhost Email Address []:adminexample.com这里的字段从上到下分别是国家、省份、城市、组织名、部门、通用名、邮箱。其中真正重要的只有Common Name这一项你要访问的域名或者IP要和它保持一致。如果填错了客户端会报hostname mismatch也就是主机名不匹配。交互式生成的证书在Nginx里的配置方法和一键生成的证书没有区别。在/etc/nginx/conf.d/下新建一个配置文件写一个HTTPS的server块server { listen 443 ssl; server_name localhost; ssl_certificate /etc/nginx/certs/server.crt; ssl_certificate_key /etc/nginx/certs/server.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }然后执行nginx -t检查语法再执行nginx -s reload让配置生效。到这里你的Nginx就成功跑起了一个HTTPS服务浏览器访问虽然会弹警告但curl加-k或者代码指定verify到对应证书已经可以正常联调了。3.3 把自签名证书变成系统“受信任”的自签名证书配好之后你会发现一个新问题访问还是报错只不过报错的地方从“证书链断裂”变成了“证书不受信任”。原因很简单自签名证书没有权威CA的背书系统默认不认它。要解决这个问题就得把证书手工加入系统的受信任根证书列表。在Windows上双击server.crt文件点击“安装证书”选择“本地计算机”然后选择“将所有证书都放入下列存储”浏览找到“受信任的根证书颁发机构”一路下一步就装好了。装完之后用浏览器访问https://localhost就不会再弹警告。在Linux服务器上操作类似但要分发行版看。CentOS/RHEL系统执行cp server.crt /etc/pki/ca-trust/source/anchors/ update-ca-trustUbuntu/Debian系统执行cp server.crt /usr/local/share/ca-certificates/ update-ca-certificates不过这里有个极其容易被忽略的坑就算你把自签名证书加入了系统信任库Python的requests库仍然可能报错。因为requests用的是certifi库提供的cacert.pem不是系统证书库。这个问题在我带新人时反复出现很多人折腾了半天系统证书最后一查Python根本读的是自己那一套。应对办法其实很简单。要么用pip install --upgrade certifi更新certifi要么在做开发调试时直接把证书路径传给verify参数import requests resp requests.get( https://localhost:8443, verify/path/to/server.crt )这样你的请求就只信任这一个证书既不用动系统配置也不用关校验心智负担最小。等以后上了正规证书把verify参数删掉恢复默认即可。4. 生产环境的证书方案免费证书、Nginx与中间件实战4.1 阿里云免费SSL证书的申请与自动续期自签名证书解决的是开发联调问题到了生产环境还是要用正规CA签发的证书。对于大多数中小项目来说阿里云的免费SSL证书已经足够用。申请流程并不复杂。登录阿里云控制台搜索“数字证书管理服务”进入SSL证书页面。免费证书额度每年有20张一般申请单域名证书即可。填写你要绑定的域名后阿里云会要求你做域名验证用来证明这个域名确实是你持有的。如果域名解析也在阿里云可以直接选“自动DNS验证”点一下就能完成。如果域名在其他服务商就得手动去DNS管理平台加一条TXT记录或者CNAME记录等验证通过后再继续。证书签发后下载Nginx版本的证书包里面一般包含证书文件扩展名通常是.pem或.crt和私钥文件.key。把这两个文件上传到服务器的/etc/nginx/certs/目录下修改Nginx配置reload一下就完了。这里我必须唠叨一句免费证书有效期的问题。现在阿里云免费证书的有效期一般在3个月到12个月之间很多都缩短到3个月了这意味着每年要续期4次。如果你每次都手动去控制台操作很容易忘记。我自己的习惯是上了acme.sh这种自动续期工具配合阿里云的DNS API能让证书在到期前自动更新然后自动reload Nginx。acme.sh的关键配置大概长这样curl https://get.acme.sh | sh alias acme.sh~/.acme.sh/acme.sh acme.sh --issue -d example.com --dns dns_ali acme.sh --install-cert -d example.com \ --key-file /etc/nginx/certs/example.com.key \ --fullchain-file /etc/nginx/certs/example.com.pem \ --reloadcmd nginx -s reload用--dns dns_ali之前需要先在环境变量里配置好阿里云的AccessKey ID和Secret。这样acme.sh就能自动调用DNS API添加解析记录完成域名验证。整个过程全自动再也不用手动续期。4.2 Nginx生产配置的关键细节生产环境配置HTTPS时很多人以为只要把证书路径写上、443端口监听起来就完事了实际上还有几个容易忽略的细节。第一个是关于证书文件的选择。阿里云下载的Nginx证书包里通常有cert.pem和full_chain.pem两个带证书的文件或者类似命名的文件。区别在于cert.pem只包含服务器证书本身full_chain.pem包含了服务器证书加中间证书的完整链。配置Nginx时我强烈建议用full_chain.pem否则部分客户端在校验时发现中间证书缺失会报CERTIFICATE_VERIFY_FAILED。这一点在你之前已经踩过坑的请求上很容易复现用浏览器访问正常用Python或者某些老版本的curl访问就报错十有八九是中间证书链没配全。第二个是HTTPS相关的性能和安全参数。除了常规的ssl_protocols和ssl_ciphers还可以开启HTTP/2和OCSP Stapling。OCSP Stapling的意思是Nginx在握手阶段替客户端向CA查询证书吊销状态并把结果缓存下来发给客户端这样客户端就不用自己再去访问CA的OCSP服务器了能减少TLS握手时间。配置很简单ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 valid300s; resolver_timeout 5s;第三个容易被忽略的是HTTP到HTTPS的跳转。很多站点把HTTP的80端口留着并在server块里返回301跳转到HTTPS这个做法本身没问题但要注意别在跳转时把POST请求变成GET请求。301跳转到另一个协议时浏览器往往会改变请求方式导致接口调不通。正确做法是用308重定向它能保持请求方法和请求体不变。4.3 中间件HTTPS化以Nacos为例现在很多公司内部中间件都是裸HTTP跑在内网大家觉得内网环境无所谓但等你要做等保测评、对接外部系统或者跨网络调用时还是得让中间件支持HTTPS。Nacos就是个很典型的例子。Nacos本身基于Spring Boot构建所以它的HTTPS配置走的也是Spring Boot那套。在Nacos的conf/application.properties文件里增加以下配置server.ssl.enabledtrue server.ssl.certificatefile:/opt/nacos/conf/cert/nacos.crt server.ssl.certificate-private-keyfile:/opt/nacos/conf/cert/nacos.key这里的路径是证书和私钥在服务器上的绝对路径。改完配置后重启Nacos控制台原来的http://ip:8848/nacos就不能再直接用了要改成https://ip:8848/nacos。这一步完成之后你会遇到一个很经典的问题客户端连不上了。Nacos的SDK默认还是按HTTP协议去连而服务端已经变成了HTTPS双方对话当然失败。解决办法是在客户端配置里把Nacos服务端地址改成HTTPS开头。Java的Spring Cloud应用里通常是在bootstrap.yml里改这个地址spring: cloud: nacos: config: server-addr: https://nacos.example.com:8848改完之后还有一个绕不开的坎Java客户端去访问HTTPS的Nacos时也要校验Nacos服务器的证书。如果Nacos用的是自签名证书Java 客户端会直接抛PKIX path building failed。这时候需要把Nacos的证书导入到Java运行环境的cacerts信任库里操作命令是keytool -importcert -alias nacos -file nacos.crt -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit如果不想改全局信任库也可以在启动脚本里加-Djavax.net.ssl.trustStore/path/to/truststore.jks指定一个单独的信任库文件。这个方案更灵活发布多套环境时互不影响。5. 报错变体与排查思路5.1 一张表看清各类SSL报错CERTIFICATE_VERIFY_FAILED只是这一类问题的总称实际项目中你会看到各种细节不同的报错文案。如果只记得一个“证书验证失败”就去网上搜很容易被带偏。我把常见报错变体整理成一张速查表大家可以存一下。报错关键字或场景根因处理方案unable to get local issuer certificate本地CA证书库缺失或证书链不完整更新certifi/系统CA库Nginx改用full_chain.pemcertificate has expired证书已过期重新申请并续期证书hostname mismatch / IP mismatch访问域名或IP与证书绑定不符改用证书绑定的域名访问或重新签发含SAN的证书self-signed certificate自签名证书未被信任将证书加入系统信任库或代码里verify指定该证书SSL: WRONG_VERSION_NUMBER客户端用HTTPS访问了HTTP端口确认端口协议检查Nginx监听端口tlsv1 alert protocol version客户端TLS版本过低升级客户端库或系统版本开启TLS1.2以上PKIX path building failedJava环境证书链不完整将根证书或中间证书导入cacerts信任库表格只能帮你快速分类真正解决时还是要理解背后的排查逻辑。5.2 5分钟排查顺序遇到CERTIFICATE_VERIFY_FAILED我的建议是先别急着改代码按下面的顺序过一遍。第一步用浏览器打开目标URL。浏览器内置的证书校验逻辑和OpenSSL基本一致但它会把证书状态以更友好的方式展示出来。如果浏览器直接报“不安全”或者“证书无效”说明服务器证书本身就有问题下一步要拿openssl s_client去检查证书链。如果浏览器正常说明问题出在客户端环境的信任库上。第二步用curl -v输出详细连接信息。这个命令会把你本地读取的CA文件路径、TLS握手过程、服务器下发的证书链都打印出来。很多Python报错在curl下会显示得很清楚比如“CAfile: /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt: No such file or directory”这基本就是本地证书库缺失或路径不对。第三步用openssl s_client -connect 目标域名:443手动看服务器下发的证书链。如果你的域名是example.com就执行openssl s_client -connect example.com:443 -servername example.com这个命令会输出服务器返回的证书链里面如果有“verify error:num20:unable to get local issuer certificate”说明服务器端的证书链存在缺口可能是Nginx没配置中间证书。如果报错“verify error:num10:certificate has expired”那就是证书过期了。第四步检查本机时间。date命令看一下系统时间如果差了一年以上直接同步时间ntpdate time.windows.com时间同步后再访问一次可能报错就消失了。这个原因真的特别容易忽略尤其是一些内网物理服务器或者虚拟机时间漂移严重。第五步检查代码环境里有没有别的干扰因素。比如Python进程里是否设置了SSL_CERT_FILE环境变量Java进程是否设了-Djavax.net.ssl.trustStore之类的参数。这几个变量的优先级很高一旦被设置代码里默认的信任库路径就会被覆盖。5.3 几个亲测有效的避坑技巧最后分享几个实践中亲测有效的技巧。第一个Python里判断当前使用的CA路径。用下面这行代码即可import ssl print(ssl.get_default_verify_paths())输出里会包含openssl_cafile、openssl_capath这些字段在排查requests证书问题时非常有用。它可以明确告诉你当前Python到底在哪个路径找CA证书避免你一直改系统证书库却不生效。第二个临时关闭警告但不要长期关闭验证。开发环境里用verifyFalse没问题但别忘了同时关掉那个烦人的InsecureRequestWarningimport urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)关掉警告只是让日志干净一点不代表你的请求是安全的。代码评审时只要看到verifyFalse出现在生产分支里我基本都会打回。第三个如果你在公司内网建议让运维搭建一个内部的私有CA并统一推送到各台开发机和服务器。这样内部系统都可以用私有CA签发证书开发环境不再天天看“不安全”警告代码里也不需要去指定verify路径。这个方案一次性投入长期收益很大比每个系统都搞自签名证书然后到处导入信任要省心得多。SSL证书验证失败这个问题说难也难说简单也简单。难的是它涉及的层面多从证书生成、服务端配置到客户端信任库任何一个环节出了岔子都会出现类似的报错。说到底CERTIFICATE_VERIFY_FAILED不是在和你作对它只是告诉你有环节没接上。只要你把证书链和信任关系理顺这个报错自然就会消失。
返回列表