grpc で認証処理を実装する
-
interceptorを使って実装すると、良さそう
- interceptor 自体は middleware 的な立ち回りをしてくれる
実装
- grpc-ecosystem にライブラリが用意されているのでそれを使っていく
- go-grpc-middleware/auth
- 基本的には Client に PW を付与して、それをリクエスト時に投げてもらい、interceptor 内でチェックします
- 実際に書くのは以下のようなコード
grpcServer = grpc.NewServer(
grpc_middleware.WithUnaryServerChain(
grpcMetrics.UnaryServerInterceptor(),
grpc_recovery.UnaryServerInterceptor(opts...),
// ここでChain
grpc_auth.UnaryServerInterceptor(AuthFunc),
),
)
// AuthFunc は go-grpc-middleware/auth で使用する認証用の関数
// この関数自体は別のパッケージに切り出しても良い
func AuthFunc(ctx context.Context) (context.Context, error) {
// ここでClientから投げられた認証ヘッダの値を取得
key, err := grpc_auth.AuthFromMD(ctx, "bearer")
if err != nil {
return nil, err
}
// ここでは"password"ハードコーディングだけど、この手前でDBアクセスしたりして任意のパスを取得したりしても良い
if key != "password" {
return nil, grpc.Errorf(codes.Unauthenticated, "invalid api password")
}
// context に Client の情報を詰めて返す
newCtx := context.WithValue(ctx, "result", "ok")
return newCtx, nil
}
-
grpc_auth.UnaryServerInterceptor(AuthFunc)
に渡すAuthFunc
は以下の型を返す関数なら中身はどうでもいいっぽい-
type AuthFunc func(ctx context.Context) (context.Context, error)
-
-
AuthFunc
の中で、やりたいように認証処理を実装していく-
AuthFromMD
は指定した文字列をキーにリクエスト認証ヘッダからバリューを取得してくれる- MD は MetaData かと思われる
- 認証ヘッダを使わない場合は、普通にhttpリクエストヘッダを使ってバリューを取得する
- よければ context を返し、ダメならエラーを返すとライブラリ側でうまいこと
UnaryServerInterceptor
かエラーを返してくれる
-
github.com/grpc-ecosystem/go-grpc-middleware/blob/master/auth/auth.go
func UnaryServerInterceptor(authFunc AuthFunc) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
var newCtx context.Context
var err error
if overrideSrv, ok := info.Server.(ServiceAuthFuncOverride); ok {
newCtx, err = overrideSrv.AuthFuncOverride(ctx, info.FullMethod)
} else {
newCtx, err = authFunc(ctx)
}
if err != nil {
return nil, err
}
return handler(newCtx, req)
}
}
使ってみる
- grpcurl してみて、ちゃんと認証処理が挟まるかチェック
grpcurl -plaintext -import-path . -proto proto/hello.proto -d '{"name": "hoge"}' -H "Authorization: bearer password" localhost:5000 hello.HelloService/Hello
所感
-
grpc.UnaryServerInterceptor
を返す部分と、認証ヘッダからバリューを取得する処理を自前で書かなくていい分楽かな - とはいえ全部自分で実装してもそんなに手間でもない気がする
特定メソッドだけ認証をスキップしたい
- 例えばヘルスチェックは認証をスキップしたい、みたいな場合はServiceAuthFuncOverrideが使える
- 以下の interface の実装が grpc server に実装されていれば、
AuthFunc
より優先的にこちらがコールされる(オーバーライドと書いてあるが、実際には実装があるか否かの分岐が入っているっぽい)
- 以下の interface の実装が grpc server に実装されていれば、
type ServiceAuthFuncOverride interface {
AuthFuncOverride(ctx context.Context, fullMethodName string) (context.Context, error)
}
-
fullMethodName
が grpc のサービスとメソッドの情報を持っているので、少なくともメソッドの粒度で何かしらの分岐が可能
func (s *Server) AuthFuncOverride(ctx context.Context, fullMethodName string) (context.Context, error) {
// HealthCheck がコールされた場合は認証のチェックを skip する
if fullMethodName == "/hoge_proto.HogeService/HealthCheck" {
return ctx, nil
}
// 認証チェック処理
// ~~省略~~
return ctx, nil
}