API 报 SSL 证书错误怎么解决?先对时间再验证书链,六种报错原文一次查到根因

SSL certificate problem 一冒出来,先别动代码。让一台机器上所有 HTTPS 请求一起失败的,占比最大的原因是系统时间——差几分钟,证书的生效期判定就全错,报错却长得像服务端配置问题。按这个顺序查:①系统时间 ②证书链本身 ③中间设备 ④运行时的证书库 ⑤极少数情况才轮到服务端。下面的命令都能直接复制运行。

五步排查路径,按顺序走一遍

第一步:先对系统时间

证书里有两个时间点,NotBeforeNotAfter,客户端拿本机时间去比,落在区间外握手当场就否了,报错还常常误导成"找不到签发者"。时间会漂的原因很实在:主板纽扣电池没电、虚拟机挂起后恢复、云主机快照回滚、NTP 端口被网络挡掉、双系统来回切。

第二步:看证书链本身

链有没有发全、叶证书什么时候到期、签发者是谁,一条 openssl s_client 全给。链里只有叶证书而没有中间证书时,命令行必报 unable to get local issuer certificate,浏览器却一切正常——这个组合本身就说明问题在链上。

第三步:看中间设备

企业出口做 TLS 检查、安全软件带 HTTPS 扫描,都会在中间重新签一次证书。特征就是同一段代码在家里正常、只有公司网里报错,或者链里多出不认识的签发者。

第四步:看运行时的证书库

Python 读 certifi 的包内证书库或 OpenSSL 默认路径,Node 读自己内置的根证书列表,Windows 上走 schannel 的程序读系统证书库,浏览器又是另一套。四个库互不同步:根证书装进系统,Python 照样报错,因为它压根没读那个库。

第五步:最后才看服务端

只有本机诊断输出里 Verify return code 是 0,或者换几个网络环境都报同样的错,才值得怀疑服务端。这时候要做的是拿证据,不是猜。

六种报错原文,各自在说什么

curl 的原文

60 是校验给出的结论,35 是连接异常,方向相反。

Python requests 的原文

requests.exceptions.SSLError: HTTPSConnectionPool(host='xyuapi.top', port=443): Max retries exceeded with url: /v1/models (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)')))

真正有用的是两段:CERTIFICATE_VERIFY_FAILED 说明卡在校验环节,unable to get local issuer certificate 说明找不到签发者。_ssl.c:1006 是 CPython 的源码行号,跟你的代码无关;Max retries exceeded 是 urllib3 的重试包装,不是网络超时。后半句换成 certificate has expired 是有效期问题,换成 self signed certificate in certificate chain,基本可以断定有中间设备在签发。

Node 和 Windows 的原文

Error: unable to verify the first certificate 是链里缺中间证书;UNABLE_TO_VERIFY_LEAF_SIGNATURE 是叶证书的签发者不在 Node 内置根证书列表里——Node 默认不读 Windows 证书库。走 schannel 的程序报 schannel: SEC_E_UNTRUSTED_ROOT (0x80090325),意思是根证书不在系统受信任列表里。

对照表:报错原文 → 含义 → 该敲什么

报错原文真正含义现在该敲的命令
curl: (60) ... unable to get local issuer certificate链缺中间证书,或本地库没有对应根证书openssl s_client -connect xyuapi.top:443 -showcerts
curl: (60) ... certificate has expired证书过期,或本机时间超前date -u,再对一遍 openssl 里的 NotAfter
curl: (35) ... SSL_ERROR_SYSCALL握手被切断,不是校验失败curl -vI https://xyuapi.top/v1/models
CERTIFICATE_VERIFY_FAILED ... unable to get local issuer certificatePython 用的证书库不认这条链python -c "import ssl;print(ssl.get_default_verify_paths())"
self signed certificate in certificate chain链里被插入自签根证书,多半是企业出口或安全软件打开 certlm.msc 看受信任的根证书颁发机构
Node: unable to verify the first certificateNode 内置根证书库里没有叶证书的签发者NODE_EXTRA_CA_CERTS 指向合并后的 PEM
Node: UNABLE_TO_VERIFY_LEAF_SIGNATURE同上,叶证书签发者不在 Node 的 CA 列表node -p "require('tls').rootCertificates.length"
schannel: SEC_E_UNTRUSTED_ROOT (0x80090325)Windows 系统证书库没有该根证书certmgr.msc → 受信任的根证书颁发机构 → 导入

第一条该敲的命令:对时间,再验证书

date -u
openssl s_client -connect xyuapi.top:443 -servername xyuapi.top -showcerts </dev/null 2>&1 | head -40
curl -vI https://xyuapi.top/v1/models 2>&1 | head -30

