Dive into gRPC(3):安全通信

在前一篇文章中,我们使用gRPC从零开始定义了一个服务。在这篇文章中,我们更近一步,在这个服务中添加TLS特性。

1. 制作证书

客户端和服务器通过gRPC相互通信,由于我们使用了protobuf来序列化和反序列化消息,因此消息的数据是二进制形式的。但是我们的通信是明文传输的,这在一些安全需求较高的场景中是不允许的,因此需要使用安全传输。幸好,在gRPC中,我们可以直接使用SSL/TLS,用来验证服务器,并对通信过程进行加密。

首先,需要生成证书。

1.1 生成私钥

在我们的simplemath项目目录中新建目录cert,进入这个目录,执行下面的命令来生成私钥:

$ openssl genrsa -out server.key 2048

我们使用openssl genrsa命令来生成私钥,并用-out选项指定输出。最后一个参数2048表示的是生成密钥的位数,如果没有指定,那么默认就是512位。

1.2 根据私钥生成CSR

如果想从一个认证中心(Certificate Authority,CA)获取一个SSL证书,我们需要生成一个证书签名请求(Certificate Signing Reqeusts,CRSs)。一个CSR主要包含钥匙对中的公钥,以及其它一些重要的信息。

我们可以根据前面生成的私钥生成一个CSR:

$ openssl req -new -sha256 -key server.key -out server.csr

执行上面的命令后,需要完成一些信息的填写,主要有:

Country Name (2 letter code) []:
State or Province Name (full name) []:
Locality Name (eg, city) []:
Organization Name (eg, company) []:
Organizational Unit Name (eg, section) []:
Common Name (eg, fully qualified host name) []:
Email Address []:

填写完这些信息后,就会生成一个证书签名请求(server.csr)。

1.3 生成证书

如果想使用一个SSL证书来对通信进行加密,但是不需要使用CA签字的证书,那么我们可以生成一个自签名的证书。

使用前面生成的私钥(server.key)以及证书签名请求(server.csr),我们可以生成一个自签名的证书:

$ openssl x509 -req -sha256 -in server.csr -signkey server.key -out server.crt -days 3650

选项-x509指定req来生成一个自签名的证书。-days 3650指定了证书的有效期是3650天。-signkey指定了私钥,而-in指定了证书签名请求。

这样,就能生成一个自签名的证书(server.crt)。

此时,整个项目的目录结构如下:

 $GOPATH                        // your $GOPATH folder
 |__src                         // the source folder
    |__simplemath               // the simplemath project folder
       |__cert                  // the certificate folder
       |  |__server.key         // the private key
       |  |__server.csr         // the certificate signing request
       |  |__server.crt         // the self-signed certificate
       |__client                // the client folder stores the client codes
       |  |__rpc                // the rpc folder stores the call function
       |  |  |__simplemath.go   // the logic code for remote call
       |  |__main.go            // client program goes from here
       |  |__client             // the executeable client file
       |__api                   // folder that stores .proto and .pb.go files
       |  |__simplemath.proto   // file defines the messages and services
       |  |__simplemath.pb.go   // file generated by protoc
       |__server                // the server folder stores the server codes
          |__rpcimpl            // the rpcimpl folder stores the logic codes
          |  |__simplemath.go   // the logic code related to simplemath
          |__main.go            // server program goes from here
          |__server             // the executable server file
    ...

证书生成后,就可以在我们的代码中加入TLS了。

2. Server+TLS

gRPC协议本身没什么变动,主要的变动就是在gRPC对象的生成,包括服务器和客户端。如果只改动一边的话是不成功的,需要两端都修改。

2.1 修改服务器端代码

在服务器端,需要修改main.go,代码如下:

package main

import (
    "google.golang.org/grpc"
    // import the credentials package
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/reflection"
    "log"
    "net"
    pb "simplemath/api"
    "simplemath/server/rpcimpl"
)

