Go微服务实战|第10章:gRPC Authentication
Synopsis: 上一篇我们通过 TLS 为通信双方建立了安全加密通道,并且数字证书也可以验证服务端的身份。但是,服务端也需要确认是哪个客户端正在调用 RPC,此客户端是否有权限调用此 RPC 呢?现在我们将会讲解服务端如何通过 token auth 或 basic auth 来认证客户端
代码已上传到 https://github.com/wangy8961/grpc-go-tutorial/tree/v0.10 ,欢迎 star
1. Bearer Token
假设服务端已经通过安全通道(比如 TLS
连接,详情可参考: https://madmalls.com/blog/category/network-security/ )将值为 some-secret-token
的 token 发送给了客户端
1.1 客户端所有 RPC 调用自动附带 token
(1) grpc.WithPerRPCCredentials
客户端如果想每次调用 RPC
时都自动带上认证凭证的话,可以使用 grpc.WithPerRPCCredentials()
生成 grpc.DialOption
:
// WithPerRPCCredentials returns a DialOption which sets credentials and places // auth state on each outbound RPC. func WithPerRPCCredentials(creds credentials.PerRPCCredentials) DialOption { ... }
(2) credentials.PerRPCCredentials 接口
那么上面的函数的参数类型 credentials.PerRPCCredentials
是啥?它其实是一个接口:
// PerRPCCredentials defines the common interface for the credentials which need to // attach security information to every RPC (e.g., oauth2). type PerRPCCredentials interface { // GetRequestMetadata gets the current request metadata, refreshing // tokens if required. This should be called by the transport layer on // each request, and the data should be populated in headers or other // context. If a status code is returned, it will be used as the status // for the RPC. uri is the URI of the entry point for the request. // When supported by the underlying implementation, ctx can be used for // timeout and cancellation. // TODO(zhaoq): Define the set of the qualified keys instead of leaving // it as an arbitrary string. GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) // RequireTransportSecurity indicates whether the credentials requires // transport security. RequireTransportSecurity() bool }
所以,如果客户端想用 token 认证,并且它每次调用 RPC 时能够 自动 在 gRPC 请求头部中添加 "authorization": "Bearer some-secret-token"
,只需要实现上面的接口即可,比如:
type tokenAuth struct { token string } // Return value is mapped to request headers. func (t *tokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { return map[string]string{ "authorization": "Bearer " + t.token, }, nil } // 是否使用 TLS 安全加密 func (t *tokenAuth) RequireTransportSecurity() bool { return true }
建议启用
TLS
加密,保证 token 和后续双方的交互数据安全传输!
(3) gRPC Client 实现
创建 grpc-go-tutorial/features/authentication/token-auth/client/main.go
文件:
// Package main implements a client for Echo service. package main import ( "context" "flag" "fmt" "log" "google.golang.org/grpc/credentials" pb "github.com/wangy8961/grpc-go-tutorial/features/echopb" "google.golang.org/grpc" ) type tokenAuth struct { token string } // Return value is mapped to request headers. func (t *tokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { return map[string]string{ "authorization": "Bearer " + t.token, }, nil } // 是否使用 TLS 安全加密 func (t *tokenAuth) RequireTransportSecurity() bool { return true } func main() { addr := flag.String("addr", "localhost:50051", "the address to connect to") certFile := flag.String("cacert", "cacert.pem", "CA root certificate") flag.Parse() creds, err := credentials.NewClientTLSFromFile(*certFile, "") if err != nil { log.Fatalf("failed to load CA root certificate: %v", err) } opts := []grpc.DialOption{ // 1. TLS 认证 grpc.WithTransportCredentials(creds), // 2. token 认证 grpc.WithPerRPCCredentials(&tokenAuth{ token: "some-secret-token", }), } // Set up a connection to the server. conn, err := grpc.Dial(*addr, opts...) // To call service methods, we first need to create a gRPC channel to communicate with the server. We create this by passing the server address and port number to grpc.Dial() if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewEchoClient(conn) // Once the gRPC channel is setup, we need a client stub to perform RPCs. We get this using the NewEchoClient method provided in the pb package we generated from our .proto. // Contact the server and print out its response. msg := "madmalls.com" resp, err := c.UnaryEcho(context.Background(), &pb.EchoRequest{Message: msg}) // Now let’s look at how we call our service methods. Note that in gRPC-Go, RPCs operate in a blocking/synchronous mode, which means that the RPC call waits for the server to respond, and will either return a response or an error. if err != nil { log.Fatalf("failed to call UnaryEcho: %v", err) } fmt.Printf("response:\n") fmt.Printf(" - %q\n", resp.GetMessage()) }
1.2 服务端验证 token
(1) metadata.FromIncomingContext
服务端可以在每一个 RPC 方法中使用 metadata.FromIncomingContext(ctx)
从上下文中读取客户端传过来的 token:
// FromIncomingContext returns the incoming metadata in ctx if it exists. The // returned MD should not be modified. Writing to it may cause races. // Modification should be made to copies of the returned MD. func FromIncomingContext(ctx context.Context) (md MD, ok bool) { ... }
token 就保存在 metadata.MD 中:
(2) gRPC Server 实现
创建 grpc-go-tutorial/features/authentication/token-auth/server/main.go
文件:
// Package main implements a server for Echo service. package main import ( "context" "flag" "fmt" "log" "net" "strings" pb "github.com/wangy8961/grpc-go-tutorial/features/echopb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) // server is used to implement echopb.EchoServer. type server struct{} /* func (s *server) UnaryEcho(ctx context.Context, req *pb.EchoRequest) (*pb.EchoResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method UnaryEcho not implemented") } */ func (s *server) UnaryEcho(ctx context.Context, req *pb.EchoRequest) (*pb.EchoResponse, error) { fmt.Printf("--- gRPC Unary RPC ---\n") fmt.Printf("request received: %v\n", req) // md 的值类似于: map[:authority:[192.168.40.123:50051] authorization:[Bearer some-secret-token] content-type:[application/grpc] user-agent:[grpc-go/1.20.1]] md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil, status.Errorf(codes.InvalidArgument, "missing metadata") } fmt.Printf("Type of 'metadata.MD' is %T, and its value is %v \n", md, md) // 1. 判断是否存在 authorization 请求头 authorization, ok := md["authorization"] if !ok { return nil, status.Errorf(codes.Unauthenticated, `missing "Authorization" header`) } fmt.Printf("Type of 'authorization' is %T, and its value is %v \n", authorization, authorization) const prefix = "Bearer "
0 条评论
评论者的用户名
评论时间暂时还没有评论.