個人網站: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 協議》,轉載必須註明作者和本文連結