date 和 w32tm 怎么读

date -u 打印 UTC 时间,跟手机对一眼,差两分钟以上就先处理时间,别往下查。Windows 上对应的是:

Get-Date -AsUTC
w32tm /query /status
w32tm /resync

w32tm /resyncno time data was available 说明对时源没连上,手动来:设置 → 时间和语言 → 日期和时间 → 打开"自动设置时间",再点"立即同步";服务被组策略关掉就先 net start w32time。Linux 上:

timedatectl status
sudo timedatectl set-ntp true
sudo chronyc makestep

第三行给装了 chrony 的机器用,让时间立刻纠正;容器时钟跟着宿主机走,容器里时间不对就去修宿主机。

openssl s_client 的输出怎么读

从上往下看四样:

再往上看 Certificate chain 下面有几段:正常两到三段,0 s: 是叶证书,1 s: 是中间证书,偶尔还有 2 s:。只有一段 0 s: 就说明服务端或你面前的中间设备没把中间证书发出来,这正是 unable to get local issuer certificate 的来源。到期时间看 NotAfter 那一行,格式是 GMT。

链只有一段,为什么浏览器却正常

浏览器会按叶证书里的 AIA 字段把中间证书下载下来补上;对做过 TLS 检查的设备,系统里装了企业根证书就直接放行。curl 和 OpenSSL 只用手上拿到的那条链,不补也不猜。所以"浏览器正常、代码报错"几乎都指向证书链或证书库,不是服务端挂了。

按场景修,每个场景给一个动作

场景一:系统时间漂了

date -u 和手机时间差超过两分钟就处理。Windows 跑 w32tm /resync,成功后用 Get-Date -AsUTC 确认;Linux 跑 sudo timedatectl set-ntp truetimedatectl status 里要看到 System clock synchronized: yes。双系统机器注意:Linux 把硬件时钟当 UTC、Windows 当本地时间,切一次差八小时,执行 timedatectl set-local-rtc 0 统一按 UTC 记。

场景二:企业网络在中间做了 TLS 检查

认三个信号:手机热点下正常;链里出现不认识的签发者;报错是 self signed certificate in certificate chain。要修的是信任配置,不是关掉校验。Windows 按 Win+R 输入 certmgr.msc,进"受信任的根证书颁发机构"→ 证书 → 右键 → 所有任务 → 导入,把企业根证书 .cer 导进去。certmgr.msc 管当前用户,服务、后台进程要用 certlm.msc 装到本地计算机级。

Python 改环境变量最快:

certutil -encode corp-root.cer corp-root.pem
# PowerShell,当前会话生效
$env:REQUESTS_CA_BUNDLE="C:\certs\corp-root.pem"
# Linux / macOS
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/corp-bundle.pem

文件要是 PEM 格式,并且包含完整链:企业根证书加中间证书拼在一起。追加到 certifi 的 cacert.pem 末尾也能用,但下次升级 certifi 会覆盖掉,所以优先用环境变量。Node 用 NODE_EXTRA_CA_CERTS,值是 PEM 绝对路径,只在进程启动时读一次,改完必须重启:

set NODE_EXTRA_CA_CERTS=C:\certs\corp-root.pem
node app.js

场景三:Python 的 certifi 太旧

症状是同一个域名在别的机器正常、你这台报 unable to get local issuer certificate,而环境里的 certifi 是几年前装依赖时顺带拉进来的。

python -m pip install --upgrade certifi
python -c "import certifi,os,datetime;print(certifi.where());print(datetime.datetime.fromtimestamp(os.path.getmtime(certifi.where())))"

第二条打出证书库路径和这个文件的修改时间。时间还停在两年前,说明更新没落到你正在用的解释器上——用 python -m pip 而不是裸 pip,保证两者是同一个环境。

场景四:安全软件在中间插了一手

装了抓包类工具,或者杀毒软件开着 HTTPS 扫描之后才开始报错,停掉那个模块就恢复。临时关掉 HTTPS 扫描,跑一遍 curl -vI https://xyuapi.top/v1/models,通了就是它。测完立刻把模块打开,这一步只是定位手段。长期方案:按场景二把它的根证书导进系统证书库,或者把接口域名加进软件白名单。

场景五:服务端证书链不完整或者已经到期

先拿证据:

echo | openssl s_client -connect xyuapi.top:443 -servername xyuapi.top 2>/dev/null | openssl x509 -noout -subject -issuer -dates
openssl s_client -connect xyuapi.top:443 -servername xyuapi.top -showcerts </dev/null 2>&1 | sed -n '1,40p'

