個人網站:linzyblog.netlify.app/
示例程式碼已經上傳到github:點選跳轉
gRPC官方文件:點選跳轉
先前的例子中 gRPC Client/Server 都是明文傳輸的,在明文通訊的情況下,你的請求就是裸奔的,有可能被第三方惡意篡改或者偽造為“非法”的資料。
我們抓個包檢視一下:
是明文傳輸,後面我們開始gRPC透過 TLS 證照建立安全連線,讓資料能夠加密處理,包括證照製作和CA簽名校驗等。
傳輸層安全 (TLS) 對透過 Internet 傳送的資料進行加密,以確保竊聽者和駭客無法看到您傳輸的內容,這對於密碼、信用卡號和個人通訊等私人和敏感資訊特別有用。
1、什麼是TLS?
傳輸層安全 (TLS) 是一種 Internet 工程任務組 ( IETF ) 標準協議,可在兩個通訊計算機應用程式之間提供身份驗證、隱私和資料完整性。它是當今使用最廣泛部署的安全協議,最適合需要透過網路安全交換資料的 Web 瀏覽器和其他應用程式。這包括 Web 瀏覽會話、檔案傳輸、虛擬專用網路 (VPN) 連線、遠端桌面會話和 IP 語音 (VoIP)。最近,TLS 被整合到包括 5G 在內的現代蜂窩傳輸技術中,以保護整個無線電接入網路 ( RAN ) 的核心網路功能。
2、TLS的工作流程
TLS 使用客戶端-伺服器握手機制來建立加密和安全的連線,並確保通訊的真實性。
- 通訊裝置交換加密功能。
- 使用數字證照進行身份驗證過程以幫助證明伺服器是它聲稱的實體。
- 發生會話金鑰交換。在此過程中,客戶端和伺服器必須就金鑰達成一致,以建立安全會話確實在客戶端和伺服器之間的事實——而不是在中間試圖劫持會話的東西。
1、概述
gRPC建立在HTTP/2協議之上,對TLS提供了很好的支援。當不需要證照認證時,可透過grpc.WithInsecure()選項跳過了對伺服器證照的驗證,沒有啟用證照的gRPC服務和客戶端進行的是明文通訊,資訊面臨被任何第三方監聽的風險。為了保證gRPC通訊不被第三方監聽、篡改或偽造,可以對伺服器啟動TLS加密特性。
gRPC 內建了以下 encryption 機制:
- SSL / TLS:透過證照進行資料加密;
- ALTS:Google開發的一種雙向身份驗證和傳輸加密系統。
- 只有執行在 Google Cloud Platform 才可用,一般不用考慮。
2、gRPC 加密型別
- 1)insecure connection:不使用TLS加密
- 2)server-side TLS:僅服務端TLS加密
- 3)mutual TLS:客戶端、服務端都使用TLS加密
我們前面的例子都是明文傳輸的,使用的都是 insecure connection,透過指定 WithInsecure option 來建立 insecure connection,不建議在生產環境使用。
後面我們瞭解如何使用 TLS 來建立安全連線。
3、server-side TLS
1)流程
服務端 TLS 具體包含以下幾個步驟:
- 製作證照,包含服務端證照和 CA 證照;
- 服務端啟動時載入證照;
- 客戶端連線時使用CA 證照校驗服務端證照有效性。
也可以不使用 CA證照,即服務端證照自簽名。
2)什麼是CA?CA證照又是什麼?
- CA是Certificate Authority的縮寫,也叫
“證照授權中心”
。它是負責管理和簽發證照的第三方機構,作用是檢查證照持有者身份的合法性,並簽發證照,以防證照被偽造或篡改。
CA實際上是一個機構,負責“證件”印製核發。就像負責頒發身份證的公安局、負責發放行駛證、駕駛證的車管所。
CA 證照就是CA頒發的證照。我們常聽到的數字證照就是CA證照,CA證照包含資訊有:
證照擁有者的身份資訊,CA機構的簽名,公鑰和私鑰。
- 身份資訊: 用於證明證照持有者的身份
- CA機構的簽名: 用於保證身份的真實性
- 公鑰和私鑰: 用於通訊過程中加解密,從而保證通訊資訊的安全性
3)什麼是SAN?
SAN(Subject Alternative Name)是 SSL 標準 x509 中定義的一個擴充套件。使用了 SAN 欄位的 SSL 證照,可以擴充套件此證照支援的域名,使得一個證照可以支援多個不同域名的解析。
我們在用go 1.15版本以上,用gRPC透過TLS建立安全連線時,會出現證照報錯問題:
panic: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate
is not valid for any names, but wanted to match localhost"
造成這個panic的原因是從go 1.15 版本開始廢棄 CommonName,我們沒有使用官方推薦的 SAN 證照(預設是沒有開啟SAN擴充套件)而出現的錯誤,導致客戶端和服務端無法建立連線。
4)目錄結構
go-grpc-example
├── client
│ └──TLS_client
│ │ └──client.go
├── conf
│ └──ca.conf
│ └──server.conf
├── proto
│ └──search
│ │ └──search.proto
├── server
│ └──TLS_server
│ │ └──server.go
├── Makefile
5)生成CA根證照
在ca.conf裡寫入內容如下:
[ req ]
default_bits = 4096
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
countryName = GB
countryName_default = CN
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = ZheJiang
localityName = Locality Name (eg, city)
localityName_default = HuZhou
organizationName = Organization Name (eg, company)
organizationName_default = Step
commonName = linzyblog.netlify.app
commonName_max = 64
commonName_default = linzyblog.netlify.app
- 生成ca私鑰,得到ca.key
openssl genrsa -out ca.key 4096
openssl genrsa:生成RSA私鑰,命令的最後一個引數,將指定生成金鑰的位數,如果沒有指定,預設512
- 生成ca證照籤發請求,得到ca.csr
$ openssl req -new -sha256 -out ca.csr -key ca.key -config ca.conf
GB [CN]:
State or Province Name (full name) [ZheJiang]:
Locality Name (eg, city) [HuZhou]:
Organization Name (eg, company) [Step]:
linzyblog.netlify.app [linzyblog.netlify.app]:
這裡一直回車就好了
openssl req:生成自簽名證照,-new指生成證照請求、-sha256指使用sha256加密、-key指定私鑰檔案、-x509指輸出證照、-days 3650為有效期,此後則輸入證照擁有者資訊
- 生成ca根證照,得到ca.crt
openssl x509 -req -days 3650 -in ca.csr -signkey ca.key -out ca.crt
6)生成終端使用者證照
在server.conf寫入以下內容:
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = CN
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = ZheJiang
localityName = Locality Name (eg, city)
localityName_default = HuZhou
organizationName = Organization Name (eg, company)
organizationName_default = Step
commonName = CommonName (e.g. server FQDN or YOUR name)
commonName_max = 64
commonName_default = linzyblog.netlify.app
[ req_ext ]
subjectAltName = @alt_names
[alt_names]
DNS.1 = go-grpc-example(這裡很重要,客戶端需要此欄位做匹配)
IP = 127.0.0.1
- 生成私鑰,得到server.key
openssl genrsa -out server.key 4096
- 生成證照籤發請求,得到server.csr
openssl req -new -sha256 -out server.csr -key server.key -config server.conf
這裡也一直回車就好。
- 用CA證照生成終端使用者證照,得到server.crt
openssl x509 -req -days 3650 -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.pem -extensions req_ext -extfile server.conf
7)server
const PORT = "8888"
func main() {
// 根據服務端輸入的證照檔案和金鑰構造 TLS 憑證
c, err := credentials.NewServerTLSFromFile("./conf/server.pem", "./conf/server.key")
if err != nil {
log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
}
// 返回一個 ServerOption,用於設定伺服器連線的憑據。
// 用於 grpc.NewServer(opt ...ServerOption) 為 gRPC Server 設定連線選項
lis, err := net.Listen("tcp", ":"+PORT) //建立 Listen,監聽 TCP 埠
if err != nil {
log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
}
search.RegisterSearchServiceServer(s, &service{})
s.Serve(lis)
}
8)client
const PORT = "8888"
func main() {
// 根據客戶端輸入的證照檔案和金鑰構造 TLS 憑證。
// 第二個引數 serverNameOverride 為服務名稱。
c, err := credentials.NewClientTLSFromFile("./conf/server.pem", "go-grpc-example")
if err != nil {
log.Fatalf("credentials.NewClientTLSFromFile err: %v", err)
}
// 返回一個配置連線的 DialOption 選項。
// 用於 grpc.Dial(target string, opts ...DialOption) 設定連線選項
conn, err := grpc.Dial(":"+PORT, grpc.WithTransportCredentials(c))
if err != nil {
log.Fatalf("grpc.Dial err: %v", err)
}
defer conn.Close()
client := pb.NewSearchServiceClient(conn)
resp, err := client.Search(context.Background(), &pb.SearchRequest{
Request: "gRPC",
})
if err != nil {
log.Fatalf("client.Search err: %v", err)
}
log.Printf("resp: %s", resp.GetResponse())
}
8)啟動 & 請求
# 啟動服務端
$ go run server.go
API server listening at: 127.0.0.1:53981
# 啟動客戶端
$ go run client.go
API server listening at: 127.0.0.1:54328
2022/11/03 19:35:10 resp: gRPC Server
抓個包再看看
4、mutual TLS
1)生成服務端證照
新增server.conf
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = CN
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = ZheJiang
localityName = Locality Name (eg, city)
localityName_default = HuZhou
organizationName = Organization Name (eg, company)
organizationName_default = Step
commonName = CommonName (e.g. server FQDN or YOUR name)
commonName_max = 64
commonName_default = linzyblog.netlify.app
[ req_ext ]
subjectAltName = @alt_names
[alt_names]
DNS.1 = go-grpc-example(這裡很重要,客戶端需要此欄位做匹配)
IP = 127.0.0.1
// 1. 生成私鑰,得到server.key
openssl genrsa -out server.key 2048
//2. 生成證照籤發請求,得到server.csr
openssl req -new -sha256 -out server.csr -key server.key -config server.conf
//3. 用CA證照生成終端使用者證照,得到server.crt
openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -in server.csr -out se
rver.crt -extensions req_ext -extfile server.conf
2)生成客戶端證照
// 1. 生成私鑰,得到client.key
openssl genrsa -out client.key 2048
//2. 生成證照籤發請求,得到client.csr
openssl req -new -key client.key -out client.csr
//3. 用CA證照生成客戶端證照,得到client.crt
openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -in client.csr -out client.crt
3)整理目錄
conf
├── ca.conf
├── ca.crt
├── ca.csr
├── ca.key
├── client
│ ├── client.csr
│ ├── client.key
│ └── client.pem
├── server
│ ├── server.conf
| └── server.crt
│ ├── server.csr
| ├── server.key
└─server_side_TLS
4)server
const PORT = "8888"
func main() {
// 公鑰中讀取和解析公鑰/私鑰對
cert, err := tls.LoadX509KeyPair("./conf/server/server.crt", "./conf/server/server.key")
if err != nil {
fmt.Println("LoadX509KeyPair error", err)
return
}
// 建立一組根證照
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("./conf/ca.crt")
if err != nil {
fmt.Println("read ca pem error ", err)
return
}
// 解析證照
if ok := certPool.AppendCertsFromPEM(ca); !ok {
fmt.Println("AppendCertsFromPEM error ")
return
}
c := credentials.NewTLS(&tls.Config{
//設定證照鏈,允許包含一個或多個
Certificates: []tls.Certificate{cert},
//要求必須校驗客戶端的證照
ClientAuth: tls.RequireAndVerifyClientCert,
//設定根證照的集合,校驗方式使用ClientAuth設定的模式
ClientCAs: certPool,
})
s := grpc.NewServer(grpc.Creds(c))
lis, err := net.Listen("tcp", ":"+PORT) //建立 Listen,監聽 TCP 埠
if err != nil {
log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
}
//將 SearchService(其包含需要被呼叫的服務端介面)註冊到 gRPC Server 的內部註冊中心。
//這樣可以在接受到請求時,透過內部的服務發現,發現該服務端介面並轉接進行邏輯處理
search.RegisterSearchServiceServer(s, &service{})
//gRPC Server 開始 lis.Accept,直到 Stop 或 GracefulStop
s.Serve(lis)
}
5)client
const PORT = "8888"
func main() {
// 公鑰中讀取和解析公鑰/私鑰對
cert, err := tls.LoadX509KeyPair("./conf/client/client.crt", "./conf/client/client.key")
if err != nil {
fmt.Println("LoadX509KeyPair error ", err)
return
}
// 建立一組根證照
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("./conf/ca.crt")
if err != nil {
fmt.Println("ReadFile ca.crt error ", err)
return
}
// 解析證照
if ok := certPool.AppendCertsFromPEM(ca); !ok {
fmt.Println("certPool.AppendCertsFromPEM error ")
return
}
c := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: "go-grpc-example",
RootCAs: certPool,
})
conn, err := grpc.Dial(":"+PORT, grpc.WithTransportCredentials(c))
if err != nil {
log.Fatalf("grpc.Dial err: %v", err)
}
defer conn.Close()
client := pb.NewSearchServiceClient(conn)
resp, err := client.Search(context.Background(), &pb.SearchRequest{
Request: "gRPC",
})
if err != nil {
log.Fatalf("client.Search err: %v", err)
}
log.Printf("resp: %s", resp.GetResponse())
}
6)啟動 & 請求
# 啟動服務端
$ go run server.go
API server listening at: 127.0.0.1:56036
# 啟動客戶端
$ go run client.go
API server listening at: 127.0.0.1:56364
2022/11/03 20:21:55 resp: gRPC Server
# 更改ServerName為linzy
$ go run client.go
API server listening at: 127.0.0.1:56424
2022/11/03 20:23:17 client.Search err: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: cer
tificate is valid for go-grpc-example, not linzy"
抓個包看看
本作品採用《CC 協議》,轉載必須註明作者和本文連結