Go gRPC進階-TLS認證+自定義方法認證(七)

煙花易冷人憔悴發表於2020-04-16

前言

前面篇章的gRPC都是明文傳輸的,容易被篡改資料。本章將介紹如何為gRPC新增安全機制,包括TLS證書認證和Token認證。

TLS證書認證

什麼是TLS

TLS(Transport Layer Security,安全傳輸層),TLS是建立在傳輸層TCP協議之上的協議,服務於應用層,它的前身是SSL(Secure Socket Layer,安全套接字層),它實現了將應用層的報文進行加密後再交由TCP進行傳輸的功能。

TLS的作用

TLS協議主要解決如下三個網路安全問題。

  • 保密(message privacy),保密通過加密encryption實現,所有資訊都加密傳輸,第三方無法嗅探;
  • 完整性(message integrity),通過MAC校驗機制,一旦被篡改,通訊雙方會立刻發現;
  • 認證(mutual authentication),雙方認證,雙方都可以配備證書,防止身份被冒充;

生成私鑰

生成RSA私鑰:openssl genrsa -out server.key 2048

生成RSA私鑰,命令的最後一個引數,將指定生成金鑰的位數,如果沒有指定,預設512

生成ECC私鑰:openssl ecparam -genkey -name secp384r1 -out server.key

生成ECC私鑰,命令為橢圓曲線金鑰引數生成及操作,本文中ECC曲線選擇的是secp384r1

生成公鑰

openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650

openssl req:生成自簽名證書,-new指生成證書請求、-sha256指使用sha256加密、-key指定私鑰檔案、-x509指輸出證書、-days 3650為有效期

此後則輸入證書擁有者資訊

Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:XxXx
Locality Name (eg, city) []:XxXx
Organization Name (eg, company) [Internet Widgits Pty Ltd]:XX Co. Ltd
Organizational Unit Name (eg, section) []:Dev
Common Name (e.g. server FQDN or YOUR name) []:go-grpc-example
Email Address []:xxx@xxx.com

服務端構建TLS證書並認證

func main() {
	// 監聽本地埠
	listener, err := net.Listen(Network, Address)
	if err != nil {
		log.Fatalf("net.Listen err: %v", err)
	}
	// 從輸入證書檔案和金鑰檔案為服務端構造TLS憑證
	creds, err := credentials.NewServerTLSFromFile("../pkg/tls/server.pem", "../pkg/tls/server.key")
	if err != nil {
		log.Fatalf("Failed to generate credentials %v", err)
	}
	// 新建gRPC伺服器例項,並開啟TLS認證
	grpcServer := grpc.NewServer(grpc.Creds(creds))
	// 在gRPC伺服器註冊我們的服務
	pb.RegisterSimpleServer(grpcServer, &SimpleService{})
	log.Println(Address + " net.Listing whth TLS and token...")
	//用伺服器 Serve() 方法以及我們的埠資訊區實現阻塞等待,直到程式被殺死或者 Stop() 被呼叫
	err = grpcServer.Serve(listener)
	if err != nil {
		log.Fatalf("grpcServer.Serve err: %v", err)
	}
}
  • credentials.NewServerTLSFromFile:從輸入證書檔案和金鑰檔案為服務端構造TLS憑證
  • grpc.Creds:返回一個ServerOption,用於設定伺服器連線的憑證。

完整server.go程式碼

客戶端配置TLS連線

var grpcClient pb.SimpleClient

func main() {
	//從輸入的證書檔案中為客戶端構造TLS憑證
	creds, err := credentials.NewClientTLSFromFile("../pkg/tls/server.pem", "go-grpc-example")
	if err != nil {
		log.Fatalf("Failed to create TLS credentials %v", err)
	}
	// 連線伺服器
	conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds))
	if err != nil {
		log.Fatalf("net.Connect err: %v", err)
	}
	defer conn.Close()

	// 建立gRPC連線
	grpcClient = pb.NewSimpleClient(conn)
}
  • credentials.NewClientTLSFromFile:從輸入的證書檔案中為客戶端構造TLS憑證。
  • grpc.WithTransportCredentials:配置連線級別的安全憑證(例如,TLS/SSL),返回一個DialOption,用於連線伺服器。

完整client.go程式碼

到這裡,已經完成TLS證書認證了,gRPC傳輸不再是明文傳輸。此外,新增自定義的驗證方法能使gRPC相對更安全。下面以Token認證為例,介紹gRPC如何新增自定義驗證方法。

Token認證

客戶端發請求時,新增Token到上下文context.Context中,伺服器接收到請求,先從上下文中獲取Token驗證,驗證通過才進行下一步處理。

客戶端請求新增Token到上下文中

