🔑

[gRPC] メソッドレベルで異なる権限を実装してみる

2023/08/16に公開
1

記事を書こうと思ったきっかけ

個人での勉強の一環でgRPCを用いたAPIの作成を行っていて、認証処理を実装するにあたりメソッドレベルで権限を分けたいケースにぶち当たったので、その際の対処法をアウトプットとして残しておこうと思い、今回記事を作成しました。

gRPCではRESTのMiddlewear感覚でInterceptorを分けられない

普通にREST APIでエンドポイントごとに権限を分けようとすると、以下のように権限が必要なエンドポイントだけに認証処理を施したMiddlewareを使用するような形になると思います。

func main() {
    http.HandleFunc("/public", publicHandler)
    http.HandleFunc("/private", AuthMiddleware(privateHandler))
    http.ListenAndServe(":8080", nil)
}

しかしgRPCサーバではそうはいかず、Interceptorをサーバに登録するとすべてのメソッド(今回ならUnary RPCメソッド)にInterceptorが適用されてしまい、REST APIのようにメソッドごとにInterceptorを適用するといったことができなくなってしまいます。

func main() {
    // gRPC サーバーを初期化
    server := grpc.NewServer(
	// Interceptorを登録(すべてのUnary RPCメソッドに適用されてしまう)
        grpc.UnaryInterceptor(AuthInterceptor),
    )
    
    // ...
    
    // サーバーを起動
    lis, _ := net.Listen("tcp", "localhost:50051")
    _ = server.Serve(lis)
}

ではgRPCでは権限が分けられないのかというとそうではなく、権限が必要なメソッドとそうでないメソッドをあらかじめ定義してInterceptor内でメソッドに認証が必要か否か判断するように実装することでこの問題を解決することができるのです。以下に実装例を示します。

実装例(Go)

package main

import (
    "context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

// 認証が必要なメソッドの名前
var authRequiredMethods = map[string]bool{
    "/yourpackage.YourService/AuthRequiredMethod": true,
    "/yourpackage.YourService/AuthNotRequiredMethod": false,
}

func AuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    // 認証が必要なメソッドかどうかをチェック
    if authRequiredMethods[info.FullMethod] {
        // 認証処理...
    }

    // 認証が通過したら、ハンドラを呼び出す
    return handler(ctx, req)
}

func main() {
    // gRPC サーバーを初期化
    server := grpc.NewServer(
        grpc.UnaryInterceptor(AuthInterceptor),
    )
    
    // ...

    // サーバーを起動
    lis, _ := net.Listen("tcp", "localhost:50051")
    _ = server.Serve(lis)
}

各部について解説

認証を必要とするメソッドの判定

// 認証が必要なメソッドの名前
var authRequiredMethods = map[string]bool{
    "/yourpackage.YourService/AuthRequiredMethod": true,
    "/yourpackage.YourService/AuthNotRequiredMethod": false,
}

ここでは認証が必要なメソッドとそうでないメソッドを判断するため、FullMethodとboolを対応させた配列を用意しています。

FullMethodとは

"/yourpackage.YourService/AuthRequiredMethod"のようにMethodの完全な名前を表す文字列のことを言います。

Interceptor

func AuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    // 認証が必要なメソッドかどうかをチェック
    if authRequiredMethods[info.FullMethod] {
        // ログイン処理...
    }

    // 認証が通過したら、ハンドラを呼び出す
    return handler(ctx, req)
}

if authRequiredMethods[info.FullMethod]の部分で認証が必要なメソッドか否かの条件分岐を行っています。
*grpc.UnaryServerInfoにFullMethodが格納されているため、info.FullMethodで呼び出されたメソッドのFullMethodを取得することができます。
認証が必要であればif内の処理を実行し、そうでなければスルーしてハンドラを呼び出します。

最後に

メモ的な感じで書いてしまったのでものすごく雑な記事になってしまいました。
今回はメソッドに対してboolで判定を行いましたが、Admin, UserなどRoleを定義してRoleごとに認証を操作するほうがより実践的かなと思います。

Discussion

Daaaai / KabosDaaaai / Kabos

[追記]
fullmethodはべた書きする必要ないですね。
protoファイルから生成されたコードに定数として定義されているのでそれを使った方がいいです。