Go 语言
标准库、反射和 cgo
net/http 实现安全通信

使用 net/http 实现安全通信

HTTP 协议是目前 Web 服务中使用最广泛的应用层协议。不过 HTTP 协议是采用明文传输的,所以存在以下风险:

  • 使用不加密的明文进行通信,内容可能会被窃听;
  • 不验证通信方的身份,因此有可能遭遇伪装;
  • 无法证明报文的完整性,所以有可能已遭篡改。

HTTPS 就是为了解决这些问题而诞生的。HTTPS 是在 HTTP 上加入了 SSL/TLS 协议,通过 SSL/TLS 协议对通信内容进行加密,以确保数据传输的安全。

Go 语言的 net/http 包提供了对 HTTPS 的支持,可以很方便地实现 HTTPS 通信。下面我们就来看看如何使用 net/http 包实现 HTTPS 通信。

func main() {
    http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, Gopher!")
    })
    http.ListenAndServeTLS(":8080", "server.crt", "server.key", nil)
}

ListenAndServe 相比 ListenAndServeTLS 多了两个参数,分别是证书信息和密钥信息。这两个参数是针对 HTTPS 协议进行内容加密、身份验证和内容完整性验证的前提。

利用 openssl 工具可以生成证书和密钥:

$ openssl genrsa -out ca.key 2048
$ openssl req -new -x509 -key ca.key -out ca.crt -days 3650
$ openssl genrsa -out server.key 2048
$ openssl req -new -key server.key -out server.csr
$ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650

可以使用 curl 命令测试:

$ curl -k https://localhost:8080
Hello, Gopher!

-k 参数是为了忽略证书验证,因为我们使用的是自签名证书。

对服务端证书的校验

我们来实现一个客户端对服务端公钥证书的校验:

server.go
func main() {
    http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, Gopher!")
    })
    http.ListenAndServeTLS(":8080", "server.crt", "server.key", nil)
}

启动后,我们创建一个客户端访问上述服务端:

client.go
func main() {
    resp, err := http.Get("https://localhost:8080")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer resp.Body.Close()
    io.Copy(os.Stdout, resp.Body)
}

这样实现的客户端默认会对服务端的证书进行检验:所以会报错:

$ go run client.go
Get https://localhost:8080: x509: certificate signed by unknown authority

我们可以通过 http.Client 结构体的 Transport 字段来设置客户端的证书校验策略:

func main() {
    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }
    client := &http.Client{Transport: tr}
    resp, err := client.Get("https://localhost:8080")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer resp.Body.Close()
    io.Copy(os.Stdout, resp.Body)
}

这样就可以忽略服务端证书的校验了。

对客户端证书的校验

要对客户端证书进行校验,首先客户端要有证书:

$ openssl genrsa -out client.key 2048
$ openssl req -new -x509 -key client.key -out client.crt -days 3650
$ openssl x509 -req -in client.csr -CA server.crt -CAkey server.key -CAcreateserial -out client.crt -days 3650

然后服务端需要对客户端证书进行校验:

server.go
func main() {
    pool := x509.NewCertPool()
    caCertPath := "ca.crt"
    caCrt, err := ioutil.ReadFile(caCertPath)
    if err != nil {
        log.Fatalf("ReadFile err: %v", err)
    }
    pool.AppendCertsFromPEM(caCrt)
 
    s := &http.Server{
        Addr: ":8080",
        Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            fmt.Fprintf(w, "Hello, Gopher!")
        }),
        TLSConfig: &tls.Config{
            ClientCAs:  pool,
            ClientAuth: tls.RequireAndVerifyClientCert,
        },
    }
    log.Fatal(s.ListenAndServeTLS("server.crt", "server.key"))
}

上面的代码通过将 ClientAuth 字段设置为 tls.RequireAndVerifyClientCert 来要求客户端提供证书,并且对客户端证书进行校验。 ClientCAs 字段是一个 *x509.CertPool 类型的对象,用于保存服务端信任的客户端证书。

客户端需要提供证书:

client.go
func main() {
    pool := x509.NewCertPool()
    caCertPath := "ca.crt"
    caCrt, err := ioutil.ReadFile(caCertPath)
    if err != nil {
        log.Fatalf("ReadFile err: %v", err)
    }
    pool.AppendCertsFromPEM(caCrt)
 
    cert, err := tls.LoadX509KeyPair("client.crt", "client.key")
    if err != nil {
        log.Fatalf("Loadx509KeyPair err: %v", err)
    }
 
    tr := &http.Transport{
        TLSClientConfig: &tls.Config{
            RootCAs:      pool,
            Certificates: []tls.Certificate{cert},
        },
    }
    client := &http.Client{Transport: tr}
    resp, err := client.Get("https://localhost:8080")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer resp.Body.Close()
    io.Copy(os.Stdout, resp.Body)
}

上面的代码通过将 RootCAs 字段设置为 *x509.CertPool 类型的对象,用于保存客户端信任的服务端证书。