1. 用户认证,简单来说就是验证请求的用户身份,避免破坏者伪造身份获取他人数据隐私。比如当访问微博网站时,微博服务端通过用户认证来识别你的身份,并返回正确的主页数据。
  2. 用户认证有很多方式。例如HTTP中使用的cookie、session、oauth、jwt等等。gRPC框架并不限制用户认证的方式,而是提供了开放的能力来支持各种各样的用户认证。
  3. gRPC的用户认证可以用两句话总结:
    • gRPC客户端提供在每一次调用注入用户凭证的能力。
    • gRPC服务端使用拦截器来验证每一个客户端的请求。
  4. gRpc 提供了一个接口,这个接口位于credentials包下,这个接口需要客户端实现。
    • 第一个方法用作获取元数据信息,也就是客户端提供的 key/value 对,context用于控制超时和取消,uri是请求入口处的uri。
    • 第二个方法的作用是否需要基于 TLS 认证进行安全传输,如果返回是true,则必须加上TLS验证,返回值是false则不用。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 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 (restricted to an allowable set of codes as defined by gRFC
    // A54). 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. Additionally, RequestInfo data will be available via ctx
    // to this call.  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
}
  1. gRPC将各种认证方式浓缩统一到一个凭证(credentials)上,可以单独使用一种凭证,比如只使用LTS凭证或者只使用自定义凭证,也可以多种凭证组合,gRpc提供统一的API验证机制,使研发人员使用方便。
  2. github Go代码:https://github.com/helium-chain/grpc-tls-token

Basic Authentication

  1. Basic Authentication是最简单的认证方式。
  2. 使用Basic Authentication时,客户端携带一个Authorization header头,值为Basic + 空格 + base64编码的用户名:密码,例如一个用户名和密码都是admin,那么header头如下 Authorization: Basic YWRtaW46YWRtaW4=
  3. 通常并不推荐使用Basic Authentication,gRPC也没有内置组件支持,但在gRPC中很容易做到。
1
2
3
4
5
6
type PerRPCCredentials interface {
    // GetRequestMetadata 获取当前请求的metadata
    GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
    // RequireTransportSecurity 是否使用安全的传输协议
    RequireTransportSecurity() bool
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var _ credentials.PerRPCCredentials = BasicAuthentication{}

type BasicAuthentication struct {
    password string
    username string
}

func (b BasicAuthentication) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    auth := b.username + ":" + b.password
    enc := base64.StdEncoding.EncodeToString([]byte(auth))

    return map[string]string{
    "authorization": "Basic " + enc,
    }, nil
}

func (b BasicAuthentication) RequireTransportSecurity() bool {
    return true
}
  1. 在创建连接时使用grpc.WithPerRPCCredentials(auth)设置每一次请求的用户凭证。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func main() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    auth := BasicAuthentication{
        username: "admin",
        password: "admin",
    }

    creds, err := credentials.NewClientTLSFromFile("./x509/rootCa.crt", "www.example.com")
    if err != nil {
        panic(err)
    }

    conn, err := grpc.Dial("localhost:8009",
    grpc.WithTransportCredentials(creds),
    grpc.WithPerRPCCredentials(auth))
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    client := pb.NewOrderManagementClient(conn)

    // Get Order
    retrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"})
    if err != nil {
        panic(err)
    }

    log.Print("GetOrder Response -> : ", retrievedOrder)
}
  1. RequireTransportSecurity()代表是否使用安全的传输协议。如果设置了true,则必须通过grpc.WithTransportCredentials()设置合理的传输层加密方式,否则会导致建立连接时失败。
  2. gRPC官方库里有个insecure.NewCredentials(),这段函数含义为禁用传输层安全协议,因此grpc.WithTransportCredentials(insecure.NewCredentials())是无效的,依旧会导致建立连接时失败。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auth := BasicAuthentication{
    username: "admin",
    password: "admin",
}

conn, err := grpc.Dial("localhost:8009",
                       grpc.WithTransportCredentials(insecure.NewCredentials()),
                       grpc.WithPerRPCCredentials(auth))
if err != nil {
    panic(err)
}
$ go run basic-authentication/client/main.go
panic: grpc: the credentials require transport level security (use grpc.WithTransportCredentials() to set)
  1. 服务端使用拦截器来验证请求是否合法,对于不合法的token返回codes.Unauthenticated,如果token合法,在ensureValidBasicCredentials中调用handler来继续请求的处理。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var (
 errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata")
 errInvalidToken    = status.Errorf(codes.Unauthenticated, "invalid credentials")
)

