Golang中使用gRPC实现数据加密的最佳实践

1. gRPC简介

gRPC是一种高性能、开源和通用的远程过程调用(RPC)框架,由Google主要面向服务端应用开发,基于标准HTTP/2设计,它可以让客户端应用像调用本地对象一样直接调用另一台服务器上的方法,从而让您更容易地创建分布式应用和服务。gRPC支持多种编程语言,包括C++、Java、Python、PHP、Objective-C,以及最近推出的Golang。

1.1 RPC

RPC(Remote Procedure Call protocol,远程过程调用协议)是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。通过RPC,开发人员可以与另外一个地址空间(通常是一个不同的机器)交互,就像调用本地程序一样,而不需要显式的网络编程。

RPC协议优点:

RPC使构建分布式计算应用变得容易

RPC抽象出高层通信概念使代码更易于维护与编写

RPC使得设计分布式应用更加简单

1.2 HTTP/2

HTTP2是HTTP协议的下一代标准,目前主流浏览器和服务器均支持HTTP/2,gRPC使用HTTP2作为底层协议。相比HTTP/1.1,HTTP/2主要优点在于:

通过多路复用技术,实现一个连接上可以并行交错多个请求和响应,从而减少TCP连接数,提高响应速度。

引入header compression技术,压缩header大小,减少请求/响应头大小,从而减少网络拥塞。

支持Server Push,此功能允许服务端通过一个请求发起多个资源的传输,加速了应用程序加载时间。

2. gRPC安装

安装gRPC:

go get -u google.golang.org/grpc

安装protobuf:

go get -u github.com/golang/protobuf/{proto,protoc-gen-go}

安装后,即可用Go的protobuf插件protoc-gen-go自动生成gRPC接口代码。

3. gRPC数据加密

gRPC消息传输是基于HTTP/2协议的,因此可以通过TLS来提供数据加密。服务器和客户端可以使用开放源代码的Let's Encrypt证书,或者商业证书,如Digicert证书等,通过证书实现数据传输的加密。

3.1 TLS证书生成

可以通过openssl命令生成自签名证书,该证书足以为个人、内部使用提供加密保护。以下是示例命令:

// 生成私钥

openssl genpkey -algorithm RSA -out server.key

openssl genpkey -algorithm RSA -out client.key

// 根据私钥生成公钥

openssl rsa -pubout -in server.key -out server.pub

openssl rsa -pubout -in client.key -out client.pub

// 在server.crt中,CN项需要设置成gRPC服务器的主机名或IP,如果是localhost则判断为Server.net.IP{"127.0.0.1","::1"}未授权访问

openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650

// 由于server.crt是自签名的,所以需要将其根证书导入client.truststore中

// -import -alias指定证书别名(不要使用默认值),-file指定证书文件路径

keytool -import -trustcacerts -alias "gRPC Server Cert" -file server.crt -keystore client.truststore

// 验证

openssl s_client --connect 127.0.0.1:50051 --cert ./client.crt --key ./client.key --cafile ./server.crt

3.2 gRPC数据加密实现

gRPC建立在HTTP/2之上,因此TLS配置需要添加到HTTP/2的监听器中。以下是示例代码:

package main

import (

"context"

"crypto/tls"

"crypto/x509"

"fmt"

"github.com/golang/protobuf/ptypes/empty"

"google.golang.org/grpc"

"google.golang.org/grpc/credentials"

"io/ioutil"

"net"

"net/http"

pb "grpc_example/proto"

"log"

)

const (

PORT = ":50051"

)

func sayHello(ctx context.Context, request *empty.Empty) (*pb.HelloReply, error) {

return &pb.HelloReply{Message: "Hello world!"}, nil

}

