[gRPC]來聊一聊gRPC的認證

小魔童哪吒發表於2021-05-27
[TOC]

我們再來回顧一下gRPC的基本結構

gRPC 是一個典型的C/S模型,需要開發客戶端 和 服務端,客戶端與服務端需要達成協議,使用某一個確認的傳輸協議來傳輸資料,gRPC通常預設是使用protobuf來作為傳輸協議,當然也是可以使用其他自定義的。

那麼,客戶端與服務端要通訊之前,客戶端如何知道自己的資料是發給哪一個明確的服務端呢?反過來,服務端是不是也需要有一種方式來弄個清楚自己的資料要返回給誰呢?

那麼就不得不提gRPC的認證

認證方式

此處說到的認證,不是使用者的身份認證而是指多個server 和 多個client之間,如何識別對方是誰,並且可以安全的進行資料傳輸

  • SSL/TLS認證方式(採用http2協議)
  • 基於Token的認證方式(基於安全連線)
  • 不採用任何措施的連線,這是不安全的連線(預設採用http1)
  • 自定義的身份認證,gRPC提供了介面用於擴充套件自定義認證方式

今天就和大家分享一下 SSL/TLS認證方式基於Token的認證方式 ,這裡再來回顧一下上一篇講到的

gRPC訊息傳輸的四種型別

  • 請求-響應式
  • 服務端流式訊息,客戶端請求一次,服務端會一一系列的資料,即資料流
  • 客戶端流式訊息,客戶端用資料流請求,服務端做響應
  • 雙向流的方式,即雙方都是流式資料

簡單的例子:

service Example{
    rpc ReqAndRsp(Req) returns (Response)
    rpc ReqAndStream(Req) returns (Stream Response)
    rpc StreamAndRsp(Stream Request) returns (Response)
    rpc BidStream(Stream Request) returns (Stream response)
}

SSL/TLS認證方式

那麼什麼是SSL/TLS?

TLS(Transport Layer Security) 是 SSL(Secure Socket Layer) 的後續版本,它們是用於在網際網路兩臺計算機之間用於身份驗證和加密的一種協議。

基於SSL/TLS的通道加密是gRPC常用的手段,那麼一般我們都是如何運用他的,他的架構一般會是啥樣的?

GRPC 預設是基於HTTP/2的TLS 對客戶端和服務端交換的所有資料進行加密傳輸的

那麼HTTP 2 預設就有加密嗎?

HTTP 2 協議預設是沒有加密的,它只是預先定義好了TLS的輪廓,是TLS保證安全性,TLS做的加密

HTTP 2 有啥特性?

這裡簡單說一下,HTTP 2 較之前的版本有如下4個重要的變化:

  • 二進位制分幀

將所有傳輸的資訊分割為更小的訊息和幀,並對它們採用二進位制格式的編碼

  • 多路io複用

在共享TCP連結的基礎上同時傳送請求和響應,http訊息被分解為獨立的幀,亂序傳送,服務端根據識別符號和首部將訊息重新組裝起來

  • 頭部壓縮
  • 伺服器推送 server push

伺服器可以額外的向客戶端推送資源,而無需客戶端明確的請求

SSL/TLS加密的基本做法是啥?

SSL/TLS 通過將稱為X.509 證照的數字文件將網站和公司的實體資訊繫結到加密金鑰來進行工作。

每一個金鑰對(key pairs)都有一個私有金鑰(private key)公有金鑰(public key),私有金鑰是獨有的,一般位於伺服器上,用於解密由公共金鑰加密過的資訊;

公有金鑰是公有的,與伺服器進行互動的每個人都可以持有公有金鑰,用公鑰加密的資訊只能由私有金鑰來解密。

簡單來說就是

SSL/TLS協議,客戶端向伺服器端索要公鑰,然後用公鑰加密資訊,伺服器收到密文後,用自己的私鑰解密。

SSL/TLS協議提供啥服務呢?

  • 認證使用者和伺服器,確保資料傳送到正確的客戶端和伺服器;

  • 加密資料以防止資料中途被竊取;

  • 維護資料的完整性,確保資料在傳輸過程中不被改變;

SSL/TLS協議提供的安全通道有哪些特性呢?

  • 機密性:SSL協議使用金鑰加密通訊資料。

  • 可靠性:伺服器和客戶端都會被認證,客戶端的認證是可選的。

  • 完整性:SSL協議會對傳送的資料進行完整性檢查。

說了這麼多,我們來演示一下gRPC的 SSL/TLS協議如何實踐吧

必要環境搭建

OpenSSL安裝

解壓原始碼

tar xzvf openssl-3.0.0-alpha17.tar.gz

進入原始碼目錄

cd openssl-3.0.0-alpha17

編譯和安裝

./Configure
make
sudo make install

