gin实现mtls必须在http.server层配置,核心是tlsconfig.clientauth设为tls.requireandverifyclientcert且clientcas非空;否则静默降级为单向tls,白名单失效。

Gin 本身不处理 TLS 握手,所以双向认证(mTLS)必须在 http.Server 层配置,不能靠 Gin 中间件或路由注册实现。 否则客户端证书会被忽略,服务端静默降级为单向 TLS,白名单逻辑完全失效。
必须显式设置 TLSConfig.ClientAuth
Go 的 http.Server 默认不验证客户端证书,即使你传了 ClientCAs,也得明确告诉它“必须且强制校验”:
-
TLSConfig.ClientAuth必须设为tls.RequireAndVerifyClientCert—— 用tls.VerifyClientCertIfGiven不行,它只在校验客户端发了证书时才做,不强制,等于没开 -
TLSConfig.ClientCAs必须是非空的*x509.CertPool,里面只放你信任的 CA 证书(根或中间),不是放客户端证书 - 如果
ClientCAs加载失败(路径错、权限不足、PEM 格式含 BOM),池为空,所有客户端都会被拒,错误日志只显示bad certificate,无具体原因
从 r.TLS.PeerCertificates 安全提取白名单标识
在 Gin handler 里拿到证书链后,不能直接用 CommonName 或 SerialNumber.String() 做比对:
- 先检查
len(r.TLS.PeerCertificates) == 0,否则 panic - 取
peerCerts[0](终端实体证书),优先用peerCerts[0].SubjectKeyId:它是 CA 签发时嵌入的、不可篡改的字节切片 - 转成十六进制字符串用
fmt.Sprintf("%x", peerCerts[0].SubjectKeyId),别用hex.EncodeToString多绕一层 - 白名单建议预加载为
map[string]struct{},避免每次查文件或 DB 引入延迟和并发风险
Gin 启动 HTTPS + mTLS 的正确写法
不要用 r.RunTLS(),它不支持传入自定义 TLSConfig;必须手动构造 http.Server:
srv := &http.Server{
Addr: ":8443",
Handler: r,
TLSConfig: &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caPool, // 已加载好 CA 证书的 *x509.CertPool
MinVersion: tls.VersionTLS12,
},
}
log.Fatal(srv.ListenAndServeTLS("server.crt", "server.key"))
-
server.crt和server.key是服务端证书和私钥,与客户端无关 -
caPool需提前用os.ReadFile+certpool.AppendCertsFromPEM()加载,注意错误检查 - 客户端需带完整证书链(含中间证书),且私钥权限必须是
0600,否则 curl 或 OpenSSL 会拒绝发送
最常被忽略的一点:TLS 层只管“证书是否由可信 CA 签发”,业务层白名单是另一层独立校验。两者脱节(比如 TLS 校验通过但 handler 里忘了查白名单,或白名单 key 提取方式错误),整个 mTLS 就形同虚设。











