使用拦截器来实现Go-gRPC的认证和授权

简而言之

    • すべての RPC に共通する処理には gRPC Interceptor を利用する

 

    • 認証 Interceptor はすべての RPC で同じ処理を行って、認可 Interceptor は gRPC サービスごとに別の処理を行えるようにする

 

    • 認証に使う Authorization 値は Metadata として送信する

 

    • 複数の Interceptor をまとめて書くには go-grpc-middleware の chain を使う

 

    ここでは簡単のため Unary RPC を前提にまとめる

gRPC 拦截器

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)

認證攔截器

在认证拦截器中,希望确认用户的授权值是否正确。作为所有RPC的共同处理来实现。由于授权值在所有RPC中都是共同的,所以最好将其作为元数据进行交换。

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中,我們希望對每個服務或RPC檢查使用者是否具有權限。例如,對於使用者服務提供的RPC,我們只需要檢查服務是否停止,而對於管理員服務提供的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()),
    )
...
}

将多个拦截器整合在一起

在 https://github.com/grpc-ecosystem/go-grpc-middleware 这个库中,有许多拦截器的实现汇总在一起。其中包括使用 chain 的方式来编写多个拦截器,如下所示。

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)
...
}
广告
将在 10 秒后关闭
bannerAds