LoginSignup
38
22

More than 5 years have passed since last update.

Go - gRPC での認証・認可を Interceptor を使って実装する

Last updated at Posted at 2018-07-11

tl;dr

  • すべての RPC に共通する処理には gRPC Interceptor を利用する
  • 認証 Interceptor はすべての RPC で同じ処理を行って、認可 Interceptor は gRPC サービスごとに別の処理を行えるようにする
  • 認証に使う Authorization 値は Metadata として送信する
  • 複数の Interceptor をまとめて書くには go-grpc-middleware の chain を使う
  • ここでは簡単のため Unary RPC を前提にまとめる

gRPC Interceptor

gRPC サーバーはオプションとして UnaryServerInterceptor 型を渡すことができる。UnaryServerInterceptor 型はすべての RPC の実行前後に処理のフックを追加することができるので、いわゆるミドルウェアを実装するために使える。

// UnaryServerInterceptor provides a hook to intercept the execution of a unary RPC on the server. info
// contains all the information of this RPC the interceptor can operate on. And handler is the wrapper
// of the service method implementation. It is the responsibility of the interceptor to invoke handler
// to complete the RPC.
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

認証 Interceptor

認証 Interceptor ではユーザーの Authorization 値が適切かを確認したいとする。すべての RPC で共通する処理として実装する。Authorization 値はすべての RPC に共通するので Metadata としてやり取りすると良い。

package grpcauthentication

import (
    "context"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

// DefaultAuthenticateFunc はすべてのサービスに共通する認証処理を行う関数を表す。
type DefaultAuthenticateFunc func(ctx context.Context) (context.Context, error)

// UnaryServerInterceptor はリクエストごとの認証処理を行う、unary サーバーインターセプターを返す。
func UnaryServerInterceptor(authFunc DefaultAuthenticateFunc) grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        newCtx, err := authFunc(ctx)
        if err != nil {
            return nil, status.Error(codes.Unauthenticated, err.Error())
        }
        return handler(newCtx, req)
    }
}
package meta

import (
    "fmt"

    "golang.org/x/net/context"
    "google.golang.org/grpc/metadata"
)


const (
    // AuthorizationKey は認証トークンに対応するキーを表す
    AuthorizationKey = "authorization"
)

// Authorization は gRPC メタデータからユーザー認証トークンを取得する
func Authorization(ctx context.Context) (string, error) {
    return fromMeta(ctx, AuthorizationKey)
}

func fromMeta(ctx context.Context, key string) (string, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return "", fmt.Errorf("not found metadata")
    }
    vs := md[key]
    if len(vs) == 0 {
        return "", fmt.Errorf("not found %s in metadata", key)
    }
    return vs[0], nil
}
package main

import (
    "context"

    "github.com/path/to/meta"
    "github.com/path/to/interceptor/grpcauthentication"

    "google.golang.org/grpc"
)


func defaultAuthentication() grpcauthentication.AuthFunc {
    return func(ctx context.Context) (context.Context, error) {
        authorization, err := meta.Authorization(ctx)
        if err != nil {
            return nil, err
        }

        err := verify(authorization)
        if err != nil {
            return nil, err
        }

        return ctx, nil
    }
}

func main() {
    grpcServer := grpc.NewServer(
        grpc.UnaryInterceptor(grpcauthentication.UnaryServerInterceptor(defaultAuthentication())),
    )
...
}

認可 Interceptor

認可 Interceptor ではサービスや RPC ごとにそのユーザーに権限があるかどうかを確認したいとする。例えば user サービスで提供する RPC ではサービス停止していないかだけを確認したいが、admin サービスで提供する RPC では管理者権限を持っているかどうかを確認したい。

サービスごとに認可処理を行えるように、各サービスは ServiceAuthorize インターフェースを実装する必要がある。

package grpcauthorization

import (
    "context"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

// ServiceAuthorize はサービスごとの認可を行う関数を実装するインターフェースを表す。
type ServiceAuthorize interface {
    Authorize(context.Context, string) error
}

// UnaryServerInterceptor はリクエストごとの認可を行う、unary サーバーインターセプターを返す。
func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        var err error
        if srv, ok := info.Server.(ServiceAuthorize); ok {
            err = srv.Authorize(ctx, info.FullMethod)
        } else {
            return nil, fmt.Errorf("each service should implement an authorization")
        }
        if err != nil {
            return nil, status.Error(codes.PermissionDenied, err.Error())
        }
        return handler(ctx, req)
    }
}
package userservice

import (
    "context"
)

// Authorize はユーザーの認可を行う。
func (*UserService) Authorize(ctx context.Context, fullMethodName string) error {
    switch fullMethodName {
    // 以下の RPC メソッドはすべてのユーザーが行うことができるメソッドなので無条件に許可する {
    case
        "/userpb.UserService/ReportTrouble":
        return nil
    // }
    default:
        return userAuthFunc(ctx)
    }
}
package adminservice

import (
    "context"
)

// Authorize はユーザーの認可を行う。
func (*AdminService) Authorize(ctx context.Context, fullMethodName string) error {
    return adminAuthFunc(ctx)
}
package main

import (
    "context"

    "github.com/path/to/interceptor/grpcauthorization"

    "google.golang.org/grpc"
)

func main() {
    grpcServer := grpc.NewServer(
        grpc.UnaryInterceptor(grpcauthorization.UnaryServerInterceptor()),
    )
...
}

複数の Interceptor をまとめる

https://github.com/grpc-ecosystem/go-grpc-middleware には色々な interceptor 実装がまとまっている。その中にある chain を使うと複数の Interceptor を以下のように書ける。

grpcServer := grpc.NewServer(
    grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
        grpcauthentication.UnaryServerInterceptor(defaultAuthentication()),
        grpcauthorization.UnaryServerInterceptor(),
    )),
)

まとめ

grpc サーバーとサービスを合わせたときの記述は次のようになる。

func main() {
    grpcServer := grpc.NewServer(
        grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
            grpcauthentication.UnaryServerInterceptor(defaultAuthentication()),
            grpcauthorization.UnaryServerInterceptor(),
        )),
    )

    usersv := userservice.NewUserService()
    adminsv := adminservice.NewAdminService()
    userpb.RegisterUserServiceServer(grpcServer, usersv)
    adminpb.RegisterAdminServiceServer(grpcServer, adminsv)
...
}
38
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
22