1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-27 23:12:20 +00:00
2018-07-14 17:37:56 +08:00

9.7 KiB
Raw Blame History

4.5. GRPC进阶

作为一个基础的RPC框架安全和扩展是经常遇到的问题。本节将简单介绍如何对GRPC进行安全认证。然后介绍通过GRPC等截取器特性以及如何通过截取器优雅低实现Token认证、调用跟踪以及Panic捕获等特性。最后介绍了GRPC服务如何和其他Web服务共存以及如何将一个GRPC服务发布为REST服务。

证书认证

GRPC建立在HTTP2协议之上对TLS提供了很好的支持。我们前面章节中GRPC的服务都没有提供证书支持因此客户端在链接服务器中通过grpc.WithInsecure()选项跳过了对服务器证书的验证。没有启用证书的GRPC服务在和客户端进行的是明文通讯信息面临被任何第三方监听的风险。为了保障GRPC通信不被第三方监听我们可以对服务器期待TLS加密特性。

可以用以下命令为服务器和客户端分别生成私钥和证书:

$ openssl genrsa -out server.key 2048
$ openssl req -new -x509 -days 3650 \
	-subj "/C=GB/L=China/O=grpc-server/CN=server.grpc.io" \
	-key server.key -out server.crt

$ openssl genrsa -out client.key 2048
$ openssl req -new -x509 -days 3650 \
	-subj "/C=GB/L=China/O=grpc-client/CN=client.grpc.io" \
	-key client.key -out client.crt

以上命令将生成server.key、server.crt、client.key和client.crt四个文件。其中以.key后缀名的时私钥文件需要妥善保管。其以.crt为后缀名是证书文件也可以简单理解为公钥文件并不需要秘密保存。在subj参数中的/CN=server.grpc.io表示服务的名字为server.grpc.io,在验证服务器的证书时需要用到该信息。

有了证书之后我们就可以在启动GRPC服务时传入证书选项参数

func main() {
	creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
	if err != nil {
		log.Fatal(err)
	}

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

	...
}

其中credentials.NewServerTLSFromFile函数是从文件为服务器构造证书对象然后通过grpc.Creds(creds)函数将证书包装为选项后作为参数输入grpc.NewServer函数。

在客户端基于服务器的证书和服务器名字就可以对服务器进行验证:

func main() {
	creds, err := credentials.NewClientTLSFromFile("server.crt", "server.grpc.io")
	if err != nil {
		log.Fatal(err)
	}

	conn, err := grpc.Dial("localhost:5000", grpc.WithTransportCredentials(creds))
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	...
}

其中redentials.NewClientTLSFromFile是构造客户端用的证书对象第一个参数时服务器的证书文件第二个参数是签发服务器证书时的名字。然后通过grpc.WithTransportCredentials(creds)将证书对象转为参数选项传人grpc.Dial函数。

以上这种方式,需要提前将服务器的证书告知客户端,这样客户端在链接服务器时才能进行对服务器证书认证。在复杂的网络环境中,服务器证书的传输本身也是一个非常危险的问题。如果在中间某个环节,服务器证书被替换那么对服务器的认证也将不再可靠。

为了避免证书的传递过程中被串改,可以通过一个安全可靠的根证书分别对服务器和客户端的证书进行签名。这样客户端或服务器在收到对方的证书后可以通过根证书进行验证证书的有效性。

根证书的生成方式和签名的自签名证书的生成方式类似:

$ openssl genrsa -out ca.key 2048
$ openssl req -new -x509 -days 3650 \
	-subj "/C=GB/L=China/O=gobook/CN=github.com" \
	-key ca.key -out ca.crt

然后是重新对服务器端证书进行签名:

$ openssl req -new \
	-subj "/C=GB/L=China/O=server/CN=server.io" \
	-key server.key \
	-out server.csr
$ openssl x509 -req -sha256 \
	-CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 \
	-in server.csr \
	-out server.crt

签名的过程中引入了一个新的以.csr为后缀名的文件它表示证书签名请求文件。在证书签名完成之后可以删除.csr文件。

然后在客户端就可以基于CA证书对服务器进行证书验证

func main() {
	certificate, err := tls.LoadX509KeyPair(client_crt, client_key)
	if err != nil {
		log.Fatal(err)
	}

	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile(ca)
	if err != nil {
		log.Fatal(err)
	}
	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		log.Fatal("failed to append ca certs")
	}

	creds := credentials.NewTLS(&tls.Config{
		Certificates:       []tls.Certificate{certificate},
		ServerName:         tlsServerName, // NOTE: this is required!
		RootCAs:            certPool,
	})

	conn, err := grpc.Dial("localhost:5000", grpc.WithTransportCredentials(creds))
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	...
}

