美文网首页
Dive into gRPC(3):安全通信

Dive into gRPC(3):安全通信

作者: 起名难倒英雄汉 | 来源:发表于2018-09-25 20:13 被阅读0次

    前一篇文章中,我们使用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(3):安全通信

          本文链接:https://www.haomeiwen.com/subject/chxroftx.html