gRPC(七)進階:自定義身份驗證

lin鍾一發表於2022-11-09

個人網站:linzyblog.netlify.app/
示例程式碼已經上傳到github:點選跳轉
gRPC官方文件:點選跳轉
在前面的章節中,我們介紹了兩種可全域性認證的方法:

  • 基於 CA 的 TLS 證照認證
  • 攔截器 interceptor

而在實際需求中,常常會對某些模組的 RPC 方法做特殊認證或校驗,而gRPC也專門提供了這類特殊認證的介面。

gRPC為每個gRPC方法呼叫提供了Token認證支援,可以基於使用者傳入的Token判斷使用者是否登陸、以及許可權等,實現Token認證的前提是,需要定義一個結構體,並實現credentials.PerRPCCredentials介面。

1、credentials.PerRPCCredentials 介面

型別定義:

type PerRPCCredentials interface {
    // 返回需要認證的必要資訊
    GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
    // 是否使用安全連結(TLS)
    RequireTransportSecurity() bool
}

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

  • GetRequestMetadata:獲取當前請求認證所需的後設資料(metadata),以 map 的形式返回本次呼叫的授權資訊,ctx 是用來控制超時的
  • RequireTransportSecurity:是否需要基於 TLS 認證進行安全傳輸,如果返回 true 則說明該 Credentials 需要在一個有 TLS 認證的安全連線上傳輸,如果當前連線並沒有使用 TLS 則會報錯:
transport: cannot send secure credentials on an insecure connection

2、實現流程

  • 在發出請求之前,gRPC 會將 Credentials(認證憑證)存放在 metadata(後設資料)中進行傳遞。
  • 在真正發起呼叫之前,gRPC 會透過 GetRequestMetadata函式,將使用者定義的 Credentials(認證憑證)提取出來,並新增到 metadata(後設資料)中,隨著請求一起傳遞到服務端。
  • 然後服務端從 metadata 中取出 Credentials 進行有效性校驗。

具體分為以下兩步:

  • 1)客戶端請求時帶上 Credentials;
  • 2)服務端取出 Credentials,並驗證有效性,一般配合攔截器使用(這裡我們使用兩種方法,攔截器以及RPC方法)。

1、目錄結構

go-grpc-example
├─client
│  ├─token_client
│  │   └──client.go
├─pkg
│  ├─token
│  │   └──token.go
├─proto
│  ├─token
│  │   └──token.proto
└─server
    ├─token_server
    │  └──server.go

2、編寫IDL

在 proto/token 資料夾下的 token.proto 檔案中,寫入如下內容:

syntax = "proto3";

option go_package = "./proto/token;token";
package tokenservice;

// 驗證引數
message TokenValidateParam {
  string token = 1;
  int32 uid = 2;
}

// 請求引數
message Request {
  string name = 1;
}

// 請求返回
message Response {
  int32 uid = 1;
  string name = 2;
}

// 服務
service TokenService {
  rpc Token(Request) returns (Response);
}

在Makefile檔案中寫入:

