在前一篇文章中,我们使用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