はじめに
gRPCを使用し、簡単な認証を行ってみます。
言語はGoを使用します。
前提
以前行った環境例をもとに、認証を追加対応しています。
認証用のメタデータのセット
クライアントが認証トークンをメタデータとして送信し、サーバーがそのメタデータを検証する方法を示します。
クライアント側でのメタデータの追加
クライアントがリクエストを送信する際に、認証情報(例えばJWTトークン)をメタデータとして設定します。これを行うには、metadata.NewOutgoingContext
関数を使用して、認証トークンを含むコンテキストを作成します。
package main
import (
"log"
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"github.com/example/grpc_sample"
)
func main() {
log.Print("Client is starting...")
conn, err := grpc.Dial("localhost:9001", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
// 認証トークンをメタデータに設定
md := metadata.New(map[string]string{
"authorization": "Bearer your_auth_token_here",
})
ctx := metadata.NewOutgoingContext(context.Background(), md)
client := grpc_sample.NewSampleServiceClient(conn)
// メタデータ付きでリクエストを送信
req := &grpc_sample.GetDataRequest{NumType: "1"}
res, err := client.GetData(ctx, req)
if err != nil {
log.Fatalf("Failed to call GetData: %v", err)
}
log.Printf("Received response: %v", res)
}
サーバー側でのメタデータの検証
サーバーでは受信したメタデータを読み取り、認証トークンを検証します。これには metadata.FromIncomingContext
を使用します。
package main
import (
"log"
"net"
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/grpc/codes"
"github.com/example/grpc_sample"
)
type Sample struct{}
func (s *Sample) GetData(ctx context.Context, req *grpc_sample.GetDataRequest) (*grpc_sample.GetDataResponse, error) {
if md, ok := metadata.FromIncomingContext(ctx); ok {
// メタデータから認証トークンを取得
values := md["authorization"]
if len(values) == 0 || !validateToken(values[0]) {
return nil, status.Errorf(codes.Unauthenticated, "invalid token")
}
} else {
return nil, status.Errorf(codes.InvalidArgument, "no metadata received")
}
log.Print("Received request: ", req.NumType)
userDatas := []*grpc_sample.UserData{
{UserId: "1", UserName: "ユーザー1"},
{UserId: "2", UserName: "ユーザー2"},
}
numMax := int32(len(userDatas))
response := &grpc_sample.GetDataResponse{
UserDatas: userDatas,
NumMax: numMax,
}
return response, nil
}
func validateToken(token string) bool {
// ここでトークンの検証ロジックを実装
return token == "Bearer your_auth_token_here"
}
func main() {
log.Print("Server is starting...")
lis, err := net.Listen("tcp", ":9001")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
srv := grpc.NewServer()
grpc_sample.RegisterSampleServiceServer(srv, &Sample{})
if err := srv.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
上記の結果例です
(サーバ側)
2024/05/01 17:42:01 Server is starting...
2024/05/01 17:42:07 Received request: 1
(クライアント側)
2024/05/01 17:42:07 Client is starting...
2024/05/01 17:42:07 Received response: user_datas:{user_id:"1" user_name:"ユーザー1"} user_datas:{user_id:"2" user_name:"ユーザー2"} num_max:2
クライアントはリクエストにメタデータを含めて送信し、サーバーはそのメタデータを検証することができます。これにより、安全な認証付きの通信を行うことが可能になります。
説明
認証のプロセスを詳しく説明すると、以下のようになります:
-
クライアント側でのメタデータの設定:
クライアントはgRPCのリクエストを送信する際に、メタデータとして「Bearer your_auth_token_here」という認証トークンをauthorization
キーにセットします。このメタデータはリクエストと共にサーバーへと送られます。 -
サーバー側でのメタデータの受信と検証:
サーバーはリクエストを受け取ると、まずメタデータからauthorization
キーを参照して認証トークンを取得します。その後、このトークンが「Bearer your_auth_token_here」と一致するかどうかを検証します。 -
認証の結果に基づく処理:
-
認証成功: トークンが期待する値と一致していれば、リクエストされた操作(この場合は
GetData
メソッド)を実行し、適切なレスポンスをクライアントに返します。 - 認証失敗: トークンが無効な場合は、エラーレスポンスを返し、リクエストされた操作は実行されません。
-
認証成功: トークンが期待する値と一致していれば、リクエストされた操作(この場合は
このプロセスにより、不正なアクセスを防ぎつつ、有効な認証情報を持つクライアントのみがリソースにアクセスできるようになります。
コンテキストとメタデータ
コンテキスとメタデータについて触れます。
gRPCでは「メタデータ」と「コンテキスト」が重要な役割を果たします。これらはクライアントとサーバー間での情報の受け渡しや、リクエストのスコープ内での情報の管理に使われます。
メタデータ (Metadata)
メタデータは、HTTP/2ヘッダーフレームに格納されるキーと値のペアで、gRPCリクエストとレスポンスの両方に追加することができます。これはクライアントとサーバー間で設定や認証情報(トークンなど)、または任意の追加情報を送受信するために使用されます。
-
クライアント側のメタデータの設定:
metadata.NewOutgoingContext
という関数を使用して、既存のコンテキストにメタデータを追加します。これにより、クライアントはリクエストに認証トークンやその他の設定情報を含めることができます。 -
サーバー側でのメタデータの読み取り:
metadata.FromIncomingContext
を使って、リクエストからメタデータを抽出します。これを使ってサーバーは認証を行ったり、他のカスタムロジックを実行したりします。
コンテキスト (Context)
コンテキストは、context パッケージは Go 言語の標準ライブラリの一部であり、タイムアウト、キャンセルシグナル、その他のリクエストスコープの値を伝達するのに使用されます。gRPCでは、すべてのサーバーAPIがコンテキストオブジェクトを最初の引数として受け取ります。
-
context.Background()
の使用: プログラムの起動時やグローバルなオペレーションに使用される、基本的な空のコンテキストです。特に何かを開始する際の「根」コンテキストとして機能し、子コンテキストの基盤となります。 -
context.WithCancel
の使用: 特定のリクエストに対してキャンセル可能なコンテキストを生成します。このコンテキストから派生した任何の処理は、キャンセル関数が呼ばれた時に中止されることが可能です。 -
デッドライン/タイムアウトの管理: クライアントは
context.WithTimeout
またはcontext.WithDeadline
を使って、特定のリクエストに対してタイムアウトまたはデッドラインを設定できます。サーバーはこの情報を読み取り、設定されたタイムアウト内にリクエストを完了させる必要があります。 -
キャンセルの伝播: クライアントがリクエストをキャンセルすると、このキャンセル情報が
context.WithCancel
によって生成されたコンテキストを通じてサーバーに伝えられます。サーバーはそれに応じてクリーンアップや早期終了を行うことができます。 - 値の伝達: コンテキストを使用して、リクエスト処理に必要な特定の値(例えばユーザーIDやロケール情報など)をサーバーに渡すことができます。
コンテキストはリクエストの管理や情報の伝達に役立血ます。特にcontext.Background()
とcontext.WithCancel
は、新たな操作を開始し、必要に応じてこれを適切に管理するための基本的なツールとして、gRPCコミュニケーションにおいて中心的な役割を果たします。
例
認証において、context.Background()を使用した例です。
クライアントがメタデータとコンテキストを使用して認証トークンを送信し、サーバーがそれを検証する一連のフローは以下のようになります:
-
クライアントがコンテキストを作成し、メタデータに認証情報をセット:
md := metadata.New(map[string]string{"authorization": "Bearer your_auth_token_here"}) ctx := metadata.NewOutgoingContext(context.Background(), md)
-
サーバーがリクエストを受け取り、メタデータから認証情報を読み取り:
if md, ok := metadata.FromIncomingContext(ctx); ok { token := md["authorization"] if !validateToken(token) { return nil, status.Error(codes.Unauthenticated, "invalid token") } }
このように、メタデータとコンテキストはgRPCでのリクエスト処理において重要な役割を担っています。
参考記事