func main() {

// 读取证书和密钥

cert, _ := tls.LoadX509KeyPair("server.crt", "server.key")

certPool := x509.NewCertPool()

ca, _ := ioutil.ReadFile("server.crt")

certPool.AppendCertsFromPEM(ca)

// 构建TLS配置

tlsConfig := &tls.Config{

Certificates: []tls.Certificate{cert},

ClientCAs: certPool,

ClientAuth: tls.RequireAndVerifyClientCert,

}

opts := []grpc.ServerOption{

// 构建gRPC服务器TLS凭证

grpc.Creds(credentials.NewTLS(tlsConfig)),

}

// 新建gRPC服务器

server := grpc.NewServer(opts...)

pb.RegisterGreeterServer(server, &server{})

lis, err := net.Listen("tcp", PORT)

if err != nil {

log.Fatalf("failed to listen: %v", err)

}

log.Printf("Server is listening on port %v...", PORT)

if err := server.Serve(lis); err != nil {

log.Fatalf("failed to serve: %v", err)

}

}

gRPC客户端代码:

package main

import (

"context"

"crypto/tls"

"crypto/x509"

"fmt"

pb "grpc_example/proto"

"io/ioutil"

"log"

"time"

"google.golang.org/grpc"

"google.golang.org/grpc/credentials"

"google.golang.org/grpc/peer"

)

const (

address = "localhost:50051"

)

func main() {

// 读取证书和密钥

cert, err := tls.LoadX509KeyPair("client.crt", "client.key")

if err != nil {

log.Fatalf("Failed to load client certificate and key. Error: %v", err)

}

RootCABytes, err := ioutil.ReadFile("server.crt")

if err != nil {

log.Fatalf("Failed to read server root CA certificate. Error: %v", err)

}

pool := x509.NewCertPool()

pool.AppendCertsFromPEM(RootCABytes)

// 连接gRPC服务器

conn, err := grpc.Dial(address, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{

ServerName: "grpc_service",

Certificates: []tls.Certificate{cert},

RootCAs: pool,

})))

if err != nil {

log.Fatalf("Failed to establish gRPC connection. Error: %v", err)

}

defer func() {

if err = conn.Close(); err != nil {

log.Fatalf("Failed to close gRPC connection. Error: %v", err)

}

}()

// 新建gRPC客户端

client := pb.NewGreeterClient(conn)

// 调用gRPC服务

r, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "world"})

if err != nil {

log.Fatalf("Failed to call SayHello. Error: %v", err)

}

log.Printf("Server response: %s", r.Message)

}

3.3 防重放攻击实现

由于TLS能够提供加密保护,但前提是Ca证书没有被替换和dns欺骗的情况下,不过还有一种攻击情况:防重放攻击。防重放攻击是指攻击者截获客户端发送的有效请求,并在攻击者操纵的独立通道上重新发送它,这时候服务端认为是另外一个客户端发出的并给其重新处理。为了避免防重放攻击,在客户端每次请求时,在请求中增加nonce(一次性令牌),然后在服务端进行校验,过滤掉已经被处理过的数据包。以下是客户端和服务端代码:

package main

import (

"bytes"

"context"

"crypto/rand"

"encoding/binary"

"crypto/tls"

"crypto/x509"

"errors"

"fmt"

pb "grpc_example/proto"

"io/ioutil"

"log"

"time"

"google.golang.org/grpc"

"google.golang.org/grpc/credentials"

"google.golang.org/grpc/peer"

)

const (

address = "localhost:50051"

)

func main() {

// 读取证书和密钥

cert, err := tls.LoadX509KeyPair("client.crt", "client.key")

if err != nil {

log.Fatalf("Failed to load client certificate and key. Error: %v", err)

}

RootCABytes, err := ioutil.ReadFile("server.crt")

if err != nil {

log.Fatalf("Failed to read server root CA certificate. Error: %v", err)

}

pool := x509.NewCertPool()

pool.AppendCertsFromPEM(RootCABytes)

// 连接gRPC服务器

conn, err := grpc.Dial(address, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{

ServerName: "grpc_service",

Certificates: []tls.Certificate{cert},

RootCAs: pool,

})))

if err != nil {

log.Fatalf("Failed to establish gRPC connection. Error: %v", err)

}

defer func() {

if err = conn.Close(); err != nil {

log.Fatalf("Failed to close gRPC connection. Error: %v", err)

}

}()