第一条给主体、签发者、生效和到期时间,第二条给完整链。反馈时把这两段输出、curl -vI 的前 30 行、公网出口 IP、报错时刻一起发出去,比说一句"我这边报证书错误"有效得多。xyuapi.topxyuai.cc 是两条独立线路,只有一个报错就先切另一个入口。

场景六:只有某个客户端报错

Chatbox、Cherry Studio 这类桌面客户端各自带运行时,证书库不一定跟着系统走。先在本机跑 curl -vI https://xyuapi.top/v1/models,返回 200 就说明问题在客户端自己的证书库。动作按顺序试:升级客户端;把根证书装到本地计算机级(certlm.msc);完全退出客户端再启动,Electron 类客户端只在启动时读一次证书库;客户端里若提供忽略证书错误的开关,只用来做一次对照确认,确认完立刻关掉。

两段能直接跑的诊断代码

Python:查时间偏差、证书库路径、握手信息

import ssl, socket, datetime, urllib.request
from email.utils import parsedate_to_datetime

HOST = "xyuapi.top"

# ① 本机时间 vs 服务端时间:用明文 HTTP 拿 Date 头,不受证书影响
try:
    with urllib.request.urlopen("http://www.baidu.com", timeout=8) as r:
        server = parsedate_to_datetime(r.headers["Date"])
    now = datetime.datetime.now(datetime.timezone.utc)
    print("本机 UTC :", now.strftime("%Y-%m-%d %H:%M:%S"))
    print("网络 UTC :", server.strftime("%Y-%m-%d %H:%M:%S"))
    print("偏差(秒) :", round((now - server).total_seconds(), 1))
except Exception as e:
    print("取时间失败:", e)

# ② 当前解释器认哪个证书库
p = ssl.get_default_verify_paths()
print("cafile   :", p.cafile or "(未设置,走 OpenSSL 默认路径)")
print("capath   :", p.capath)
try:
    import certifi
    print("certifi  :", certifi.where())
except ImportError:
    print("certifi  : 未安装")

# ③ 握手一次,把签发者、SAN、到期时间打出来
ctx = ssl.create_default_context()
try:
    with socket.create_connection((HOST, 443), timeout=10) as sock:
        with ctx.wrap_socket(sock, server_hostname=HOST) as ss:
            cert = ss.getpeercert()
            print("TLS 版本 :", ss.version())
            print("签发者   :", dict(x[0] for x in cert["issuer"]))
            print("生效     :", cert["notBefore"])
            print("到期     :", cert["notAfter"])
            print("SAN      :", [v for k, v in cert.get("subjectAltName", ()) if k == "DNS"][:6])
except ssl.SSLCertVerificationError as e:
    print("证书校验失败:", e.verify_message, "| code =", e.verify_code)
except Exception as e:
    print("握手失败:", type(e).__name__, e)

第一段拿明文 HTTP 的 Date 头当参照,超过两分钟就是时间问题;第二段说明这个解释器在读哪个证书文件;第三段把叶证书的签发者、到期时间和 SAN 打出来,过期、域名不符还是签发者不认一眼就分清。

Node:正确指定 CA 证书的做法

const https = require('https');
const tls = require('tls');
const fs = require('fs');

// ca 是替换而不是追加:必须把 Node 自带的根证书一起带上,
// 否则访问其他正常站点会全部失败
const agent = new https.Agent({
  ca: [...tls.rootCertificates, fs.readFileSync('C:/certs/corp-root.pem', 'utf8')],
  keepAlive: true,
  minVersion: 'TLSv1.2',
});

const req = https.request('https://xyuapi.top/v1/models', {
  method: 'GET',
  agent,
  headers: { Authorization: `Bearer ${process.env.XYU_API_KEY}` },
}, (res) => {
  console.log(res.statusCode, res.headers['content-type']);
  res.resume();
});

req.on('error', (e) => console.error('请求失败:', e.code, e.message));
req.end();

ca 是替换语义,这是常见的坑:只传企业根证书,连别的 HTTPS 站点也会开始报错。写成数组、把 tls.rootCertificates 展开在前面,才是"多加一个信任的根"。为什么不要全局关校验:

process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';   // 反例,别这么写
new https.Agent({ rejectUnauthorized: false });   // 反例,别这么写

rejectUnauthorized: false 关掉的正是"对端是不是真的 xyuapi.top"这个判断。之后的请求里 Authorization: Bearer ... 照样发出去,任何能改你流量的中间人都能收到这把 Key。这段配置通常写在全局 agent 上,等于整个进程的外发请求一起失去身份确认。企业根证书要处理的是"多信任谁",不是"不检查信任"。