func ensureValidBasicCredentials(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, errMissingMetadata
    }

    authorization := md["authorization"]

    if len(authorization) < 1 {
        return nil, errInvalidToken
    }

    token := strings.TrimPrefix(authorization[0], "Basic ")
    if token != base64.StdEncoding.EncodeToString([]byte("admin:admin")) {
        return nil, errInvalidToken
    }

    return handler(ctx, req)
}

func main() {
    l, err := net.Listen("tcp", ":8009")
        if err != nil {
        panic(err)
    }

    creds, err := credentials.NewServerTLSFromFile("./x509/server.crt", "./x509/server.key")
    s := grpc.NewServer(
        grpc.UnaryInterceptor(ensureValidBasicCredentials),
        grpc.Creds(creds),
    )

    pb.RegisterOrderManagementServer(s, &server{})

    if err := s.Serve(l); err != nil {
        panic(err)
    }
}
  1. github 参考代码:https://github.com/helium-chain/grpc-test01

JWT

  1. 客户端的token应该是由服务端返回的,而不是客户端自己生成的,这里只是为了方便演示,主要逻辑是声明claims然后使用secret key进行签名。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package main

import (
 "context"
 "log"
 "time"

 "github.com/golang-jwt/jwt/v4"
 pb "github.com/liangwt/note/grpc/authentication/ecommerce"
 "google.golang.org/grpc"
 "google.golang.org/grpc/credentials"
 "google.golang.org/protobuf/types/known/wrapperspb"
)

var _ credentials.PerRPCCredentials = JwtAuthentication{}

type JwtAuthentication struct {
    Key []byte
}

func (a JwtAuthentication) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    // Create a new token object, specifying signing method and the claims
    // you would like it to contain.
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
    ID:        "example",
    ExpiresAt: jwt.NewNumericDate(time.Now().Add(-2 * time.Hour)),
    })

    // Sign and get the complete encoded token as a string using the secret
    tokenString, err := token.SignedString(a.Key)
    if err != nil {
    return nil, err
    }

    return map[string]string{
    "authorization": "Bearer " + tokenString,
    }, nil
}

func (b JwtAuthentication) RequireTransportSecurity() bool {
    return true
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    creds, err := credentials.NewClientTLSFromFile("./x509/rootCa.crt", "www.example.com")
    if err != nil {
    panic(err)
    }

    jwtAuth := JwtAuthentication{[]byte("154a8b3aa89d3d4c49826f6dbbbe5542b5a9fbbb")}

    conn, err := grpc.Dial("localhost:8009",
    grpc.WithTransportCredentials(creds),
    grpc.WithPerRPCCredentials(jwtAuth))
    if err != nil {
    panic(err)
    }
    defer conn.Close()

    client := pb.NewOrderManagementClient(conn)

    // Get Order
    retrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"})
    if err != nil {
    panic(err)
    }

    log.Printf("GetOrder Response -> : %+v\n", retrievedOrder)
}
  1. 服务端代码使用拦截器,来对jwt进行验证。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main

import (
 "context"
 "fmt"
 "net"
 "strings"

 "github.com/golang-jwt/jwt/v4"
 pb "github.com/liangwt/note/grpc/authentication/ecommerce"
 "google.golang.org/grpc"
 "google.golang.org/grpc/codes"
 "google.golang.org/grpc/credentials"
 "google.golang.org/grpc/metadata"
 "google.golang.org/grpc/status"
)

var (
    errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata")
)

func ensureValidBasicCredentials(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
    return nil, errMissingMetadata
    }

    tokenString := strings.TrimPrefix(md["authorization"][0], "Bearer ")

    token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
    // Don't forget to validate the alg is what you expect:
    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
    return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
    }

    return []byte("154a8b3aa89d3d4c49826f6dbbbe5542b5a9fbbb"), nil
    })

    claims, ok := token.Claims.(*jwt.RegisteredClaims)
    if !ok || !token.Valid {
    return nil, status.Errorf(codes.Unauthenticated, err.Error())
    }

    fmt.Println(claims.ID)

    return handler(ctx, req)
}

func main() {
    l, err := net.Listen("tcp", ":8009")
    if err != nil {
    panic(err)
    }

    creds, err := credentials.NewServerTLSFromFile("./x509/server.crt", "./x509/server.key")
    s := grpc.NewServer(
    grpc.UnaryInterceptor(ensureValidBasicCredentials),
    grpc.Creds(creds),
    )

    pb.RegisterOrderManagementServer(s, &server{})

    if err := s.Serve(l); err != nil {
    panic(err)
    }
}
  1. 自定义实现jwt推荐使用github.com/golang-jwt/jwt。

参考

  1. 写给go开发者的gRPC教程-用户认证