openssl s_client是最轻量可靠的TLS握手验证手段,能快速区分证书链与配置问题;报错num=20说明CA根证书不匹配,num=21表明客户端证书链不完整;Go客户端需显式设置GetClientCertificate回调以确保mTLS证书发送。

openssl s_client 能直接验证握手是否成功
这是最轻量、最可靠的测试手段,不依赖 Go 代码,能第一时间定位是证书链问题还是配置逻辑问题。只要它连不上,Go 服务端或客户端就不可能通。
关键命令格式:
openssl s_client -connect localhost:8443 \ -cert client.crt -key client.key \ -CAfile ca.crt \ -showcerts
注意点:
-
-CAfile必须填服务端信任的 CA 根证书(即签发client.crt的那个 CA),不是客户端自己的证书文件 -
-showcerts会打印服务端实际返回的证书链,帮你确认它有没有发全(比如漏了中间 CA) - 若报
verify error:num=20:unable to get local issuer certificate,说明-CAfile指向的根证书没覆盖服务端证书的签发者 - 若报
verify error:num=21:unable to verify the first certificate,通常是客户端证书链不完整(client.crt只含 leaf,缺中间 CA)
Go 客户端测试时 TLSClientConfig 必须设 GetClientCertificate
只配 Certificates 字段,Go 默认不会主动发送客户端证书——它等服务端先发 CertificateRequest。但某些服务端在 mTLS 强制模式下,若首次握手没收到证书,会直接断开,导致“连都连不上”。
解决办法是显式提供回调:
cfg := &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: serverCAPool,
ServerName: "my-service.internal",
}
cfg.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
return &clientCert, nil
}
这样无论服务端是否挑战,客户端都会把证书带上。漏掉这行,http.Client 在 mTLS 场景下大概率静默失败。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
服务端日志里看 ClientAuth 和 ClientCAs 是否生效
Go 不会在启动时报错告诉你 ClientAuth 没设对,而是默默降级为单向 TLS。必须靠日志或抓包确认服务端真正在校验。
加一行简单日志到 http.HandlerFunc 里:
func handler(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
log.Printf("✅ client cert CN: %s", r.TLS.PeerCertificates[0].Subject.CommonName)
} else {
log.Printf("❌ no client cert presented")
}
}
如果日志里总是打 ❌,立刻检查两件事:
- 服务端
tls.Config.ClientAuth是否为tls.RequireAndVerifyClientCert(不是VerifyClientCertIfGiven,也不是没设) -
tls.Config.ClientCAs是否用AppendCertsFromPEM()正确加载,且返回true(直接赋值 PEM 字节会静默失效)
抓包看 CertificateRequest 和 Certificate 消息是否出现
Wireshark 过滤 tls.handshake.type == 11 || tls.handshake.type == 12,能直观看到 TLS 握手阶段的关键消息:
- 服务端是否发了
CertificateRequest(type 11)?没发,说明ClientAuth没生效 - 客户端是否回了
Certificate(type 12)?没回,说明客户端没配Certificates或GetClientCertificate - 若服务端发了 Request,客户端也回了 Certificate,但最后报
remote error: tls: bad certificate,基本锁定在ClientCAs加载错误或证书链不匹配
这个层面的问题,光看 Go 日志很难定位,抓包是最直接的证据链。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