临时不做证书校验:只用来确认问题在哪

关掉校验能告诉你什么

curl -k 一跑就通,说明握手本身没问题,卡点在"校验"这一环,接着去查证书链和证书库;还是报 curl: (35) ... SSL_ERROR_SYSCALL,那就跟证书无关,是连接被切了,去查出口防火墙和中间设备。

curl -k -vI https://xyuapi.top/v1/models 2>&1 | head -20
curl -k -o /dev/null -s -w 'http=%{http_code} tls=%{time_appconnect}\n' https://xyuapi.top/v1/models

Python 里对应的是:

import requests, urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
r = requests.get("https://xyuapi.top/v1/models", verify=False, timeout=10)
print(r.status_code)

为什么生产环境一律不做

关掉校验等于放弃确认"你在跟谁说话"。TLS 干两件事:加密,以及确认对端身份。关掉的是第二件,第一件还在——流量还是密文,但你把它交给了未经确认的对象。中间人只要能把流量引过去,就能完整看到你发出去的 Key、模型名和全部提示词,再原样转发给真服务端,两边都不报错。这个开关还会传染:写下的 verify=False 会被复制到下一个文件、下一条流水线,当成一次性器械用完就扔。

用完之后的检查清单

确认完,把这四样在代码库里搜一遍并清掉:

grep -rn "verify=False|rejectUnauthorized|NODE_TLS_REJECT_UNAUTHORIZED|curl -k|--insecure" \
  --include="*.py" --include="*.js" --include="*.sh" --include="Dockerfile*" .

搜出命中就改回正常校验。

速查表:症状 → 根因 → 动作

症状根因动作
这台机器所有 HTTPS 一起报证书错误系统时间漂出证书有效期区间w32tm /resync,或 sudo timedatectl set-ntp true
浏览器正常,curl 报 unable to get local issuer certificate链里缺中间证书,命令行不补链Certificate chain 的段数,只有一段就是没发全
公司网报错,手机热点正常出口设备做了 TLS 检查把企业根证书导入系统证书库
Python 报 CERTIFICATE_VERIFY_FAILED,其他语言正常certifi 或 OpenSSL 证书库太旧python -m pip install --upgrade certifi
Node 报 unable to verify the first certificateNode 不读 Windows 证书库NODE_EXTRA_CA_CERTS 指向 PEM 文件
装了抓包工具或开扫描后才报错中间插入了自签根证书临时停用模块做对照,确认后导入根证书
curl: (35) SSL_ERROR_SYSCALL握手被切断,不是校验失败curl -vI 看卡在哪一步,查出口设备
只有 xyuai.cc 报错,xyuapi.top 正常该线路的证书链异常先切另一条入口,附 openssl 输出反馈

接口地址和协议先确认一遍

接入信息

小鱼API(xyuai.cc)是 AI API 接入平台,把主流大模型聚合成一个 OpenAI 兼容接口,改一行 base URL 就能接上。

配客户端时的证书相关坑:base URL 要带 /v1,填成裸域名会 404,某些客户端把 404 包装成"连接失败",让人误以为是证书问题。先跑 curl -vI https://xyuapi.top/v1/models 确认命令行能通,再配客户端。

部分模型按次计费参考价

模型单价
gemini-2.5-pro0.031 元/次
deepseek-v3.2-thinking0.049 元/次
deepseek-r1-thinking0.049 元/次
grok-4.10.05 元/次
gemini-3-pro-preview0.05 元/次
claude-sonnet-4-5-thinking0.09 元/次
kimi-k2.5 / kimi-k2.60.09 元/次
claude-opus-4-5-thinking0.12 元/次
gpt-5.4-pro-thinking0.15 元/次
claude-sonnet-4-6-thinking0.2 元/次
claude-opus-4-6-thinking0.25 元/次
NanoBanana-Pro(生图)0.27 元/次
gpt-5.3-pro0.3 元/次

按次计费的好处是排查成本可预期:一次请求多少钱是固定的,上下文塞多长都一样。

排查完了接上就能用

证书问题解决后,验证整条链路省事的办法就是打一次模型列表:

curl -s -H "Authorization: Bearer $XYU_API_KEY" https://xyuapi.top/v1/models | head -c 300

返回的 JSON 里有带 id 的模型名,说明从 TLS 握手到鉴权这一路都通了。换成 https://xyuai.cc/v1/models 再跑一次,两条入口都通才算这台机器真的没问题。

相关阅读

🚀 想要立即使用?来小鱼API体验全系列AI模型

查看全部产品

支持Gemini / Claude / GPT / Grok / DeepSeek · 国内直连 · 支付宝/微信