2、HTTPS与TLS:TLS握手原理、Go中配置HTTPS服务、自签名证书生成、双向TLS认证
说实话,很多刚入行的Go后端同学,对HTTPS的理解就停留在「加个证书就行」。但我在实际项目中踩过不少坑——有一次线上服务突然大面积报错,排查了半天,发现是TLS握手超时导致的。从那以后,我对TLS的每个环节都格外上心。
这一章,咱们就把HTTPS和TLS聊透。从握手原理开始,再到Go里怎么配、自签名证书怎么生成,最后聊聊双向TLS认证——也就是传说中的mTLS。
2.1 TLS握手到底在握什么?
先问个问题:为什么需要TLS握手?说白了,就是客户端和服务端要商量好三件事:
- 加密算法:用哪种对称加密来传输数据
- 会话密钥:双方协商出一个只有彼此知道的密钥
- 身份验证:服务端(有时也包括客户端)证明「我是我」
我习惯把TLS握手分成四个阶段来理解:
- Client Hello:客户端发个招呼,带上支持的TLS版本、加密套件列表、一个随机数。
- Server Hello:服务端从列表里挑一个加密套件,发回自己的证书和另一个随机数。
- 证书验证与密钥交换:客户端验证证书合法性,然后生成预主密钥,用服务端的公钥加密发过去。服务端用自己的私钥解密。
- 握手完成:双方用预主密钥和两个随机数,算出最终的会话密钥。之后所有通信都用这个对称密钥加密。
关键点:TLS握手用的是非对称加密(公钥/私钥),但握手完成后,实际数据传输用的是对称加密。这样既保证了安全性,又兼顾了性能。
我的经验:有一次我调试一个性能问题,发现TLS握手占了连接建立时间的70%。后来启用了TLS会话复用(Session Resumption),直接把握手时间砍掉了大半。这个优化在Go里默认就是开启的,但如果你自己写底层net.Conn,记得要手动支持。
2.2 Go中配置HTTPS服务
Go标准库对HTTPS的支持非常友好。你只需要一个证书文件和一个密钥文件,几行代码就能跑起来。
先看一个最基本的例子:
package main
import (
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, HTTPS!"))
})
// 启动HTTPS服务
err := http.ListenAndServeTLS(":443", "server.crt", "server.key", mux)
if err != nil {
log.Fatal(err)
}
}
嗯,就这么简单。但这里有个坑——ListenAndServeTLS默认使用的是Go标准库的TLS配置。如果你需要更精细的控制,比如指定最低TLS版本、强制使用某些加密套件,那就得自己构造http.Server了。
我个人习惯这样写:
package main
import (
"crypto/tls"
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, Secure World!"))
})
// 自定义TLS配置
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
},
PreferServerCipherSuites: true,
}
server := &http.Server{
Addr: ":443",
Handler: mux,
TLSConfig: tlsConfig,
}
log.Fatal(server.ListenAndServeTLS("server.crt", "server.key"))
}
避坑指南:我曾经在生产环境遇到过一个问题——某些老旧的客户端(比如Android 4.x)只支持TLS 1.0,而我们强制要求TLS 1.2以上,导致这些用户无法访问。后来我们做了兼容处理:对敏感接口强制TLS 1.2,对普通接口放宽到TLS 1.0。但说实话,现在TLS 1.0和1.1已经被广泛认为不安全了,能禁就禁吧。
2.3 自签名证书生成
开发环境或者内网服务,没必要去CA机构买证书。自己签一个就行。Go标准库的crypto/x509包就能搞定。
下面是我常用的生成脚本:
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"os"
"time"
)
func main() {
// 生成私钥
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
// 构造证书模板
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"My Org"},
CommonName: "localhost",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
DNSNames: []string{"localhost", "example.com"},
}
// 自签名
certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
// 写入证书文件
certFile, _ := os.Create("server.crt")
pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
certFile.Close()
// 写入密钥文件
keyFile, _ := os.Create("server.key")
pem.Encode(keyFile, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
keyFile.Close()
}
小技巧:自签名证书的浏览器会提示不安全,这是正常的。你可以把证书导入到系统的信任根证书列表里,或者干脆在开发时用InsecureSkipVerify: true跳过验证——但千万别在生产环境这么干!
2.4 双向TLS认证(mTLS)
普通的HTTPS只验证服务端的身份。但有些场景下,服务端也需要验证客户端的身份——这就是双向TLS认证,也叫mTLS。
什么时候用?微服务之间的通信、API网关和后端服务之间、或者一些金融级别的接口。说白了,就是双方都要证明「我是我」。
实现起来也不复杂。服务端需要配置ClientAuth为tls.RequireAndVerifyClientCert,然后提供一个CA证书来验证客户端证书。
服务端代码示例:
package main
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
"net/http"
)
func main() {
// 加载CA证书,用于验证客户端
caCert, _ := ioutil.ReadFile("ca.crt")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCertPool,
MinVersion: tls.VersionTLS12,
}
server := &http.Server{
Addr: ":8443",
TLSConfig: tlsConfig,
}
log.Fatal(server.ListenAndServeTLS("server.crt", "server.key"))
}
客户端代码示例:
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net/http"
)
func main() {
// 加载客户端证书和密钥
cert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
// 加载CA证书
caCert, _ := ioutil.ReadFile("ca.crt")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}
resp, _ := client.Get("https://localhost:8443/")
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}
核心要点:mTLS的核心是「双向验证」。服务端用CA证书验证客户端证书的合法性,客户端用CA证书验证服务端证书的合法性。双方都持有自己的私钥,确保通信的机密性和完整性。
我曾经踩过的坑:有一次配置mTLS,客户端一直报「tls: bad certificate」。查了半天,发现是客户端证书的ExtKeyUsage没设置x509.ExtKeyUsageClientAuth。Go的TLS库会检查这个字段,如果没设置,直接拒绝连接。所以生成客户端证书时,别忘了加上ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}。
2.5 总结一下
这一章我们聊了:
- TLS握手的四个阶段:Client Hello、Server Hello、证书验证与密钥交换、握手完成
- Go中配置HTTPS服务的两种方式:简单版和自定义TLS配置版
- 自签名证书的生成方法,以及开发环境的使用技巧
- 双向TLS认证的实现,包括服务端和客户端的完整代码
说实话,TLS这块内容看起来多,但实际用起来就那么几个套路。你只要把证书生成、服务端配置、客户端配置这三个环节搞明白,大部分场景都能应付。
下一章,咱们聊聊JWT——另一种常见的鉴权方式。到时候你会发现,TLS和JWT其实是互补的,一个管传输安全,一个管身份认证。