安裝結束後,使用 openssl version 檢視openssl 版本號

若報錯如下資訊:

openssl: error while loading shared libraries: libssl.so.3: cannot open shared object file: No such file or directory

通常情況下只需要建一個軟連結,連結過去即可

sudo ln -s /usr/local/lib/libssl.so.3 /usr/lib/libssl.so.3
sudo ln -s /usr/local/lib/libcrypto.so.3 /usr/lib/libcrypto.so.3

TLS證照製作

如下是一張生成key的簡單方式,不適用go1.15之後的版本,go1.15已經棄用了 x509

# 製作私鑰
openssl genrsa -out server.key 2048

openssl ecparam -genkey -name secp384r1 -out server.key

# 自簽名公鑰 ,設定有效時間365 天
openssl req -new -x509 -sha256 -key server.key -out server.pem -days 365

一個DEMO

開始使用上述生成的key

.
├── client
│ ├── keys
│ │ ├── server.key
│ │ └── server.pem
│ ├── main.go
│ ├── myclient
│ └── protoc
│ └── hi
│ ├── hi.pb.go
│ └── hi.proto
└── server
├── keys
│ ├── server.key
│ └── server.pem
├── main.go
├── myserver
└── protoc
└── hi
├── hi.pb.go
└── hi.proto

hi.proto

將proto編譯成pb.go檔案

protoc --go_out=plugins=grpc:. hi.proto

syntax = "proto3"; // 指定proto版本
package hi;     // 指定預設包名

// 指定golang包名
option go_package = "hi";

// 定義Hi服務
service Hi {
// 定義SayHi方法
rpc SayHi(HiRequest) returns (HiResponse) {}
}

// HiRequest 請求結構
message HiRequest {
string name = 1;
}

// HiResponse 響應結構
message HiResponse {
string message = 1;
}

server/main.go

package main

import (
   "fmt"
   "log"
   "net"

   pb "myserver/protoc/hi"

   "golang.org/x/net/context"
   "google.golang.org/grpc"
   "google.golang.org/grpc/credentials" // 引入grpc認證包
)

const (
   // Address gRPC服務地址
   Address = "127.0.0.1:9999"
)

// 定義HiService並實現約定的介面
type HiService struct{}

// HiService Hello服務
var HiSer = HiService{}

// SayHi 實現Hi服務介面
func (h HiService) SayHi(ctx context.Context, in *pb.HiRequest) (*pb.HiResponse, error) {
   resp := new(pb.HiResponse)
   resp.Message = fmt.Sprintf("Hi %s.", in.Name)

   return resp, nil
}

func main() {
   log.SetFlags(log.Ltime | log.Llongfile)
   listen, err := net.Listen("tcp", Address)
   if err != nil {
      log.Panicf("Failed to listen: %v", err)
   }

   // TLS認證
   creds, err := credentials.NewServerTLSFromFile("./keys/server.pem", "./keys/server.key")
   if err != nil {
      log.Panicf("Failed to generate credentials %v", err)
   }

   // 例項化grpc Server, 並開啟TLS認證
   s := grpc.NewServer(grpc.Creds(creds))

   // 註冊HelloService
   pb.RegisterHiServer(s, HiSer)

   log.Println("Listen on " + Address + " with TLS")

   s.Serve(listen)
}

client/main.go

package main

import (
   "log"
   pb "myclient/protoc/hi" // 引入proto包

   "golang.org/x/net/context"
   "google.golang.org/grpc"
   "google.golang.org/grpc/credentials" // 引入grpc認證包
   "google.golang.org/grpc/grpclog"
)

const (
   // Address gRPC服務地址
   Address = "127.0.0.1:9999"
)

func main() {
   log.SetFlags(log.Ltime | log.Llongfile)
   // TLS連線  記得把xxx改成你寫的伺服器地址
   creds, err := credentials.NewClientTLSFromFile("./keys/server.pem", "xiaomotong")
   if err != nil {
      log.Panicf("Failed to create TLS credentials %v", err)
   }

   conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds))
   if err != nil {
      grpclog.Fatalln(err)
   }
   defer conn.Close()

   // 初始化客戶端
   c := pb.NewHiClient(conn)

   // 呼叫方法
   req := &pb.HiRequest{Name: "gRPC"}
   res, err := c.SayHi(context.Background(), req)
   if err != nil {
      log.Panicln(err)
   }

   log.Println(res.Message)
}

如果你的go是在1.15版本以上的,請重新生成key,參考文件 openssl證照生成 記錄(GO1.15版本以上)

生成後,放到專案響應的位置,編譯執行即可效果如下:

服務端:

客戶端:

基於Token的認證方式

將上述TLS實踐DEMO進行優化,加上Token機制,需要做如下2點改動

  • 客戶端,實現credentials包的介面,GetRequestMetadataRequireTransportSecurity
  • 服務端在 metadata 驗證客戶端的資訊

