GRPC - 认证

gRPC 默认提供了两种认证方式(可以混合使用):

  • 基于SSL/TLS认证方式
  • 远程调用认证方式

TLS认证示例

这里直接扩展前面那个hello示例,实现TLS认证机制

首先需要准备证书,在hello目录新建keys目录用于存放证书文件。

证书制作

制作私钥 ( .key )

1
2
3
4
5
6
# Key considerations for algorithm "RSA" ≥ 2048-bit
openssl genrsa -out keys/server.key 2048

# Key considerations for algorithm "ECDSA" ≥ secp384r1
# List ECDSA the supported curves (openssl ecparam -list_curves)
openssl ecparam -genkey -name secp384r1 -out keys/server.key

自签名公钥( x509 ) ( PEM-encodings .pem|.crt )

1
openssl req -new -x509 -sha256 -key keys/server.key -out keys/server.pem -days 3650

自定义信息

1
2
3
4
5
6
7
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:GuangDong
Locality Name (eg, city) []:ShenZhen
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:Dev
Common Name (e.g. server FQDN or YOUR name) []:dev.liusha.me
Email Address []:itchenyi@gmail.com

修改服务端代码:server/main.go

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
package main

import (
"os"
"fmt"
"net"

// 引入编译生成的包
pb "liusha.me/example/proto"

"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)

const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)

// 定义helloService并实现约定的接口
type helloService struct{}

// HelloService ...
var HelloService = helloService{}

func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
resp := new(pb.HelloReply)
resp.Message = "Hello " + in.Name + "."

return resp, nil
}

func main() {
listen, err := net.Listen("tcp", Address)
if err != nil {
fmt.Println("failed to listen:", err)
os.Exit(1)
}

// TLS 认证
creds, err := credentials.NewServerTLSFromFile("./keys/server.pem", "./keys/server.key")
if err != nil {
fmt.Println("Failed to generate credentials:", err)
os.Exit(1)
}

// 实例化grpc Server
s := grpc.NewServer(grpc.Creds(creds))

// 注册HelloService
pb.RegisterHelloServer(s, HelloService)

fmt.Println("Listen on " + Address)

s.Serve(listen)
}

运行:

1
2
3
go run hello/server/main.go

// 服务端在实例化grpc Server时,可配置多种选项,TLS认证是其中之一

客户端添加TLS认证:client/main.go

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
package main

import (
"os"
"fmt"

// 引入proto包
pb "liusha.me/example/proto"

"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)

const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)

func main() {
// TLS连接
creds, err := credentials.NewClientTLSFromFile("./keys/server.pem", "dev.liusha.me")
if err != nil {
fmt.Println("Failed to create TLS credentials:", err)
os.Exit(1)
}

// 连接
conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds))

if err != nil {
fmt.Println(err)
os.Exit(1)
}

defer conn.Close()

// 初始化客户端
c := pb.NewHelloClient(conn)

// 调用方法
reqBody := new(pb.HelloRequest)
reqBody.Name = "gRPC"
r, err := c.SayHello(context.Background(), reqBody)
if err != nil {
fmt.Println(err)
os.Exit(1)
}

fmt.Println(r.Message)
}

运行:

客户端添加TLS认证的方式和服务端类似,在创建连接Dial时,同样可以配置多种选项,后面的示例中会看到更多的选项。

1
2
3
go run hello/client/main.go

Hello gRPC.

Token认证示例

客户端实现: client/main.go

这里我们定义了一个 customCredential 结构,并实现了两个方法 GetRequestMetadataRequireTransportSecurity 。这是 gRPC 提供的自定义认证方式,每次RPC调用都会传输认证信息。 customCredential 其实是实现了 grpc/credential 包内的 PerRPCCredentials 接口。每次调用, token 信息会通过请求的 metadata 传输到服务端。

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
73
74
75
76
77
78
79
80
81
82
package main

import (
"os"
"fmt"

// 引入proto包
pb "liusha.me/example/proto"

"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)

const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"

// OpenTLS 是否开启TLS认证
OpenTLS = true
)

// customCredential 自定义认证
type customCredential struct{}

func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appid": "101010",
"appkey": "i am key",
}, nil
}

func (c customCredential) RequireTransportSecurity() bool {
if OpenTLS {
return true
}

return false
}

func main() {
var err error
var opts []grpc.DialOption

if OpenTLS {
// TLS连接
creds, err := credentials.NewClientTLSFromFile("./keys/server.pem", "dev.liusha.me")
if err != nil {
fmt.Println("Failed to create TLS credentials:", err)
os.Exit(1)
}
opts = append(opts, grpc.WithTransportCredentials(creds))
} else {
opts = append(opts, grpc.WithInsecure())
}

// 使用自定义认证
opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))

conn, err := grpc.Dial(Address, opts...)

if err != nil {
fmt.Println(err)
os.Exit(1)
}

defer conn.Close()

// 初始化客户端
c := pb.NewHelloClient(conn)

// 调用方法
reqBody := new(pb.HelloRequest)
reqBody.Name = "gRPC"
r, err := c.SayHello(context.Background(), reqBody)
if err != nil {
fmt.Println(err)
os.Exit(1)
}

fmt.Println(r.Message)
}

修改 server/main.go 中的 SayHello 方法:

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
73
74
75
76
77
78
79
80
81
82
83
package main

import (
"os"
"fmt"
"net"
"errors"

// 引入编译生成的包
pb "liusha.me/example/proto"

"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/codes"
)

const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)

// 定义helloService并实现约定的接口
type helloService struct{}

// HelloService ...
var HelloService = helloService{}

func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
// 解析metada中的信息并验证
md, ok := metadata.FromContext(ctx)
if !ok {
return nil, fmt.Errorf("Err%d: 无Token认证信息", codes.Unauthenticated)
}

var (
appid string
appkey string
)

if val, ok := md["appid"]; ok {
appid = val[0]
}

if val, ok := md["appkey"]; ok {
appkey = val[0]
}

if appid != "101010" || appkey != "i am key" {
return nil, fmt.Errorf("Err%d: Token认证信息无效: appid=%s, appkey=%s", codes.Unauthenticated, appid, appkey)
}

resp := new(pb.HelloReply)
resp.Message = fmt.Sprintf("Hello %s.\nToken info: appid=%s,appkey=%s", in.Name, appid, appkey)

return resp, nil
}

func main() {
listen, err := net.Listen("tcp", Address)
if err != nil {
fmt.Println("failed to listen:", err)
os.Exit(1)
}

// TLS 认证
creds, err := credentials.NewServerTLSFromFile("./keys/server.pem", "./keys/server.key")
if err != nil {
fmt.Println("Failed to generate credentials:", err)
os.Exit(1)
}

// 实例化grpc Server
s := grpc.NewServer(grpc.Creds(creds))

// 注册HelloService
pb.RegisterHelloServer(s, HelloService)

fmt.Println("Listen on " + Address)

s.Serve(listen)
}

运行:

1
2
3
4
5
6
7
8
9
10
11
#: 认证成功
go run hello/client/main.go

Hello gRPC.
Token info: appid=101010,appkey=i am key

#: 认证失败
go run hello/client/main.go

rpc error: code = Unknown desc = Err16: Token认证信息无效: appid=101010, appkey=i am key
exit status 1