Go gRPC 系列:
第一篇入門說過 gRPC 底層是基於 HTTP/2 協議的,HTTP 本身不帶任何加密傳輸功能,基於 SSL 的 HTTPS 協議才是加密傳輸。gRPC 使用了 HTTP/2 協議但是並未使用 HTTPS,即少了加密傳輸的部分。
對於加密傳輸的部分 gRPC 將它抽出來作為一個元件,可以由使用者自由選擇。gRPC 內預設提供了兩種 內建的認證方式:
- 基於 CA 證書的 SSL/TLS 認證方式;
- 基於 Token 的認證方式。
同時也提供了可擴充套件的使用者自定義認證方式。
gRPC 中的連線型別一共有以下 3 種:
- insecure connection:不使用 TLS 加密;
- server-side TLS:僅服務端 TLS 加密;
- mutual TLS:客戶端、服務端都使用 TLS 加密。
我們之前的例項中都是使用 insecure connection:
conn, err := grpc.Dial(":8972", grpc.WithInsecure())
這種方式相當於裸奔的資料在網路上行走,生產環境下這樣使用肯定是不行的。下面我們來說一下基於 TLS 認證方式加密操作。
server-side TLS
服務端 TLS 具體包含以下幾個步驟:
- 製作證書,包含服務端證書和 CA 證書;
- 服務端啟動時載入證書;
- 客戶端連線時使用CA 證書校驗服務端證書有效性。
CA 證書製作:
# 生成.key 私鑰檔案
$ openssl genrsa -out ca.key 2048
# 生成.csr 證書籤名請求檔案
$ openssl req -new -key ca.key -out ca.csr -subj "/C=GB/L=China/O=rickiyang/CN=www.rickiyang.com"
# 自簽名生成.crt 證書檔案
$ openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -subj "/C=GB/L=China/O=rickiyang/CN=www.rickiyang.com"
服務端證書
和生成 CA證書類似,不過最後一步由 CA 證書進行簽名,而不是自簽名。
然後openssl 配置檔案可能位置不同,需要自己修改一下。
首先找到你的 openssl 位置:
$ find / -name "openssl.cnf"
然後生成簽名證書:
# 生成.key 私鑰檔案
$ openssl genrsa -out server.key 2048
# 生成.csr 證書籤名請求檔案
$ openssl req -new -key server.key -out server.csr \
-subj "/C=GB/L=China/O=rickiyang/CN=www.rickiyang.com" \
-reqexts SAN \
-config <(cat /usr/local/etc/openssl@1.1/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.rickiyang.com"))
# 簽名生成.crt 證書檔案
$ openssl x509 -req -days 3650 \
-in server.csr -out server.crt \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-extensions SAN \
-extfile <(cat /usr/local/etc/openssl@1.1/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.rickiyang.com"))
至此會生成如下檔案:
-rw-r--r-- 1 rickiyang staff 1119 6 30 10:32 ca.crt
-rw-r--r-- 1 rickiyang staff 964 6 30 10:32 ca.csr
-rw-r--r-- 1 rickiyang staff 1679 6 30 10:31 ca.key
-rw-r--r-- 1 rickiyang staff 17 6 30 10:34 ca.srl
-rw-r--r-- 1 rickiyang staff 1164 6 30 10:34 server.crt
-rw-r--r-- 1 rickiyang staff 1017 6 30 10:33 server.csr
-rw-r--r-- 1 rickiyang staff 1679 6 30 10:32 server.key
下面我們用到的會有這三個:
ca.crt
server.key
server.crt
下面來看一下如何將加密校驗邏輯加入到程式碼中。相關程式碼在 Github 上,自行下載檢視。
服務端的修改點如下:
- NewServerTLSFromFile 載入證書
- NewServer 時指定 Creds。
func TestGrpcServer(t *testing.T) {
// 監聽本地的8972埠
lis, err := net.Listen("tcp", ":8972")
if err != nil {
fmt.Printf("failed to listen: %v", err)
return
}
// TLS認證
creds, err := credentials.NewServerTLSFromFile("/Users/rickiyang/server.crt", "/Users/rickiyang/server.key")
if err != nil {
grpclog.Fatalf("Failed to generate credentials %v", err)
}
//開啟TLS認證, 註冊攔截器
s := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(LoggingInterceptor)) // 建立gRPC伺服器
pb.RegisterGreeterServer(s, &server{}) // 在gRPC服務端註冊服務
reflection.Register(s) //在給定的gRPC伺服器上註冊伺服器反射服務
// Serve方法在lis上接受傳入連線,為每個連線建立一個ServerTransport和server的goroutine。
// 該goroutine讀取gRPC請求,然後呼叫已註冊的處理程式來響應它們。
err = s.Serve(lis)
if err != nil {
fmt.Printf("failed to serve: %v", err)
return
}
}
同樣服務端使用 TLS 連線,客戶端也需要使用對應的連線方式才能進行傳輸。客戶端程式碼主要修改點:
- NewClientTLSFromFile 指定使用 CA 證書來校驗服務端的證書有效性。注意:第二個引數域名就是前面生成服務端證書時指定的CN引數。
- 建立連線時 指定建立安全連線 WithTransportCredentials。
對應程式碼邏輯如下:
func TestGrpcClient(t *testing.T) {
// TLS連線
creds, err := credentials.NewClientTLSFromFile("/Users/rickiyang2/ca.crt", "www.rickiyang.com")
if err != nil {
grpclog.Fatalf("Failed to create TLS credentials %v", err)
}
// 連線伺服器
conn, err := grpc.Dial(":8972", grpc.WithTransportCredentials(creds))
if err != nil {
fmt.Printf("faild to connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
// 呼叫服務端的SayHello
r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: "CN"})
if err != nil {
fmt.Printf("could not greet: %v", err)
}
fmt.Printf("Greeting: %s !\n", r.Message)
}
mutual TLS
server-side TLS 中雖然服務端使用了證書,但是客戶端卻沒有使用證書,本章節會給客戶端也生成一個證書,並完成 mutual TLS。
生成客戶端證書和生成服務端證書沒有什麼不同:
# 生成.key 私鑰檔案
openssl genrsa -out client.key 2048
# 生成.csr 證書籤名請求檔案
openssl req -new -key client.key -out client.csr \
-subj "/C=GB/L=China/O=lixd/CN=www.rickiyang.com" \
-reqexts SAN \
-config <(cat /usr/local/etc/openssl@1.1/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.rickiyang.com"))
# 簽名生成.crt 證書檔案
openssl x509 -req -days 3650 \
-in client.csr -out client.crt \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-extensions SAN \
-extfile <(cat /usr/local/etc/openssl@1.1/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.rickiyang.com"))
執行上面的命令之後我們會得到這兩個重要的檔案:
client.crt
client.key
下面就是在程式碼中去引用這些檔案,mutual TLS 配置客戶端和服務端都需要修改,相關程式碼點選檢視。
具體改動如下:
服務端:
func TestGrpcServer(t *testing.T) {
// 從證書相關檔案中讀取和解析資訊,得到證書公鑰、金鑰對
certificate, err := tls.LoadX509KeyPair(data.Path("/Users/rickiyang2/server.crt"), data.Path("/Users/rickiyang2/server.key"))
if err != nil {
fmt.Errorf("err, %v", err)
}
// 建立CertPool,後續就用池裡的證書來校驗客戶端證書有效性
// 所以如果有多個客戶端 可以給每個客戶端使用不同的 CA 證書,來實現分別校驗的目的
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile(data.Path("/Users/rickiyang2/ca.crt"))
if err != nil {
fmt.Errorf("err, %v", err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
fmt.Errorf("failed to append certs")
}
// 構建基於 TLS 的 TransportCredentials
creds := credentials.NewTLS(&tls.Config{
// 設定證書鏈,允許包含一個或多個
Certificates: []tls.Certificate{certificate},
// 要求必須校驗客戶端的證書 可以根據實際情況選用其他引數
ClientAuth: tls.RequireAndVerifyClientCert, // NOTE: this is optional!
// 設定根證書的集合,校驗方式使用 ClientAuth 中設定的模式
ClientCAs: certPool,
})
//開啟TLS認證, 註冊攔截器
s := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(LoggingInterceptor)) // 建立gRPC伺服器
pb.RegisterGreeterServer(s, &server{}) // 在gRPC服務端註冊服務
// 監聽本地的8972埠
lis, err := net.Listen("tcp", ":8972")
if err != nil {
fmt.Printf("failed to listen: %v", err)
return
}
reflection.Register(s) //在給定的gRPC伺服器上註冊伺服器反射服務
// Serve方法在lis上接受傳入連線,為每個連線建立一個ServerTransport和server的goroutine。
// 該goroutine讀取gRPC請求,然後呼叫已註冊的處理程式來響應它們。
err = s.Serve(lis)
if err != nil {
fmt.Printf("failed to serve: %v", err)
return
}
}
客戶端:
func TestGrpcClient(t *testing.T) {
// 載入客戶端證書
certificate, err := tls.LoadX509KeyPair(data.Path("/Users/rickiyang2/client.crt"), data.Path("/Users/rickiyang2/client.key"))
if err != nil {
fmt.Errorf("err, %v", err)
}
// 構建CertPool以校驗服務端證書有效性
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile(data.Path("/Users/rickiyang2/ca.crt"))
if err != nil {
fmt.Errorf("err, %v", err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
fmt.Errorf("failed to append ca certs")
}
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{certificate},
ServerName: "www.rickiyang.com", // NOTE: this is required!
RootCAs: certPool,
})
// 連線伺服器
conn, err := grpc.Dial(":8972", grpc.WithTransportCredentials(creds))
if err != nil {
fmt.Printf("faild to connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
// 呼叫服務端的SayHello
r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: "CN"})
if err != nil {
fmt.Printf("could not greet: %v", err)
}
fmt.Printf("Greeting: %s !\n", r.Message)
}
本篇只介紹 SSL/TLS 認證相關的方式,生成證書相關操作本文是在 Mac 上操作,不同系統可能會有不一樣的環境問題, 如果出現問題按照相關提示排除錯誤。下一篇我們繼續介紹 Token 認證和自定義認證方式。