Go微服务实战|第10章:gRPC Authentication

  • 原创
  • Madman
  • /
  • /
  • 0
  • 1722 次阅读

Go 微服务实战.png

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 中:

// MD is a mapping from metadata keys to values.
type MD map[string][]string

(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 "

    // 2. 如果存在 authorization 请求头的话,则 md["authorization"] 是一个 []string
    if !strings.HasPrefix(authorization[0
                                
                            
未经允许不得转载: LIFE & SHARE - 王颜公子 » Go微服务实战|第10章:gRPC Authentication

分享

作者

作者头像

Madman

如需 Linux / Python 相关问题付费解答,请按如下方式联系我

0 条评论

暂时还没有评论.

专题系列