0. 概述

好吧,可能你会觉得很奇怪,为什么上面的 6 和 8 之间夹杂了一个 7: FAQs。嗯,对于这个我很抱歉,本来我想将这篇内容写到 03 中的,但是,后面发现不合适,所以就没办法了,现在补充一个 8 来完善这个内容。同时,我认为这篇内容对于 GRPC 来说,还是很重要的,重要程度到什么程度呢?谁知道呢,反正我是额外写了一篇文章来介绍它。

在开始 GRPC 的安全之前,我希望你已经了解了 TLS 以及相关的内容,并且会创建自签名的 SSL 证书或者公签的 SSL 证书,如果你对自签名证书不太会的话,不妨看下我之前写的一篇:Nginx SSL 双向认证,key 生成和配置。在开始下面的内容之前,我希望你已经准备好了以下的文件:

1. 一个普通的 CS 代码

要想使用 TLS,中间可能会出现各种问题,例如证书错误啊,这些,要想解决好这些问题,那么要先有个可以正常工作的简单 CS 代码很重要,所以这里我还是会使用到我们这个系列的旧代码,先把他们跑起来,这份原始的代码可以从这个版本的这个目录下查找:点击跳转

这份代码的效果就是这样的:

[[email protected]]# go run server-01.go
request: {
        "name": "lucifer"
}
response: {
        "message": "Hello: lucifer, Welcome to https://liqiang.io"
}

[[email protected]]# go run client.go
2019/06/12 22:24:20 [D] resp: Hello: lucifer, Welcome to https://liqiang.io

下面开始进行 TLS 的添加,TLS 顾名思义,就是传输层的加密,所以应该不会上升到应用层之中,那么也就是说我们的业务代码应该是无感知的,所以这个应该是在初始化 Transport 的时候设置。

2. 设置 Server 端的 TLS

Server 端的前半部分还是和普通的是一样的:

// create by https://liqiang.io
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
    log.Fatalf("failed to listen: %v", err)
}

crt := "/tmp/server.crt"
key := "/tmp/server.key"
creds, err := credentials.NewServerTLSFromFile(crt, key)
if err != nil {
    panic(err)
}

grpcServer := grpc.NewServer(grpc.Creds(creds))

但是,中间加上了一个创建 credentials 的过程,并且把它用于创建 Server 对象,放在了 NewServer 的第二个参数。设置完服务端之后,我们是时候来设置一下客户端的选项了,其实差别还是在于创建 Connection 的时候:

cert := "/tmp/server.crt"
creds, err := credentials.NewClientTLSFromFile(cert, "local.liqiang.io")
if err != nil {
    panic(err)
}

conn, err := grpc.Dial("local.liqiang.io:8080", grpc.WithTransportCredentials(creds))
if err != nil {
    panic(err)
}
defer conn.Close()

可以看到,第一步还是需要创建 credentials,然后,再通过这个 credentials 来创建和 Server 端的连接,然后后面就和普通的一样了。还是一样,这些完整代码你都可以再我的 代码仓库 中找到。

现在我们再来尝试一下代码:

[[email protected]]# go run server-02.go

[[email protected]]# go run client-02.go
2019/06/12 22:24:20 [D] resp: Hello: lucifer, Welcome to https://liqiang.io

因为我把 Server 端的输出去掉了,所以这里是没有输出了,但是客户端访问是正常了,那么表示我们的 TLS 是设置成功啦。

3. 验证客户端的 TLS

虽然,我们服务端可以要求客户端必须以安全的方式来连接,但是,这只能让客户端知道,它请求的服务端是正常的,是不会被伪造的,但是,有个问题就是,如果双方是对等的,那么,服务端怎么知道客户端就是允许被访问服务端的终端呢?这样描述你可能不太理解,换种说法,我是一个 SAAS 供应商,想要使用我的服务都需要先在我这里注册,那么你在使用我的服务的时候,你肯定会用 TLS 来确保,你访问的服务是正版的,而不是某个黑客伪造的服务器用于盗取数据;同样的,我作为服务提供商,我不能让任意的人都可以连接我的服务,我只允许我认识的客户端才能连接我的服务,虽然这可以在应用层面通过帐号密码来控制,但是,在传输层,也可以通过 TLS 来控制。

这里,我就要求我的客户端使用的证书,必须是通过我的 CA 认证过的,也就是说,客户端连接的时候,同时也要向我出示公钥,然后我通过我的 CA 验证一下,通过了才继续连接,不通过则拒绝服务。

为了实现这个目的,第一步肯定还是得先修改一下服务端:

// created by https://liqiang.io
var (
    certificate tls.Certificate
    certPool    = x509.NewCertPool()
)
if certificate, err = tls.LoadX509KeyPair(“ServerCertFile”, “ServerKeyFile”); err != nil {
    return errors.Wrap(err, "load tls files")
}
var bs []byte
if bs, err = os.ReadFile(“CaCertFile”); err != nil {
    return errors.Wrap(err, "load client ca cert")
}
if ok := certPool.AppendCertsFromPEM(bs); !ok {
    return errors.Wrap(err, "append client certs")
}

creds := credentials.NewTLS(&tls.Config{
        ServerName:   "local.liqiang.io",
        Certificates: []tls.Certificate{certificate},
        RootCAs:      certPool,
})

除了添加服务器监听得证书之外,还需要额外得添加一个 RootCAs,这里只是将关键得地方写出来,更完整得代码可以通过代码仓库查看。然后看下客户端的,其实客户端的代码已经和服务端的 TLS 代码基本一致了,因为我这里是设置了 客户端 和 服务器 互相验证,这样,其实双方的代码就会很相似了:

// created by https://liqiang.io
    var dialOpts grpc.DialOption
    if opts.MutualTls {
        var (
            certificate tls.Certificate
            certPool    *x509.CertPool
        )
        if certificate, err = tls.LoadX509KeyPair(opts.ClientCertFile, opts.ClientKeyFile); err != nil {
            return errors.Wrap(err, "load client certs")
        }

        certPool = x509.NewCertPool()
        var bs []byte
        if bs, err = ioutil.ReadFile(opts.CaCertFile); err != nil {
            return errors.Wrap(err, "read server ca cert")
        }

        if ok := certPool.AppendCertsFromPEM(bs); !ok {
            return errors.Wrap(err, "append server certs")
        }

        transportCreds := credentials.NewTLS(&tls.Config{
            ServerName:   opts.ServerName,
            Certificates: []tls.Certificate{certificate},
            RootCAs:      certPool,
        })
        dialOpts = grpc.WithTransportCredentials(transportCreds)
    } else {
        dialOpts = grpc.WithInsecure()
    }

    var mux = runtime.NewServeMux(
        runtime.WithMarshalerOption(
            runtime.MIMEWildcard, &runtime.JSONPb{
                OrigName:     true,
                EmitDefaults: true,
            }))
    var grpcOpts = []grpc.DialOption{
        dialOpts,
    }

最后还是运行一下代码:

[[email protected]]# go run server-03.go


[[email protected]]# go run client-03.go
2019/06/12 22:24:20 [D] resp: Hello: lucifer, Welcome to https://liqiang.io

如期望中运行!

4. Reference