Go + gRPC-Gateway(V2) 마이크로서비스实战,小程序 로그인 인증 서비스(5): 인증 gRPC-Interceptor 구현

인터셉터(gRPC-Interceptor) 소개

gRPC-InterceptorGin 미들웨어(Middleware)와 유사한 개념입니다. 이를 통해 실제 RPC 서비스 호출 전에 인증, 파라미터 검증, Rate Limiting 등의 공통 작업을 수행할 수 있습니다.

UnaryInterceptor 이해하기

gRPC 소스 코드를 분석해보면 다음과 같은 정의를 확인할 수 있습니다:

// UnaryInterceptor는 gRPC 서버에 UnaryServerInterceptor를 설정하는 ServerOption을 반환합니다.
// 하나의 unary interceptor만 설치할 수 있으며, 여러 interceptor의 연결(chaining)은 호출자 측에서 구현해야 합니다.
func UnaryInterceptor(i UnaryServerInterceptor) ServerOption {
    return newFuncServerOption(func(o *serverOptions) {
        if o.unaryInt != nil {
            panic("The unary server interceptor was already set and may not be reset.")
        }
        o.unaryInt = i
    })
}

다음으로 인터셉터의 타입 정의를 살펴보겠습니다:

// UnaryServerInterceptor는 서버에서 unary RPC 실행을 가로채는hook을 제공합니다.
// info는 interceptor가 조작할 수 있는 해당 RPC의 모든 정보를 포함합니다.
// handler는 서비스 메서드 구현의 래퍼입니다.
// interceptor의 책임은 handler를 호출하여 RPC를 완료하는 것입니다.
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

이Interceptror를 활용하면 RPC 서비스 호출 전에 인증(authentication) 같은 공통 작업을 수행할 수 있습니다.

인증 인터셉터 구현

비즈니스 요구사항:

  • 요청 헤더(Header)에서 authorization 필드에 전달된 토큰을 추출
  • 공개 키(Public Key)를 사용하여 토큰 유효성 검증
  • 유효한 경우 계정 ID(claims.subject)를 현재 요청 컨텍스트에附加

핵심 인터셉터 구현 코드:

type authInterceptor struct {
    tokenValidator tokenValidator
}

func (ai *authInterceptor) authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    // 토큰 추출
    tokenStr, err := extractToken(ctx)
    if err != nil {
        return nil, status.Error(codes.Unauthenticated, "")
    }
    
    // 토큰 검증
    accountID, err := ai.tokenValidator.validate(tokenStr)
    if err != nil {
        return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
    }
    
    // 실제 RPC 메서드 호출
    return handler(attachAccountID(ctx, AccountID(accountID)), req)
}

상세 구현은 /microsvcs/shared/auth/auth.go 파일에서 확인하세요.

Todo 마이크로서비스

테스트를 위한 간단한 할 일 목록(Todo-List) 서비스입니다. Todo RPC 서비스에 접근하기 전에 인증Interceptor를 통해 유효성을 검사합니다.

Proto 정의

todo.proto 파일:

syntax = "proto3";
package todo.v1;
option go_package="server/todo/api/gen/v1;todopb";

message CreateTodoRequest {
    string title = 1;
}

message CreateTodoResponse {
}

service TodoService {
    rpc CreateTodo (CreateTodoRequest) returns (CreateTodoResponse);
}

간단하게 테스트용으로 title 필드만 정의했습니다.

google.api.Service 정의

todo.yaml 파일:

type: google.api.Service
config_version: 3

http:
  rules:
  - selector: todo.v1.TodoService.CreateTodo
    post: /v1/todo
    body: "*"

코드 생성

microsvcs 디렉토리에서 실행:

sh gen.sh

생성되는 파일:

  • microsvcs/todo/api/gen/v1/todo_grpc.pb.go
  • microsvcs/todo/api/gen/v1/todo.pb.go
  • microsvcs/todo/api/gen/v1/todo.pb.gw.go

client 디렉토리에서 실행:

sh gen_ts.sh

생성되는 파일:

  • client/miniprogram/service/proto_gen/todo/todo_pb.js
  • client/miniprogram/service/proto_gen/todo/todo_pb.d.ts

CreateTodo 서비스 구현

상세 구현은 microsvcs/todo/todo/todo.go 파일에서 확인하세요:

type todoService struct {
    Logger *zap.Logger
    todopb.UnimplementedTodoServiceServer
}

