gRPC、Go 和 Cloud Run - 如何优雅地处理客户端授权?

问题描述

我们在 Google Cloud Run 实例上部署了一个 gRPC 服务器,我们希望从其他 Google Cloud 环境(特别是 GKE 和 Cloud Run)访问该实例。

我们有以下代码获取连接对象以及从 Google 认凭据流生成的不记名令牌的上下文:

import (
    "context"
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "os"
    "regexp"

    "google.golang.org/api/idtoken"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    grpcMetadata "google.golang.org/grpc/Metadata"
)

type ServerConnection struct {
    Conn   *grpc.ClientConn
    Ctx    context.Context
}

// NewServerConnection creates a new gRPC connection and request a Token to be used in the context.
//
// The host should be the domain where the Service is hosted,e.g.,my-cloudrun-url-v1-inb33tjqiq-ew.a.run.app
//
// This method also uses the Google Default Credentials workflow.  To run this locally ensure that you have the
// environmental variable GOOGLE_APPLICATION_CREDENTIALS = ../key.json set.
//
// Best practise is to create a new connection at global level,which Could be used to run many methods.  This avoids
// unnecessary api calls to retrieve the required ID tokens each time a single method is called.
func NewServerConnection(ctx context.Context,host string) (*ServerConnection,error) {

    // Establishes a connection
    var opts []grpc.DialOption
    if host != "" {
        opts = append(opts,grpc.WithAuthority(host+":443"))
    }

    systemRoots,err := x509.SystemCertPool()
    if err != nil {
        return nil,err
    }

    cred := credentials.NewTLS(&tls.Config{
        RootCAs: systemRoots,})
    opts = append(opts,grpc.WithTransportCredentials(cred))
    opts = append(opts,grpc.WithPerRPCCredentials())

    conn,err := grpc.Dial(host+":443",opts...)

    // Creates an identity token.
    // A given TokenSource is specific to the audience.
    tokenSource,err := idtoken.NewTokenSource(ctx,"https://"+host)
    if err != nil {
        return nil,err
    }
    token,err := tokenSource.Token()
    if err != nil {
        return nil,err
    }

    // Add token to gRPC Request.
    ctx = grpcMetadata.AppendToOutgoingContext(ctx,"authorization","Bearer "+token.Accesstoken)

    return &ServerConnection{
        Conn: conn,Ctx:  ctx,},nil
}

然后使用上面的:

// Declare Globally
var myServer *ServerConnection

func TestNewServerConnection(t *testing.T) {
    // Connects to the server and add token to ctx.
    // In cloud run this is done once,populating the global variable
    ctx := context.Background()
    var err error;
    myServer,_ = NewServerConnection(ctx,"my-cloudrun-url-v1-inb33tjqiq-ew.a.run.app")

    // Now that we have a connection as well as a Context object with the Token 
    // we would like to make many client calls.
    client := pb.NewBookstoreClient(myServer.Conn)
    result,err := client.CreateBook(myServer.Ctx,&pb.Book{})
    if err != nil {
        // Todo: handle error
    }
    // Use result
    _ = result
    
    // ... make more client procedure calls here...
}

要强调的几点:

问题:

  • 以上是访问 Cloud Run 的一种优雅方式吗?
  • 目前我们必须将 myServer.Ctx 添加到我们所有的客户端过程调用中 - 有没有办法将它“嵌入”到 myServer.Conn 中? WithPerRPCCredentials 在这里有用吗?
  • 如何处理过期的令牌?令牌的认到期时间为 1 小时,任何从初始实例化开始超过 1 小时的客户端过程调用都将失败。是否有一种优雅的方式来“刷新”或生成新令牌?

希望这一切都有意义!在 Google Cloud 上运行服务时,用于管理访问权限的 Cloudrun、gRPC 和 IAM 可能是一个非常优雅的设置。

解决方法

这里有一些相当优雅的东西。它使用 Google 应用程序凭据并将 lstStudents = thisStudents.Cast(Of Student)().ToList() 对象附加到 gRPC 连接对象。我的理解是,如果需要,这将允许在每次 gRPC 调用时自动刷新令牌。

NewTokenSource

可以如下使用:

// NewServerConnection creates a new gRPC connection.
//
// The host should be the domain where the Cloud Run Service is hosted
//
// This method also uses the Google Default Credentials workflow.  To run this locally ensure that you have the
// environmental variable GOOGLE_APPLICATION_CREDENTIALS = ../key.json set.
//
// Best practise is to create a new connection at global level,which could be used to run many methods.  This avoids
// unnecessary api calls to retrieve the required ID tokens each time a single method is called.
func NewServerConnection(ctx context.Context,host string) (*grpc.ClientConn,error) {

    // Creates an identity token.
    // With a global TokenSource tokens would be reused and auto-refreshed at need.
    // A given TokenSource is specific to the audience.
    tokenSource,err := idtoken.NewTokenSource(ctx,"https://"+host)
    if err != nil {
        return nil,status.Errorf(
            codes.Unauthenticated,"NewTokenSource: %s",err,)
    }

    // Establishes a connection
    var opts []grpc.DialOption
    if host != "" {
        opts = append(opts,grpc.WithAuthority(host+":443"))
    }

    systemRoots,err := x509.SystemCertPool()
    if err != nil {
        return nil,err
    }

    cred := credentials.NewTLS(&tls.Config{
        RootCAs: systemRoots,})
    opts = append(opts,grpc.WithTransportCredentials(cred))
    opts = append(opts,grpc.WithPerRPCCredentials(grpcTokenSource{
        TokenSource: oauth.TokenSource{
            tokenSource,},}))

    conn,err := grpc.Dial(host+":443",opts...)
    if err != nil {
        return nil,"grpc.Dail: %s",)
    }

    return conn,nil
}