程式碼結構與上一個DEMO一致,分別在客戶端和服務端的程式碼中加入相應的邏輯即可。

如下是credentials包中待實現介面:

又一個DEMO

client/main.go

新增如下邏輯即可

package main

import (
    "log"
    pb "myclient/protoc/hi" // 引入proto包

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials" // 引入grpc認證包
    "google.golang.org/grpc/grpclog"
)

const (
    // Address gRPC服務地址
    Address = "127.0.0.1:9999"
)

// ====== 新增的邏輯  START ==============
var IsTls = true

// myCredential 自定義認證
type myCredential struct{}

// GetRequestMetadata 實現自定義認證介面
func (c myCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{
        "appid":  "myappid",
        "appkey": "mykey",
    }, nil
}

// RequireTransportSecurity 自定義認證是否開啟TLS
func (c myCredential) RequireTransportSecurity() bool {
    return IsTls
}
// ====== 新增的邏輯  END  ==============

func main() {
    log.SetFlags(log.Ltime | log.Llongfile)


// ====== 新增的邏輯  START ==============
    var err error
    var opts []grpc.DialOption

    if IsTls {
        //開啟tls 走tls認證
        creds, err := credentials.NewClientTLSFromFile("./keys/server.pem", "小魔童")
        if err != nil {
            log.Panicf("Failed to create TLS mycredentials %v", err)
        }
        opts = append(opts, grpc.WithTransportCredentials(creds))
    }else{
        opts = append(opts, grpc.WithInsecure())
    }

    opts = append(opts, grpc.WithPerRPCCredentials(new(myCredential)))

    // TLS連線  記得把xxx改成你寫的伺服器地址,可以預設寫127.0.0.1
    conn, err := grpc.Dial(Address, opts...)
    if err != nil {
        grpclog.Fatalln(err)
    }
// ====== 新增的邏輯  END  ==============

    defer conn.Close()

    // 初始化客戶端
    c := pb.NewHiClient(conn)

    // 呼叫方法
    req := &pb.HiRequest{Name: "gRPC"}
    res, err := c.SayHi(context.Background(), req)
    if err != nil {
        log.Panicln(err)
    }

    log.Println(res.Message)
}

server/main.go

新增如下邏輯即可

package main

import (
   "fmt"
   "google.golang.org/grpc/codes"
   "google.golang.org/grpc/metadata"
   "log"
   "net"

   pb "myserver/protoc/hi"

   "golang.org/x/net/context"
   "google.golang.org/grpc"
   "google.golang.org/grpc/credentials" // 引入grpc認證包
)

const (
   // Address gRPC服務地址
   Address = "127.0.0.1:9999"
)

// 定義helloService並實現約定的介面
type HiService struct{}

// HiService Hello服務
var HiSer = HiService{}

// SayHello 實現Hello服務介面
func (h HiService) SayHi(ctx context.Context, in *pb.HiRequest) (*pb.HiResponse, error) {

// ====== 新增的邏輯  START ==============

   // 解析metada中的資訊並驗證
   md, ok := metadata.FromIncomingContext(ctx)
   if !ok {
      return nil, grpc.Errorf(codes.Unauthenticated, "no token ")
   }

   var (
      appId  string
      appKey string
   )

   // md 是一個 map[string][]string 型別的
   if val, ok := md["appid"]; ok {
      appId = val[0]
   }

   if val, ok := md["appkey"]; ok {
      appKey = val[0]
   }

   if appId != "myappid" || appKey != "mykey" {
      return nil, grpc.Errorf(codes.Unauthenticated, "token invalide: appid=%s, appkey=%s", appId, appKey)
   }

// ====== 新增的邏輯  END ==============

   resp := new(pb.HiResponse)
   resp.Message = fmt.Sprintf("Hi %s.", in.Name)

   return resp, nil
}

func main() {
   log.SetFlags(log.Ltime | log.Llongfile)
   listen, err := net.Listen("tcp", Address)
   if err != nil {
      log.Panicf("Failed to listen: %v", err)
   }

   // TLS認證
   creds, err := credentials.NewServerTLSFromFile("./keys/server.pem", "./keys/server.key")
   if err != nil {
      log.Panicf("Failed to generate credentials %v", err)
   }

   // 例項化grpc Server, 並開啟TLS認證
   s := grpc.NewServer(grpc.Creds(creds))

   // 註冊HelloService
   pb.RegisterHiServer(s, HiSer)

   log.Println("Listen on " + Address + " with TLS")

   s.Serve(listen)
}

好了,本次就到這裡,下一次分享 gRPC的interceptor

技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是小魔童哪吒,歡迎點贊關注收藏,下次見~

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章