gRPC、Go & Cloud 运行 - 如何优雅地处理客户端授权?
gRPC, Go & Cloud Run - How to elegantly handle client authorisation?
我们在 Google 云 运行 实例上部署了一个 gRPC 服务器,我们希望从其他 Google 云环境(尤其是 GKE 和云 运行 访问该实例).
我们有以下代码来获取连接对象以及从 Google 默认凭证流生成的 Bearer 令牌的上下文:
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...
}
要强调的几点:
- NewServerConnection 基于 Google 的文档:Obtaining an OIDC token for the default service account and Sending gRPC requests with authentication
- 我们全局声明
myServer
对象并初始化一次。这是为了避免对底层元数据服务器进行不必要的调用以检索 Google 默认凭证,即令牌。这是来自 Google's Documentation 关于此概念的 link
- 一旦 'initalised' 我们有一个 ctx 对象,它包含一个不记名令牌,然后我们在每次调用客户端的任何 rpc 方法时使用它。
问题:
- 以上是访问云的优雅方式吗运行?
- 目前我们必须将
myServer.Ctx
添加到我们所有的客户端过程调用中 - 有没有办法在 myServer.Conn
中 'embed' 这个? WithPerRPCCredentials在这里有用吗?
- 如何处理过期的令牌?令牌的默认到期时间为 1 小时,任何从初始实例化起超过 1 小时的客户端过程调用都将失败。有没有一种优雅的方式来 'refresh' 或生成一个新的令牌?
希望这一切都有意义!当 Google 服务在 Google 云上时,用于管理访问的 Cloudrun、gRPC 和 IAM 可能是一个非常优雅的设置。
这里有一些相当优雅的东西。它使用 Google 应用程序凭据并将 NewTokenSource
对象附加到 gRPC 连接对象。我的理解是,如果需要,这将允许在每次 gRPC 调用时自动刷新令牌。
// 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, status.Errorf(
codes.Unauthenticated,
"grpc.Dail: %s", err,
)
}
return conn, nil
}
可以这样使用:
import (
"context"
pb "path-to-your-protos"
"google.golang.org/grpc"
)
func ExampleNewServerConnection() {
// Creates the connection and Authorise using default credentials.
var err error
var myConn *grpc.ClientConn
myConn, err = NewServerConnection(context.Background(), "cloudrun-url-...-.app")
if err != nil {
// TODO: handle error
}
// Create a client from the server connection.
client := pb.NewServicesClient(myConn)
// Once the connection is created and tokens retrieved, make one or more calls to the respective methods.
result1, err := client.CreateBook(context.Background(), &pb.Book{})
if err != nil {
// TODO: handle error
}
// Use the result
_ = result1
// Another call
result2, err := client.CreateBook(context.Background(), &pb.Book{})
if err != nil {
// TODO: handle error
}
// Use the result
_ = result2
}
我们在 Google 云 运行 实例上部署了一个 gRPC 服务器,我们希望从其他 Google 云环境(尤其是 GKE 和云 运行 访问该实例).
我们有以下代码来获取连接对象以及从 Google 默认凭证流生成的 Bearer 令牌的上下文:
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...
}
要强调的几点:
- NewServerConnection 基于 Google 的文档:Obtaining an OIDC token for the default service account and Sending gRPC requests with authentication
- 我们全局声明
myServer
对象并初始化一次。这是为了避免对底层元数据服务器进行不必要的调用以检索 Google 默认凭证,即令牌。这是来自 Google's Documentation 关于此概念的 link
- 一旦 'initalised' 我们有一个 ctx 对象,它包含一个不记名令牌,然后我们在每次调用客户端的任何 rpc 方法时使用它。
问题:
- 以上是访问云的优雅方式吗运行?
- 目前我们必须将
myServer.Ctx
添加到我们所有的客户端过程调用中 - 有没有办法在myServer.Conn
中 'embed' 这个? WithPerRPCCredentials在这里有用吗? - 如何处理过期的令牌?令牌的默认到期时间为 1 小时,任何从初始实例化起超过 1 小时的客户端过程调用都将失败。有没有一种优雅的方式来 'refresh' 或生成一个新的令牌?
希望这一切都有意义!当 Google 服务在 Google 云上时,用于管理访问的 Cloudrun、gRPC 和 IAM 可能是一个非常优雅的设置。
这里有一些相当优雅的东西。它使用 Google 应用程序凭据并将 NewTokenSource
对象附加到 gRPC 连接对象。我的理解是,如果需要,这将允许在每次 gRPC 调用时自动刷新令牌。
// 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, status.Errorf(
codes.Unauthenticated,
"grpc.Dail: %s", err,
)
}
return conn, nil
}
可以这样使用:
import (
"context"
pb "path-to-your-protos"
"google.golang.org/grpc"
)
func ExampleNewServerConnection() {
// Creates the connection and Authorise using default credentials.
var err error
var myConn *grpc.ClientConn
myConn, err = NewServerConnection(context.Background(), "cloudrun-url-...-.app")
if err != nil {
// TODO: handle error
}
// Create a client from the server connection.
client := pb.NewServicesClient(myConn)
// Once the connection is created and tokens retrieved, make one or more calls to the respective methods.
result1, err := client.CreateBook(context.Background(), &pb.Book{})
if err != nil {
// TODO: handle error
}
// Use the result
_ = result1
// Another call
result2, err := client.CreateBook(context.Background(), &pb.Book{})
if err != nil {
// TODO: handle error
}
// Use the result
_ = result2
}