token:
    protoc --go_out=. --go-grpc_out=. ./proto/token/*.proto

make token指令生成Go程式碼:

make token
protoc --go_out=. --go-grpc_out=. ./proto/token/*.proto

在這裡插入圖片描述

3、編寫基礎模板和空定義

我們先把基礎的模板和空定義寫出來在進行完善

1)server.go

const Address = "127.0.0.1:8888"

type TokenService struct {
    token.UnimplementedTokenServiceServer
}

func main() {
    listen, err := net.Listen("tcp", Address)
    if err != nil {
        fmt.Println("start error:", err)
        return
    }

    var opts []grpc.ServerOption

    server := grpc.NewServer(opts...)
    token.RegisterTokenServiceServer(server, &TokenService{})

    fmt.Println("服務啟動成功....")
    server.Serve(listen)
}

2)client.go

const Address = "127.0.0.1:8888"

func main() {
    var opts []grpc.DialOption

    conn, err := grpc.Dial(Address, opts...)
    if err != nil {
        fmt.Println("grpc.Dial error:", err)
        return
    }
    defer conn.Close()
    // 例項化客戶端
    client := token.NewTokenServiceClient(conn)

    // 呼叫具體方法
    token, err := client.Token(context.Background(), &token.Request{Name: "linzy"})
    if err != nil {
        fmt.Println("client.Token error:", err)
        return
    }
    fmt.Println("return result:", token)
}

4、實現PerRPCCredentials 介面

我們在 pkg/token 目錄裡的 token.go 檔案內實現PerRPCCredentials 介面的方法:

const IsTLS = false

// 定義一個認證的結構體,這裡是因為我在porto寫好了一個資料結構
// 也可以自定義認證欄位
type TokenAuth struct {
    token.TokenValidateParam
}

func (x *TokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    // 將 Credentials(認證憑證)存放在 metadata(後設資料)中進行傳遞。
    return map[string]string{
        "uid":   strconv.FormatInt(int64(x.GetUid()), 10),
        "token": x.GetToken(),
    }, nil
}

func (x *TokenAuth) RequireTransportSecurity() bool {
    return IsTLS
}

5、實現認證功能

我們已經實現了客戶端請求時帶上 Credentials 憑證,後面就需要實現服務端的功能,在獲取授權資訊並校驗有效性。

1)實現攔截器認證

pkg/Interceptor 目錄下的 Interceptor.go 檔案內寫入以下內容:

// 用一元攔截器實現認證
func ServerInterceptorCheckToken() grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
        handler grpc.UnaryHandler) (resp interface{}, err error) {
        // 驗證token
        _, err = CheckToken(ctx)
        if err != nil {
            fmt.Println("Interceptor 攔截器內token認證失敗\n")
            return nil, err
        }
        fmt.Println("Interceptor 攔截器內token認證成功\n")
        return handler(ctx, req)
    }
}

// 驗證
func CheckToken(ctx context.Context) (*token.Response, error) {
    // 取出後設資料
    md, b := metadata.FromIncomingContext(ctx)
    if !b {
        return nil, status.Error(codes.InvalidArgument, "token資訊不存在")
    }

    var token, uid string
    // 取出token
    tokenInfo, ok := md["token"]
    if !ok {
        return nil, status.Error(codes.InvalidArgument, "token不存在")
    }

    token = tokenInfo[0]

    // 取出uid
    uidTmp, ok := md["uid"]
    if !ok {
        return nil, status.Error(codes.InvalidArgument, "uid不存在")
    }
    uid = uidTmp[0]

    //驗證
    sum := md5.Sum([]byte(uid))
    md5Str := fmt.Sprintf("%x", sum)
    if md5Str != token {
        fmt.Println("md5Str:", md5Str)
        fmt.Println("uid:", uid)
        fmt.Println("token:", token)
        return nil, status.Error(codes.InvalidArgument, "token驗證失敗")
    }
    return nil, nil
}

gPRC 傳輸的時候把授權資訊存放在 metada 的,所以需要先獲取 metadata。透過metadata.FromIncomingContext可以從 ctx 中取出本次呼叫的 metadata,然後再從 md 中取出授權資訊並校驗即可。

在server.go檔案內新增攔截器:

opts = append(opts, grpc.UnaryInterceptor(Interceptor.ServerInterceptorCheckToken()))

2)實現RPC方法認證

實現了校驗有效性我們就需要在 server.go 服務端實現Token RPC的方法進行授權認證:

type TokenService struct {
    token.UnimplementedTokenServiceServer
    tokenAuth.TokenAuth
}

func (u TokenService) Token(ctx context.Context, r *token.Request) (*token.Response, error) {
    // 驗證token
    _, err := Interceptor.CheckToken(ctx)
    if err != nil {
        fmt.Println("Token RPC方法內token認證失敗\n")
        return nil, err
    }
    fmt.Printf("%v Token RPC方法內token認證成功\n", r.GetName())
    return &token.Response{Name: r.GetName()}, nil
}

同樣的在client.go 檔案內輸入token資訊,並呼叫grpc.WithPerRPCCredentials:

// token資訊
auth := tokenAuth.TokenAuth{
    token.TokenValidateParam{
        Token: "81dc9bdb52d04dc20036dbd8313ed055",
        Uid:   1234,
    },
}
opts = append(opts, grpc.WithPerRPCCredentials(&auth))

6、啟動 & 請求

輸入一個正確的token:

# 啟動服務端
$ go run server.go
API server listening at: 127.0.0.1:52505
服務啟動成功....
Interceptor 攔截器內token認證成功

linzy Token RPC方法內token認證成功

# 啟動客戶端
$ go run client.go 
API server listening at: 127.0.0.1:52545
return result: name:"linzy"

修改token資訊為:

// token資訊
    auth := tokenAuth.TokenAuth{
        token.TokenValidateParam{
            Token: "81dc9bdb52d0ed0585",
            Uid:   1234,
        },
    }

測試一下:

# 啟動服務端
$ go run server.go
API server listening at: 127.0.0.1:52505
服務啟動成功....
md5Str: 81dc9bdb52d04dc20036dbd8313ed055
uid: 1234
token: 81dc9bdb52d0ed0585
Interceptor 攔截器內token認證失敗

# 啟動客戶端
$ go run client.go 
API server listening at: 127.0.0.1:52857
client.Token error: rpc error: code = InvalidArgument desc = token驗證失敗

7、實現RequireTransportSecurity()方法

身份認證功能已經完成,但是我們gRPC通訊還是明文傳輸,對於如此重要的資訊肯定要建立安全連線,所以要實現 RequireTransportSecurity 方法。

方法實現很簡單,我們只需要建立安全連線的時候,返回一個true就行,使用我們之前的證照進行TLS連線即可。

具體可以看我的上一篇《透過TLS建立安全連線》

server.go新增以下內容:

if tokenAuth.IsTLS {
    // TLS認證
    // 根據服務端輸入的證照檔案和金鑰構造 TLS 憑證
    c, err := credentials.NewServerTLSFromFile("./conf/server_side_TLS/server.pem", "./conf/server_side_TLS/server.key")
    if err != nil {
        log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
    }
    opts = append(opts, grpc.Creds(c))
}

client.go新增以下內容:

if tokenAuth.IsTLS {
    //開啟tls 走tls認證
    // 根據客戶端輸入的證照檔案和金鑰構造 TLS 憑證。
    // 第二個引數 serverNameOverride 為服務名稱。
    c, err := credentials.NewClientTLSFromFile("./conf/server_side_TLS/server.pem", "go-grpc-example")
    if err != nil {
        log.Fatalf("credentials.NewClientTLSFromFile err: %v", err)
    }
    opts = append(opts, grpc.WithTransportCredentials(c))
} else {
    opts = append(opts, grpc.WithInsecure())
}

我們只需要修改token.go檔案內的IsTLS變數就可以實現是否使用安全連結(TLS)。

啟動 & 請求之後我們抓個包看一下是否已經建立安全連結了了。

在這裡插入圖片描述
在這裡插入圖片描述

1)實現credentials.PerRPCCredentials介面就可以把資料當做 gRPC 中的 Credential 在新增到 metadata 中,跟著請求一起傳遞到服務端;
2)服務端從 ctx 中解析 metadata,然後從 metadata 中獲取 授權資訊並進行驗證;
3)可以藉助 Interceptor 實現全域性身份驗證。

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

相關文章