go的net/http默认不校验https主机名,必须显式设置tls.config.servername才能触发san匹配验证,否则可能遭受mitm攻击;禁用insecureskipverify是危险做法,应通过rootcas加载私有ca证书。

Go 的 net/http 默认不校验主机名,这很危险
Go 的 http.Client 在发起 HTTPS 请求时,默认不会验证服务器证书中的 Subject Alternative Name (SAN) 是否匹配你请求的域名。也就是说,如果你用 http.Get("https://example.com"),底层 TLS 握手成功了,但 Go 不会自动检查证书里有没有 example.com —— 它把这事交给了 tls.Config.VerifyPeerCertificate 和更底层的 tls.Config.InsecureSkipVerify 控制逻辑,而默认配置下这个校验是「静默跳过」的(实际由 crypto/tls 自动触发,但前提是 InsecureSkipVerify 为 false,且你没覆盖 VerifyPeerCertificate)。问题在于:很多开发者误以为「用了 HTTPS 就安全」,却没意识到 Go 不像浏览器或 curl 那样强制做主机名匹配。
常见错误现象:x509: certificate is valid for other.example.net, not example.com 这类错误其实只在你显式启用了校验时才会暴露;默认情况下,它可能悄悄连上一个证书完全不匹配的中间人服务器。
- 必须手动设置
tls.Config.ServerName,否则crypto/tls无法知道该拿哪个域名去比对 SAN -
http.Transport的TLSClientConfig若未指定ServerName,Go 会尝试从 URL Host 推导,但推导失败(如 IP 地址、带端口的 host)时就放弃校验 - 若服务端证书是泛域名(
*.example.com),而你请求的是api.example.com,只要ServerName正确设置,校验能通过;但请求example.com就不行 —— 泛域名不匹配裸域
如何正确配置 http.Client 做主机名验证
核心不是「开启校验」,而是「确保校验有依据」:你要告诉 TLS 层「我期望连的是谁」,它才好拿证书里的 SAN 去比对。这靠 tls.Config.ServerName 实现,而不是改 InsecureSkipVerify。
使用场景:调用内部微服务、对接第三方 API、爬虫需防 MITM。
立即学习“go语言免费学习笔记(深入)”;
- 永远不要设
InsecureSkipVerify: true,哪怕测试环境也不推荐 —— 改用本地自签名 CA +RootCAs更安全 - 如果 URL 是
https://127.0.0.1:8443,ServerName不能留空,得显式设成你期望的域名(如"localhost"),否则校验无目标 - 多个域名共用一个 client?不行 ——
ServerName是全局配置,不同域名需不同http.Transport实例,或每次请求前动态构造 transport(不推荐) - 示例关键片段:
tr := &http.Transport{ TLSClientConfig: &tls.Config{ ServerName: "api.example.com", // 必须和 URL Host 语义一致 // RootCAs: rootCAs, // 如需自定义 CA,才设这个 }, } client := &http.Client{Transport: tr}tls.Dial连接时忘记传serverName参数直接调
tls.Dial(比如做长连接、自定义协议)时,第三个参数是serverName字符串,它和tls.Config.ServerName是同一回事 —— 忘了传,等于没校验。常见错误现象:连接成功,但后续读写突然报
remote error: tls: bad certificate,或者更隐蔽地,被中间人劫持后通信仍「看似正常」。- 传空字符串
""和不传效果一样:TLS 层无法提取预期主机名,跳过 SAN 比较 - 如果后端是 SNI 虚拟主机(如 Nginx 多域名共用 IP),
serverName还影响服务端选哪个证书,不传可能导致返回错误证书 - 示例:
conn, err := tls.Dial("tcp", "10.0.1.5:443", &tls.Config{ ServerName: "service.internal", // 这里设了,但 dial 第三个参数还得传! }, nil) // 正确写法: conn, err := tls.Dial("tcp", "10.0.1.5:443", &tls.Config{}, "service.internal")自签名证书或私有 CA 下的验证绕过陷阱
开发时用
mkcert或 OpenSSL 生成的证书,浏览器能点信任,但 Go 默认不认 —— 它不会自动加载系统根证书,也不会读取CERTIFICATE_PATH环境变量。很多人第一反应是设InsecureSkipVerify: true,这是最危险的惯性操作。性能/兼容性影响:跳过校验本身开销小,但一旦上线就等于裸奔;而正确加载私有 CA 只需一次解析,后续连接复用即可。
- 正确做法是把 CA 证书 PEM 内容读入
*x509.CertPool,再赋给tls.Config.RootCAs - 别用
os.ReadFile每次都读 —— 初始化时读一次,全局复用*x509.CertPool - 如果证书链不完整(比如缺 intermediate),
RootCAs只加了 root,校验仍会失败;此时要把整个 chain(root + intermediate)都加进去 - 验证是否生效:抓包看 Client Hello 的 SNI 是否正确,再看 Server Hello 返回的证书是否被接受(可加
VerifyPeerCertificate回调打印错误)
crypto/tls底层,不显式干预就依赖默认行为;而 Go 的默认行为对主机名验证既不激进也不保守 —— 它要求你明确说出「我在连谁」,否则就不较真。这点和多数语言不同,容易被忽略。 - 正确做法是把 CA 证书 PEM 内容读入
- 传空字符串