const (
    port = ":50051"
)

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    // create the TLS credentials from files
    creds, err := credentials.NewServerTLSFromFile("../cert/server.crt", "../cert/server.key")
    if err != nil {
        log.Fatalf("could not load TLS keys: %s", err)
    }
    // create a gRPC option array with the credentials
    opts := []grpc.ServerOption{grpc.Creds(creds)}
    // create a gRPC server object with server options(opts)
    s := grpc.NewServer(opts...)
    pb.RegisterSimpleMathServer(s, &rpcimpl.SimpleMathServer{})
    reflection.Register(s)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

2.2 我们变了什么?

这个代码和之前的有什么变化呢?

首先,为了使用TLS,我们需要导入google.golang.org/credentials包。

然后,使用我们之前生成的私钥(server.key)以及自签名证书(server.crt)构建一个credentials对象creds

由于这个TLS是在我们原来的服务器上增加的特性,所以需要创建一个grpc.ServerOption,参数就是我们刚才创建的creds

最后,我们将创建的grpc.ServerOption传入生成grpc服务器的构造函数grpc.NewServer()中。

注意grpc.NewServer()函数是一个可变数量参数的函数,我们可以传入零个或多个参数(grpc.ServerOption),以后我们会用到更多。

2.3 注意Common Name

有一个需要注意的问题是,我们在生成服务器时绑定的地主必须和在生成CSR时候填写的Common Name信息一致,不然会出现下面的错误:

2018/09/25 17:04:02 cound not compute: rpc error: code = Unavailable desc = all SubConns are in TransientFailure, latest connection error: connection error: desc = "transport: authentication handshake failed: x509: certificate is valid for example.com, not localhost"

就是说,我们在生成CSR是填写的信息是example.com,但是使用的时候却是localhost,那么这是认证不通过的。因此,我们在生成CSR的时候,Common Name信息需要填成localhost

2.4 The First Try

当我们只修改服务器端,而客户端使用的是之前的版本,那么会出现如下的错误:

2018/09/25 16:58:43 cound not compute: rpc error: code = Unavailable desc = transport is closing

连接失败。因为两端都需要修改。

3. Client+TLS

在客户端这一部分,我们需要使用相同的证书来创建一个grpc客户端。由于我们是在client/rpc/simplemath.go中创建grpc客户端并建立连接,因此需要修改这个文件:

ackage rpc

import (
    "golang.org/x/net/context"
    // import credentials package
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc"
    "log"
    pb "simplemath/api"
    "strconv"
    "time"
)

const (
    address = "localhost:50051"
)

func GreatCommonDivisor(first, second string) {
    // create the client TLS credentials
    creds, err := credentials.NewClientTLSFromFile("../cert/server.crt", "")
    // initiate a connection with the server using creds
    conn, err := grpc.Dial(address, grpc.WithTransportCredentials(creds))
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewSimpleMathClient(conn)
    a, _ := strconv.ParseInt(first, 10, 32)
    b, _ := strconv.ParseInt(second, 10, 32)
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.GreatCommonDivisor(ctx, &pb.GCDRequest{First: int32(a), Second: int32(b)})
    if err != nil {
        log.Fatalf("could not compute: %v", err)
    }
    log.Printf("The Greatest Common Divisor of %d and %d is %d", a, b, r.Result)
}

和服务器端类似,我们使用credentials.NewClientTLSFromFile()函数创建一个credentials对象creds,并用这个对象创建连接。这个函数也是可变数量参数的函数,可以用来指定多个参数,用来指导建立连接时的行为。

注意,在创建creds的时候我们没有用到私钥(server.key),从名字可知,这个私钥是服务器的。

这样,客户端和服务器端都使用了credentials,因此可以通过加密的方式进行通信了。

和原来一样,我们编译并运行服务器和客户端的代码,结果如下:

2018/09/25 18:11:26 The Greatest Common Divisor of 12 and 15 is 3

对话成功。

To Be Continued~

4. 系列目录

  • Dive into gRPC(1):gRPC简介
  • Dive into gRPC(2):实现一个服务
  • Dive into gRPC(3):安全通信
  • Dive into gRPC(4):Streaming
  • Dive into gRPC(5):验证客户端
  • Dive into gRPC(6):metadata

你可能感兴趣的:(Dive into gRPC(3):安全通信)