在新的客户端代码中我们不再直接依赖服务器端证书文件。在credentials.NewTLS函数调用中客户端通过引入一个CA根证书和服务器的名字来实现对服务器进行验证。客户端在链接服务器时会首先请求服务器的证书然后使用CA根证书对收到服务器端证书进行验证。

如果客户端的证书也采用CA根证书签名的话服务器端也可以对客户端进行证书认证。我们用CA根证书对客户端证书签名

$ openssl req -new \
	-subj "/C=GB/L=China/O=client/CN=client.io" \
	-key client.key \
	-out client.csr
$ openssl x509 -req -sha256 \
	-CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 \
	-in client.csr \
	-out client.crt

因为引入了CA根证书签名在启动服务器时同样要配置根证书

func main() {
	certificate, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		log.Fatal(err)
	}

	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile("ca.crt")
	if err != nil {
		log.Fatal(err)
	}
	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		log.Fatal("failed to append certs")
	}

	creds := credentials.NewTLS(&tls.Config{
		Certificates: []tls.Certificate{certificate},
		ClientAuth:   tls.RequireAndVerifyClientCert, // NOTE: this is optional!
		ClientCAs:    certPool,
	})

	server := grpc.NewServer(grpc.Creds(creds))
	...
}

服务器端同样改用credentials.NewTLS函数生成证书通过ClientCAs选项CA根证书并通过ClientAuth选项启用对客户端进行验证。

到此我们就实现了一个服务器和客户端进行双向证书验证的通信可靠的GRPC系统。

Token认证

前面讲述的基于证书的认证是针对每个GRPC链接的认证。GRPC还为每个GRPC方法调用提供了认证支持这样就可以基于不同的用户对不同对方法访问进行权限管理。

要实现对每个GRPC方法进行认证需要实现grpc.PerRPCCredentials接口

type PerRPCCredentials interface {
    // GetRequestMetadata gets the current request metadata, refreshing
    // tokens if required. This should be called by the transport layer on
    // each request, and the data should be populated in headers or other
    // context. If a status code is returned, it will be used as the status
    // for the RPC. uri is the URI of the entry point for the request.
    // When supported by the underlying implementation, ctx can be used for
    // timeout and cancellation.
    // TODO(zhaoq): Define the set of the qualified keys instead of leaving
    // it as an arbitrary string.
    GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
    // RequireTransportSecurity indicates whether the credentials requires
    // transport security.
    RequireTransportSecurity() bool
}

我们可以创建一个Authentication类型用于实现用户名和密码对认证

type Authentication struct {
	User     string
	Password string
}

func (a *Authentication) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
	return map[string]string{"login": a.Login, "password": a.Password}, nil
}
func (a *Authentication) RequireTransportSecurity() bool {
	return false
}

在GetRequestMetadata方法中返回认证需要对必要信息。为了演示代码简单RequireTransportSecurity方法表示不要求底层使用安全链接。在真实对环境中建议底层必须要求启用安全对链接否则认证信息有泄露和被串改的风险。

func main() {
	auth := Authentication{
		Login:    "gopher",
		Password: "password",
	}

	conn, err := grpc.Dial("localhost"+port, grpc.WithInsecure(), grpc.WithPerRPCCredentials(&auth))
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	...
}

通过grpc.WithPerRPCCredentials函数将Authentication对象转为grpc.Dial参数。因为这里没有启用安全链接需要传人grpc.WithInsecure()表示忽略证书认证。

然后在GRPC服务端的每个方法中通过Authentication类型的Auth方法进行身份认证

type grpcServer struct { auth *Authentication }

func (p *grpcServer) SomeMethod(ctx context.Context, in *HelloRequest) (*HelloReply, error) {
	if err := p.auth.Auth(ctx); err != nil {
		return nil, err
	}

	return &HelloReply{Message: "Hello " + in.Name}, nil
}

func (a *Authentication) Auth(ctx context.Context) error {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return fmt.Errorf("missing credentials")
	}

	var appid string
	var appkey string

	if val, ok := md["login"]; ok { appid = val[0] }
	if val, ok := md["password"]; ok { appkey = val[0] }

	if appid != a.Login || appkey != a.Password {
		return grpc.Errorf(codes.Unauthenticated, "invalid token")
	}

	return nil
}

首先通过metadata.FromIncomingContext从ctx上下文中获取元信息然后取出相应的认证信息进行认证。