|
|
|
|
公众号矩阵

如何使用gRPC和Mutual TLS连接Python和Go应用程序?

本教程通过使用Mutual TLS 身份验证的 gRPC 框架,向你介绍用 Python 和 Go 编写的服务连接的过程。

作者:Pavel Zinovkin来源:hackernoon.com|2021-11-24 16:51

【51CTO.com快译】本教程通过使用Mutual TLS 身份验证的 gRPC 框架,向你介绍用 Python 和 Go 编写的服务连接的过程。因多数开发者对 Python/Django 和 Go 开发比较熟悉,本文将省略大多数无聊的东西,比如使用 Django 应用程序引导 virtualenv 或者如何“ manage.py runserver”它。

引言

有一个Python中的旧系统正在进行大的修改。它是由两部分组成的系统:

  • Webapp是一个使用Django框架构建的面向用户的web应用程序。它充当API客户机,连接到几个节点执行一些操作。
  • 每个节点(服务器)都是一个用Python编写的简单服务器,它驻留在Nginx后面。一些节点位于私有网络之外,通信通过公共网络进行。

在进行一些清理、重构和测试工作之后,客户机基本上满足了它的需求。另一方面,服务器有稳定性和性能问题,所以在Go (Golang)重写服务器是对性能提升很有帮助的解决方案。

而Python和Go之间的通信成了唯一的障碍。

用于客户机和服务器之间通信的现有JSON API是旧的,没有文档记录。正是因为从零开始重建它比试图复兴它更容易。将此API重写为REST/JSON相对容易,但JSON作为交换格式将不能提供Python和Go之间的互换性和类型兼容性。在这两种语言中,类型系统是不同的,要让它工作是很繁琐且容易出错的。

一个更好的解决方案是使用跨平台的序列化格式,比如协议缓冲区(protobuf)。它的构建是为了提供跨平台兼容性,在Python和Go中得到很好的支持,而且它比JSON更小、更快。Protobuf可以与REST API一起使用,以确保编程语言之间的数据互操作性。但是一个更好的解决方案是使用gRPC框架来完全替换旧的API。

gRPC是一个远程过程调用(RPC)框架,它在跨服务通信场景中工作得非常好。它使用协议缓冲区作为接口定义语言(Interface Definition Language, IDL)和消息交换格式。gRPC使用HTTP/2作为传输,并支持传输层安全(TLS)协议,它可以在没有TLS的情况下工作——基本上,这是大多数教程告诉我们的。这样,通信是通过h2c协议完成的,本质上是纯文本HTTP/2,没有TLS加密。然而,当通过公共网络进行通信时,TLS是必需的。考虑到现代安全威胁,TLS甚至应该被考虑用于私有网络连接[1]。

在本系统中,服务到服务的通信不需要区分客户机,也不需要向它们授予不同的权限。尽管如此,确保只有授权的客户机才能与服务器通信是很重要的。使用互TLS (mTLS)作为身份验证机制很容易实现。

通常在TLS中,服务器有证书和公钥/私钥对,而客户端没有。然后,服务器将其证书发送给客户机进行验证。在mTLS中,服务器和客户机都有证书,服务器也验证客户机的证书。只有在这之后,服务器才会授予对客户端[2]的访问权。

让我们创建一个类似的东西——一个简单的Python/Django web服务,它将通过gRPC/mTLS调用Go服务器,并在浏览器中显示结果,并从存储库的结构开始。

代码布局

对于这样的项目,使用单个存储库(monorepo)就不需要共享API模式。对于如何组织代码库,每个人都有自己的偏好,只要记住protobuf编译器,protoc,对如何组织代码有自己的想法。

原型文件的位置会影响编译后的代码。它可能需要对编译器标志进行一些试验来生成工作代码。将原型文件放在带代码的主文件夹之外,这样重新组织代码就不会破坏原型编译。

我建议这样的目录结构:

  1. tree -L 1 -d . 
  2. ├── certs 
  3. ├── client 
  4. ├── proto 
  5. └── server 
  • certs -我们用于自签名证书的公钥基础设施。
  • client - Python/Django web应用程序和gRPC客户端API。它基本上是一个' django-admin startproject client . '的结果,通过剥离配置,因为不需要数据库。
  • proto -是放置gRPC的protobuf源文件的地方。
  • server - Go中的gRPC服务器。

公钥基础设施

要开始使用TLS,您需要客户端和服务器的证书。要创建自签名证书,我建议使用CloudFlare的PKI工具包CFSSL。

首先,您需要创建一个证书颁发机构(CA),该机构将用于为服务器和客户端生成TLS证书。此CA证书还用于在建立TLS连接时验证另一方证书的真实性。

通过JSON文件配置CFSSL,并提供命令来生成默认的配置模板开始:

  1. cd certs 
  2. cfssl print-defaults config > ca-config.json 