func (ts *todoService) CreateTodo(c context.Context, req *todopb.CreateTodoRequest) (*todopb.CreateTodoResponse, error) {
    // 토큰에서 accountId 파싱 후 신원 확인
    accountID, err := auth.AccountIDFromContext(c)
    if err != nil {
        return nil, err
    }
    ts.Logger.Info("create todo", zap.String("title", req.Title), zap.String("account_id", accountID.String()))
    return nil, status.Error(codes.Unimplemented, "")
}

gRPC 서버起動重构

여러 마이크로서비스가 추가됨에 따라 서버起動 코드의 중복을 제거하기 위해 다음과 같이重构했습니다:

구현 위치: microsvcs/shared/server/grpc.go

func RunGRPCServer(cfg *GRPCConfig) error {
    serviceField := zap.String("service", cfg.ServiceName)
    listener, err := net.Listen("tcp", cfg.Addr)
    if err != nil {
        cfg.Logger.Fatal("failed to listen", serviceField, zap.Error(err))
    }
    
    var options []grpc.ServerOption
    // 인증 서비스는 auth interceptor가 필요 없음
    if cfg.AuthPublicKeyFile != "" {
        authInterceptor, err := auth.BuildInterceptor(cfg.AuthPublicKeyFile)
        if err != nil {
            cfg.Logger.Fatal("cannot create auth interceptor", serviceField, zap.Error(err))
        }
        options = append(options, grpc.UnaryInterceptor(authInterceptor))
    }
    
    grpcServer := grpc.NewServer(options...)
    cfg.RegistrationFunc(grpcServer)
    cfg.Logger.Info("server started", serviceField, zap.String("address", cfg.Addr))
    return grpcServer.Serve(listener)
}

다른 마이크로서비스의 서버起動 코드가 훨씬 간결해졌습니다:

구현 위치: todo/main.go

logger.Sugar().Fatal(
    server.RunGRPCServer(&server.GRPCConfig{
        ServiceName:      "todo",
        Addr:             ":8082",
        AuthPublicKeyFile: "shared/auth/public.key",
        Logger:           logger,
        RegistrationFunc: func(s *grpc.Server) {
            todopb.RegisterTodoServiceServer(s, &todo.Service{
                Logger: logger,
            })
        },
    }),
)

구현 위치: auth/main.go

logger.Sugar().Fatal(
    server.RunGRPCServer(&server.GRPCConfig{
        ServiceName: "auth",
        Addr:        ":8081",
        Logger:      logger,
        RegistrationFunc: func(s *grpc.Server) {
            authpb.RegisterAuthServiceServer(s, &auth.Service{
                OpenIDResolver: &wechat.Service{
                    AppID:     "your-appid",
                    AppSecret: "your-appsecret",
                },
                Mongo:          dao.NewMongo(mongoClient.Database("grpc-gateway-auth")),
                Logger:         logger,
                TokenExpire:    2 * time.Hour,
                TokenGenerator: token.NewJWTTokenGen("server/auth", privKey),
            })
        },
    }),
)

게이트웨이 연동

게이트웨이 서버重构

여러 gRPC 서버로 역방향 프록시 설정:

구현 위치: microsvcs/gateway/main.go

serverConfig := []struct {
    name         string
    addr         string
    registerFunc func(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error)
}{
    {
        name:         "auth",
        addr:         "localhost:8081",
        registerFunc: authpb.RegisterAuthServiceHandlerFromEndpoint,
    },
    {
        name:         "todo",
        addr:         "localhost:8082",
        registerFunc: todopb.RegisterTodoServiceHandlerFromEndpoint,
    },
}

for _, svc := range serverConfig {
    err := svc.registerFunc(
        ctx, mux, svc.addr,
        []grpc.DialOption{grpc.WithInsecure()},
    )
    if err != nil {
        logger.Sugar().Fatalf("cannot register service %s : %v", svc.name, err)
    }
}

listenAddr := ":8080"
logger.Sugar().Infof("grpc gateway started at %s", listenAddr)
logger.Sugar().Fatal(http.ListenAndServe(listenAddr, mux))

테스트

구성 완료 후 각 마이크로서비스와 게이트웨이를 실행하여 연동 테스트를 진행하세요.

참고 자료

  • grpc-ecosystem/go-grpc-middleware
  • API Security: API key is dead..Long live Distributed Token by value
  • Demo: go-grpc-gateway-v2-microservice
  • gRPC-Gateway
  • gRPC-Gateway Docs

태그: Golang gRPC gRPC-Gateway microservices Interceptor

7월 22일 06:57에 게시됨