type PerRPCCredentials interface {
    GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
    RequireTransportSecurity() bool
}

gRPC 中預設定義了 PerRPCCredentials,是提供用於自定義認證的介面,它的作用是將所需的安全認證資訊新增到每個RPC方法的上下文中。其包含 2 個方法:

  • GetRequestMetadata:獲取當前請求認證所需的後設資料
  • RequireTransportSecurity:是否需要基於 TLS 認證進行安全傳輸

接下來我們實現這兩個方法

// Token token認證
type Token struct {
	AppID     string
	AppSecret string
}

// GetRequestMetadata 獲取當前請求認證所需的後設資料(metadata)
func (t *Token) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{"app_id": t.AppID, "app_secret": t.AppSecret}, nil
}

// RequireTransportSecurity 是否需要基於 TLS 認證進行安全傳輸
func (t *Token) RequireTransportSecurity() bool {
	return true
}

然後再客戶端中呼叫Dial時新增自定義驗證方法進去

//構建Token
	token := auth.Token{
		AppID:     "grpc_token",
		AppSecret: "123456",
	}
	// 連線伺服器
	conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(&token))

完整client.go程式碼

服務端驗證Token

首先需要從上下文中獲取後設資料,然後從後設資料中解析Token進行驗證

// Check 驗證token
func Check(ctx context.Context) error {
	//從上下文中獲取後設資料
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return status.Errorf(codes.Unauthenticated, "獲取Token失敗")
	}
	var (
		appID     string
		appSecret string
	)
	if value, ok := md["app_id"]; ok {
		appID = value[0]
	}
	if value, ok := md["app_secret"]; ok {
		appSecret = value[0]
	}
	if appID != "grpc_token" || appSecret != "123456" {
		return status.Errorf(codes.Unauthenticated, "Token無效: app_id=%s, app_secret=%s", appID, appSecret)
	}
	return nil
}

// Route 實現Route方法
func (s *SimpleService) Route(ctx context.Context, req *pb.SimpleRequest) (*pb.SimpleResponse, error) {
    //檢測Token是否有效
	if err := Check(ctx); err != nil {
		return nil, err
	}
	res := pb.SimpleResponse{
		Code:  200,
		Value: "hello " + req.Data,
	}
	return &res, nil
}
  • metadata.FromIncomingContext:從上下文中獲取後設資料

完整server.go程式碼

服務端程式碼中,每個服務的方法都需要新增Check(ctx)來驗證Token,這樣十分麻煩。gRPC攔截器,能很好地解決這個問題。gRPC攔截器功能類似中介軟體,攔截器收到請求後,先進行一些操作,然後才進入服務的程式碼處理。

服務端新增攔截器

func main() {
	// 監聽本地埠
	listener, err := net.Listen(Network, Address)
	if err != nil {
		log.Fatalf("net.Listen err: %v", err)
	}
	// 從輸入證書檔案和金鑰檔案為服務端構造TLS憑證
	creds, err := credentials.NewServerTLSFromFile("../pkg/tls/server.pem", "../pkg/tls/server.key")
	if err != nil {
		log.Fatalf("Failed to generate credentials %v", err)
	}
	//普通方法:一元攔截器(grpc.UnaryInterceptor)
	var interceptor grpc.UnaryServerInterceptor
	interceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		//攔截普通方法請求,驗證Token
		err = Check(ctx)
		if err != nil {
			return
		}
		// 繼續處理請求
		return handler(ctx, req)
	}
	// 新建gRPC伺服器例項,並開啟TLS認證和Token認證
	grpcServer := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(interceptor))
	// 在gRPC伺服器註冊我們的服務
	pb.RegisterSimpleServer(grpcServer, &SimpleService{})
	log.Println(Address + " net.Listing whth TLS and token...")
	//用伺服器 Serve() 方法以及我們的埠資訊區實現阻塞等待,直到程式被殺死或者 Stop() 被呼叫
	err = grpcServer.Serve(listener)
	if err != nil {
		log.Fatalf("grpcServer.Serve err: %v", err)
	}
}
  • grpc.UnaryServerInterceptor:為一元攔截器,只會攔截簡單RPC方法。流式RPC方法需要使用流式攔截器grpc.StreamInterceptor進行攔截。

客戶端發起請求,當Token不正確時候,會返回

Call Route err: rpc error: code = Unauthenticated desc = Token無效: app_id=grpc_token, app_secret=12345

總結

本篇介紹如何為gRPC新增TLS證書認證和自定義認證,從而讓gRPC更安全。新增gRPC攔截器,從而省略在每個方法前新增Token檢測程式碼,使程式碼更簡潔。

教程原始碼地址:https://github.com/Bingjian-Zhu/go-grpc-example

參考:

相關文章