默认ca-config。Json提供了足够满足我们需求的配置文件。让我们生成一个CA证书签名请求配置,证书和私钥:

  1. cat > ca-csr.json <<EOF 
  2. "CN": "CA", 
  3. "key": { 
  4. "algo": "ecdsa", 
  5. "size": 256 
  6. }, 
  7. "names": [ 
  8. "C": "US", 
  9. "ST": "CA", 
  10. "L": "San Francisco" 
  11. EOF 
  12.  
  13. cfssl gencert -initca ca-csr.json | cfssljson -bare ca - 

客户端证书、公钥和私钥:

  1. cat > client-csr.json <<EOF 
  2. "CN": "client", 
  3. "key": { 
  4. "algo": "ecdsa", 
  5. "size": 256 
  6. }, 
  7. "names": [ 
  8. "C": "US", 
  9. "ST": "CA", 
  10. "L": "San Francisco" 
  11. EOF 
  12.  
  13. cfssl gencert \ 
  14. -ca=ca.pem \ 
  15. -ca-key=ca-key.pem \ 
  16. -config=ca-config.json \ 
  17. -profile=client client-csr.json | cfssljson -bare client 

服务器的IP地址必须包含在API服务器证书的主题替代名称列表中。这将确保远程客户端可以验证证书。

  1. cat > server-csr.json <<EOF 
  2. "CN": "server", 
  3. "key": { 
  4. "algo": "ecdsa", 
  5. "size": 256 
  6. }, 
  7. "names": [ 
  8. "C": "US", 
  9. "ST": "CA", 
  10. "L": "San Francisco" 
  11. EOF 
  12.  
  13. cfssl gencert \ 
  14. -ca=ca.pem \ 
  15. -ca-key=ca-key.pem \ 
  16. -config=ca-config.json \ 
  17. -hostname=127.0.0.1 \ 
  18. -profile=server server-csr.json | cfssljson -bare server 

Protobuf和gRPC

现在证书已经准备好了,下一步是为gRPC所需的API创建一个模式定义。它将是一个名为DiceService的简单服务,以演示gRPC和mTLS是如何工作的。

下面是proto/api.proto文件。它定义了一个RPC端点RollDie,该端点接受RollDieRequest并在rolldierresponse的值字段中返回滚模的值。

  1. syntax = "proto3"
  2.  
  3. option go_package = "server/api"
  4.  
  5. package api; 
  6.  
  7. message RollDieRequest {} 
  8.  
  9. message RollDieResponse { 
  10. int32 value = 1
  11.  
  12. service DiceService { 
  13. rpc RollDie (RollDieRequest) returns (RollDieResponse) {} 

下一步是使用protobuf编译器- protoc从原型定义中为每种语言生成代码。此外,每种语言都需要自己的一组依赖项。

安装所需的包,并构建Python的原型文件:

  1. pip install grpcio grpcio-tools 
  2. python -m grpc_tools.protoc -I proto --proto_path=proto \ 
  3. --python_out=client/api --grpc_python_out=client/api proto/api.proto 

编译后的文件位于client/api目录中。由于某些原因,protocol的Python编译器在生成的代码中使用了绝对导入[3],它应该是固定的:

  1. cd client/api && cat api_pb2_grpc.py | \ 
  2. sed -E 's/^(import api_pb2.*)/from client.api \1/g' > api_pb2_grpc.tmp && \ 
  3. mv -f api_pb2_grpc.tmp api_pb2_grpc.py 

安装所需的模块和构建原型文件。protoc-gen-go和protoc-gen-go-grpc默认安装在GOBIN目录下。你可以覆盖GOBIN并将其指向virtualenv的bin目录——这使得之后更容易清理。

  1. go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 
  2. go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 
  3. protoc -I. --go_out=. --go-grpc_out=. proto/api.proto 

编译后的文件位于server/api目录中。

Python客户端

为了将其余的代码库与Protobuf/gRPC代码隔离开来,创建一个简单的包装器:api/client.py。该包装器需要CA证书、客户端证书和密钥来建立到提供地址的TLS连接。

  1. import grpc 
  2.  
  3. from . import api_pb2, api_pb2_grpc 
  4.  
  5. class Certs: 
  6. root = None 
  7. cert = None 
  8. key = None 
  9.  
  10. def __init__(self, root, cert, key): 
  11. self.root = open(root, 'rb').read() 
  12. self.cert = open(cert, 'rb').read() 
  13. self.key = open(key, 'rb').read() 
  14.  
  15. class Client: 
  16. rpc = None 
  17.  
  18. def __init__(self, addr: str, crt: Certs): 
  19. creds = grpc.ssl_channel_credentials(crt.root, crt.key, crt.cert) 
  20. channel = grpc.secure_channel(addr, creds) 
  21. self.rpc = api_pb2_grpc.DiceServiceStub(channel) 
  22.  
  23. def roll_die(self) -> int: 
  24. return self.rpc.RollDie(api_pb2.RollDieRequest()).value 

这是如何在web应用程序的视图中使用这个客户端。这里的变量值包含RPC调用的结果。

  1. ICONS = ["?", "⚀", "⚁", "⚂", "⚃", "⚄", "⚅"] 
  2.  
  3. def grpc(request): 
  4. grpc_addr = "127.0.0.1:8443" 
  5.  
  6. crt = api.Certs('certs/ca.pem', 'certs/client.pem', 'certs/client-key.pem') 
  7. try: 
  8. value = api.Client(grpc_addr, crt).roll_die() 
  9. except Exception as e: 
  10. logger.exception(e) 
  11.  
  12. return HttpResponse('Value: ' + ICONS[value]) 

现在,如果你试图通过启动web应用程序并点击相应的视图来执行这段代码,你会得到一个错误。这是预期的-服务器还没有创建。这里有趣的部分是错误——它会说一些关于“连接到所有地址失败”的东西,这并不多。但是设置环境变量GRPC_VERBOSITY=debug会使gRPC输出更加详细,并有助于进行故障排除。它可以在client/settings.py文件中完成,例如:

  1. if DEBUG: 
  2. os.environ['GRPC_VERBOSITY'] = 'debug' 

服务器端

在server/api/server.go中实现DiceService逻辑。它初始化伪随机数生成器,并根据请求返回范围从1到6的随机值。

  1. // Number of dots on a die 
  2. const Dots = 6 
  3.  
  4. type Server struct { 
  5. UnimplementedDiceServiceServer 
  6. rnd *rand.Rand 
  7.  
  8. func NewServer() *Server { 
  9. return &Server{ 
  10. rnd: rand.New(rand.NewSource(time.Now().UnixNano())), 
  11.  
  12. func (s *Server) RollDie(ctx context.Context, req *RollDieRequest) (*RollDieResponse, error) { 
  13. // rand.Intn returns a value in [0, Dots) interval 
  14. value :s.rnd.Intn(Dots) + 1 
  15. return &RollDieResponse{Value: int32(value)}, nil 

服务实现已经准备就绪。下一步是为gRPC服务器提供证书并启动它。你可以把它放在这里服务器/服务器。启用mTLS的一个重要时刻是设置tls。配置{ClientAuth: tls。RequireAndVerifyClientCert},它指示服务器请求并验证客户端的证书。

  1. secureAddress :"127.0.0.1:8443" 
  2.  
  3. serverCert, err :tls.LoadX509KeyPair("certs/server.pem", "certs/server-key.pem") 
  4. if err != nil { 
  5. log.Printf("failed to load server cert/key: %s", err) 
  6. os.Exit(1) 
  7.  
  8. caCert, err :ioutil.ReadFile("certs/ca.pem") 
  9. if err != nil { 
  10. log.Printf("failed to load CA cert: %s", err) 
  11. os.Exit(1) 
  12.  
  13. caCertPool :x509.NewCertPool() 
  14. caCertPool.AppendCertsFromPEM(caCert) 
  15. creds :credentials.NewTLS(&tls.Config{ 
  16. Certificates: []tls.Certificate{serverCert}, 
  17. ClientCAs: caCertPool, 
  18. ClientAuth: tls.RequireAndVerifyClientCert, 
  19. }) 
  20.  
  21. secureSrv :grpc.NewServer(grpc.Creds(creds)) 
  22. log.Printf("Starting gRPC server, address=%q", secureAddress) 
  23. lis, err :net.Listen("tcp", secureAddress) 
  24. if err != nil { 
  25. log.Printf("failed to listen: %s", err) 
  26. os.Exit(1) 
  27.  
  28. api.RegisterDiceServiceServer(secureSrv, api.NewServer()) 
  29. if err :secureSrv.Serve(lis); err != nil { 
  30. log.Printf("failed to serve: %s", err) 
  31. os.Exit(1) 

现在使用运行server/server运行服务器。去,确保web应用程序运行和访问它的url -你应该看到RPC请求的结果。gRPC服务器不会记录任何关于传入请求的信息,而且很难通过查看服务器的输出来判断发生了什么。

幸运的是,有一个API可以拦截RPC请求的执行,您可以使用它添加类似于任何HTTP服务器的日志记录。它接近于Django中间件的工作方式。下面是一个简单的日志拦截器。要使用它,您需要将它传递给grpc.NewServer。

  1. func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 
  2. ts :time.Now() 
  3.  
  4. peer, ok :peer.FromContext(ctx) 
  5. if !ok { 
  6. return nil, status.Errorf(codes.InvalidArgument, "missing peer") 
  7. md, ok :metadata.FromIncomingContext(ctx) 
  8. if !ok { 
  9. return nil, status.Errorf(codes.InvalidArgument, "missing metadata") 
  10.  
  11. res, err :handler(ctx, req) 
  12.  
  13. log.Printf("server=%q ip=%q method=%q status=%s duration=%s user-agent=%q", 
  14. md[":authority"][0], 
  15. peer.Addr.String(), 
  16. info.FullMethod, 
  17. status.FromContextError(err).Code(), 
  18. time.Since(ts), 
  19. md["user-agent"][0], 
  20. return res, err 
  21.  
  22. ... 
  23.  
  24. secureSrv :grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(loggingInterceptor)) 

看来目标已经达到了。客户端通过gRPC与服务器通信,由于mTLS,连接是安全的,并且是相互验证的。但请记住,该服务器非常基础,需要进行一些工作和加固,才能在公共网络上的生产环境中使用它。

让我们把服务器放在Nginx后面

另一种方法是把Nginx放在服务器之前,我更喜欢它,而不是把Go服务暴露在互联网上。开箱即用,你将获得所有经过战斗测试的功能,如负载平衡和速率限制,它还将减少你需要编写和支持的代码数量。

Nginx从1.13.10版本开始就支持gPRC,并且可以终止、检查和路由gRPC方法调用。所以让我们在服务器之前添加Nginx,让它处理mTLS和通过未加密的HTTP/2的代理请求。这个设置有点复杂,所以这里是图表:

让我们从客户机中的另一个视图开始。它将使用不同的端口号。

  1. def nginx(request): 
  2. nginx_addr = "127.0.0.1:9443" 
  3.  
  4. crt = api.Certs('certs/ca.pem', 'certs/client.pem', 'certs/client-key.pem') 
  5. try: 
  6. value = api.Client(nginx_addr, crt).roll_die() 
  7. except Exception as e: 
  8. logger.exception(e) 
  9.  
  10. return HttpResponse('Value: ' + ICONS[value]) 

因为Nginx将完成所有围绕TLS的工作,所以没有必要在服务器代码中为gRPC服务器提供证书:

  1. insecureAddress :"127.0.0.1:50051" 
  2. insecureSrv :grpc.NewServer(grpc.UnaryInterceptor(loggingInterceptor)) 
  3. log.Printf("Starting gRPC server (h2c), address=%q", insecureAddress) 
  4. lis, err :net.Listen("tcp", insecureAddress) 
  5. if err != nil { 
  6. log.Printf("failed to listen: %s", err) 
  7. os.Exit(1) 
  8.  
  9. api.RegisterDiceServiceServer(insecureSrv, api.NewServer()) 
  10. if err :insecureSrv.Serve(lis); err != nil { 
  11. log.Printf("failed to serve: %s", err) 
  12. os.Exit(1) 

Nginx的配置文件:Nginx .conf这个配置禁用了妖魔化,并启动了一个记录到stdout的进程。这对于演示目的来说更方便。

NoneBashCSSCC#GoHTMLJavaJavaScriptJSONPHPPowershellPythonRubySQLTypeScriptYAMLCopy

  1. events { 
  2. worker_connections 1024; 
  3.  
  4. # Do not use it in production! 
  5. daemon off; 
  6. master_process off; 
  7.  
  8. http { 
  9.  
  10. upstream grpcservers { 
  11. server 127.0.0.1:50051; 
  12.  
  13. server { 
  14. listen 9443 ssl http2; 
  15.  
  16. error_log /dev/stdout; 
  17. access_log /dev/stdout; 
  18.  
  19. # Server's tls config 
  20. ssl_certificate certs/server.pem; 
  21. ssl_certificate_key certs/server-key.pem; 
  22.  
  23. # mTLS part 
  24. ssl_client_certificate certs/ca.pem; 
  25. ssl_verify_client on; 
  26.  
  27. location / { 
  28. grpc_pass grpc://grpcservers; 

开启Nginx。

  1. nginx -p $(pwd) -c nginx.conf 

确保所有的服务都启动了,并访问之前创建的视图的URL——您应该看到RPC请求的结果。如果有些东西不能工作-检查这篇文章在GitHub上的代码。

【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】

鸿蒙官方战略合作共建——HarmonyOS技术社区

【责任编辑:黄显东 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

带你轻松入门 RabbitMQ

带你轻松入门 RabbitMQ

轻松入门RabbitMQ
共4章 | loong576

58人订阅学习

数据湖与数据仓库的分析实践攻略

数据湖与数据仓库的分析实践攻略

助力现代化数据管理:数据湖与数据仓库的分析实践攻略
共3章 | 创世达人

14人订阅学习

云原生架构实践

云原生架构实践

新技术引领移动互联网进入急速赛道
共3章 | KaliArch

43人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微