// 新建gRPC客户端

client := pb.NewGreeterClient(conn)

// 发送消息

err = SendRequest(client)

if err != nil {

log.Fatalf("Failed to send request. Error: %v", err)

}

}

func SendRequest(client pb.GreeterClient) (err error) {

// 生成随机数

var nonce int64

binary.Read(rand.Reader, binary.BigEndian, &nonce)

// 填充请求metadata

var header *pb.Header

header = &pb.Header{Nonce: nonce}

log.Printf("Sending request (%v)...", header)

// 调用gRPC服务

ctx := context.Background()

ctx = MetadataInjector(ctx, header)

response, err := client.SayHello(ctx, &pb.HelloRequest{Name: "world"})

if err != nil {

return fmt.Errorf("failed to call SayHello. Error: %v", err)

}

log.Printf("Server response: %s\n", response.Message)

return nil

}

// 自定义客户端拦截器

func MetadataInjector(ctx context.Context, header *pb.Header) context.Context {

md, ok := metadata.FromIncomingContext(ctx)

if !ok {

md = metadata.New(nil)

}

var buffer bytes.Buffer

err := binary.Write(&buffer, binary.BigEndian, header)

if err != nil {

log.Fatalln("Failed to encode header.", err)

}

md["header-bin"] = [][]byte{buffer.Bytes()}

return metadata.NewOutgoingContext(ctx, md)

}

服务端验证请求中的nonce,并过滤掉已经处理过的请求。以下是服务端代码:

package main

import (

"bytes"

"context"

"crypto/tls"

"crypto/x509"

"encoding/binary"

"io/ioutil"

"log"

"net"

"time"

pb "grpc_example/proto"

"google.golang.org/grpc"

"google.golang.org/grpc/credentials"

"google.golang.org/grpc/metadata"

)

var processedNonces = make(map[int64]bool)

type server struct{}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {

md, ok := metadata.FromIncomingContext(ctx)

if !ok {

return nil, errors.New("Failed to extract metadata from context.")

}

if len(md["header-bin"]) == 0 {

return nil, errors.New("No metadata 'header-bin' found in request.")

}

// 解码header

headerData := md["header-bin"][0]

var header pb.Header

err := binary.Read(bytes.NewReader(headerData), binary.BigEndian, &header)

if err != nil {

return nil, errors.New("Failed to decode header. Error: " + err.Error())

}

log.Printf("Received request: %s (nonce: %v)...", in.Name, header.Nonce)

// 如果nonce已经处理过,返回错误

if processedNonces[header.Nonce] {

return nil, errors.New("Nonce has already been processed.")

} else {

processedNonces[header.Nonce] = true

}

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

}

const (

PORT = ":50051"

)

func main() {

// 读取证书和密钥

cert, _ := tls.LoadX509KeyPair("server.crt", "server.key")

certPool := x509.NewCertPool()

ca, _ := ioutil.ReadFile("server.crt")

certPool.AppendCertsFromPEM(ca)

// 构建TLS配置

tlsConfig := &tls.Config{

Certificates: []tls.Certificate{cert},

ClientCAs: certPool,

ClientAuth: tls.RequireAndVerifyClientCert,

}

opts := []grpc.ServerOption{

// 构建gRPC服务器TLS凭证

grpc.Creds(credentials.NewTLS(tlsConfig)),

}

// 新建gRPC服务器

server := grpc.NewServer(opts...)

// 创建gRPC服务

pb.RegisterGreeterServer(server, &server{})

lis, err := net.Listen("tcp", PORT)

if err != nil {

log.Fatalf("failed to listen: %v", err)

}

log.Printf("Server is listening on port %v...", PORT)

if err := server.Serve(lis); err != nil {

log.Fatalf("failed to serve: %v", err)

}

}

4. 总结

本文主要介绍了gRPC的基础知识,并以数据加密为例,介绍了如何在gRPC中使用TLS对消息进行加密保护,以及如何避免防重放攻击,对于gRPC开发和运维人员来说是不可或缺的重要知识。

后端开发标签