• SSL/TLS 是一个安全协议,它通过一系列的手段、一系列的算法让客户端与服务端之间加密传输数据,避免数据被攻击者窃听。

TLS 生成

openssl

  1. 官网地址:https://www.openssl.org/source/
  2. 由于 linux 自带了 openssl,所以跳过了安装,如果没有请自行安装。

生成证书

1
2
3
4
5
6
7
8
# 1. 生成私钥
openssl genrsa -out server.key 2048

# 2. 生成证书 全部回车即可,可以不填
openssl req -new -x509 -key server.key -out server.crt -days 36500

# 3. 生成 csr 全部回车即可,可以不填
openssl req -new -key server.key -out server.csr
  1. 更改 openssl.cnf(windows上是 openssl.cfg),通过 openssl version -a 的 OPENSSLDIR 为根目录,默认为 /etc/pki/tls。
    • 复制一份到本地:cp /etc/pki/tls/openssl.cnf /root/workspace/learn-grpc/key/
1
2
3
4
1. 找到 [ CA_default ],打开 copy_extensions = copy,去掉前面井号。
2. 找到 [ req ],打开 req_extensions = v3_req,去掉前面井号。
3. 找到 [ v3_req ],添加 subjectAltName = @alt_names。
4. 添加新的标签 [ alt_names ],和标签字段 DNS.1 = *.heliu.site。配置多个添加 DNS.2、DNS.3等。
  1. 生成证书私钥。
1
2
3
4
5
6
7
8
# 生成证书私钥 test.key
openssl genpkey -algorithm RSA -out test.key

# 通过私钥test.key生成证书请求文件test.csr
openssl req -new -nodes -key test.key -out test.csr -days 3650 -subj "/C=cn/OU=myorg/O=mycomp/CN=myname" -config ./openssl.cnf -extensions v3_req

# 生成SAN证书 pem
openssl x509 -req -days 365 -in test.csr -out test.pem -CA server.crt -CAkey server.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
  1. 最终的 test.key(RSA 的私钥,用来进行数字签名) 和 test.pem(自签名的服务端证书,其中包含与私钥对应的公钥、网站域名、签名算法等信息) 文件。

github

  1. Go 示例代码:https://github.com/helium-chain/grpc-demo-ssl

直接使用根证书

服务端

 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
func main() {
    // 绝对地址
    // 方法一
    creds, err1 := credentials.NewServerTLSFromFile(
        "/root/workspace/learn-grpc/key/test.pem",
        "/root/workspace/learn-grpc/key/test.key",
    )

    if err1 != nil {
        fmt.Printf("证书错误:%v", err1)
        return
    }

    // 方法二
    //cert, err := tls.LoadX509KeyPair("", "")
    //if err != nil {
    //	fmt.Printf("私钥错误:%v", err)
    //	return
    //}
    //
    //creds := credentials.NewServerTLSFromCert(&cert)
    // 开启端口
    listen, _ := net.Listen("tcp", ":9090")
    // 创建grpc服务
    grpcServer := grpc.NewServer(grpc.Creds(creds),
        grpc.UnaryInterceptor(interceptor.UnaryServerInterceptor()),
        grpc.StreamInterceptor(interceptor.StreamServerInterceptor())) // 在grpc服务端中注册我们自己编写的服务
    pb.RegisterSayHelloServer(grpcServer, &server{})

    // 启动服务
    err := grpcServer.Serve(listen)
    if err != nil {
        fmt.Println(err)
        return
    }
}

客户端

  1. NewClientTLSFromFile指定使用 CA 证书来校验服务端的证书有效性。注意:第二个参数域名就是服务端证书时的 CN 参数
  2. 建立连接时 指定建立安全连接WithTransportCredentials。
 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
84
85
86
87
88
89
90
func main() {
    // 配置ssl,"*.heliu.site"在实际开发中从浏览器中取获取,证书路径使用绝对路径
    creds, _ := credentials.NewClientTLSFromFile(
        "/root/workspace/learn-grpc/key/test.pem",
        "*.heliu.site",
    )

    var opts []grpc.DialOption
    // 不带TLS这里是grpc.WithTransportCredentials(insecure.NewCredentials())
    opts = append(opts, grpc.WithTransportCredentials(creds))
    opts = append(opts, grpc.WithPerRPCCredentials(&ClientTokenAuth{}))
    // 添加客户端拦截器
    opts = append(opts, grpc.WithUnaryInterceptor(interceptor.UnaryClientInterceptor()))
    // 添加流拦截器
    opts = append(opts, grpc.WithStreamInterceptor(interceptor.StreamClientInterceptor()))

    // 连接server端,使用ssl加密通信
    conn, err := grpc.NewClient("127.0.0.1:9090", opts...)
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }

    defer conn.Close()

    // 建立连接
    client := pb.NewSayHelloClient(conn)

    fmt.Printf("now-Time: %s\n", time.Now().Format(time.DateTime))
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    var header metadata.MD
    var tr metadata.MD

    var opts1 []grpc.CallOption = []grpc.CallOption{
        grpc.WaitForReady(false),
        grpc.Header(&header),
        grpc.Trailer(&tr),
    }

    // 执行rpc调用(这个方法在服务器端来实现并返回结构)
    resp, err := client.SayHello(ctx, &pb.HelloRequest{RequestName: "gh", Age: 12}, opts1...)

    if err != nil {
        fmt.Printf("%v\n", err)
        return
    }

    fmt.Printf("Response-Header: %v\n", header)

    fmt.Println(resp.GetResponseMsg())

    fmt.Printf("Response-Trailer: %v\n", tr)

    // 客户端调用Channel方法,获取返回的流对象
    stream, err := client.Channel(context.Background())
    if err != nil {
        log.Fatalf("error creating stream: %v", err)
    }

    // 在客户端将发送和接收放到两个独立的 goroutine

    // 向服务器发送数据:
    go func() {
        for {
            req := &pb.Request{
                Value: "张三",
            }

            if err := stream.Send(req); err != nil {
                log.Fatalf("error sending message: %v", err)
                return
            }
            time.Sleep(time.Second)
        }
    }()

    // 然后再循环中接收服务端返回的数据
    for {
        reply, err := stream.Recv()
        if err != nil {
            if err == io.EOF {
                break
            }
            log.Fatalf("error receiving message: %v", err)
            return
        }
        fmt.Printf("Received: %s\n", reply.GetValue())
    }
}

参考

  1. 写给go开发者的gRPC教程-安全