【.com快速翻译】本教程将带您完成连接到用Python编写的服务的过程,并使用双向TLS身份验证通过gRPC框架。由于大多数开发人员更熟悉Python/Django和Go开发,本文将省略大部分无聊的内容,例如使用Django应用程序引导virtualenv或如何“manage.pyrunserver”。简介Python中有一个遗留系统正在进行大修。它是一个由两部分组成的系统:Webapp是一个使用Django框架构建的面向用户的Web应用程序。它充当API客户端,连接到多个节点以执行一些操作。每个节点(服务器)都是一个用Python编写的简单服务器,位于Nginx后面。一些节点位于专用网络之外,通信通过公共网络进行。在经过一些清理、重构和测试之后,客户端基本上可以做它需要做的事情。另一方面,服务端存在稳定性和性能问题,所以用Go(Golang)重写服务端是对性能提升非常有帮助的解决方案。而Python和Go之间的通信成为了唯一的障碍。用于客户端和服务器之间通信的现有JSONAPI是旧的且没有文档记录。正是因为从头开始重建它比尝试重振它更容易。将此API重写为REST/JSON相对容易,但JSON作为一种交换格式不会提供Python和Go之间的互换性和类型兼容性。两种语言的类型系统不同,让它工作起来既乏味又容易出错。更好的解决方案是使用协议缓冲区(protobuf)等跨平台序列化格式。它专为跨平台兼容性而构建,在Python和Go中得到很好的支持,并且比JSON更小更快。Protobuf可以与RESTAPI一起使用,以确保编程语言之间的数据互操作性。但更好的解决方案是使用gRPC框架来完全替换旧的API。gRPC是一个远程过程调用(RPC)框架,在跨服务通信场景中非常有效。它使用ProtocolBuffers作为接口定义语言(IDL)和消息交换格式。gRPC使用HTTP/2作为传输并支持传输层安全(TLS)协议,它可以在没有TLS的情况下工作——基本上,这就是大多数教程告诉我们的。这样,通信是通过h2c协议完成的,本质上是纯文本HTTP/2,没有TLS加密。但是,通过公共网络进行通信时需要TLS。鉴于现代安全威胁,甚至应该考虑将TLS用于专用网络连接[1]。在这个系统中,服务到服务的通信不需要区分客户或授予他们不同的权限。尽管如此,重要的是要确保只有授权的客户端才能与服务器通信。使用双向TLS(mTLS)作为身份验证机制很容易实现这一点。通常在TLS中,服务器有证书和公钥/私钥对,但客户端没有。服务器然后将其证书发送给客户端进行验证。在mTLS中,服务端和客户端都有证书,服务端也会验证客户端的证书。只有在这之后,服务器才会授予客户端访问权限[2]。让我们创建一个类似的东西——一个简单的Python/DjangoWeb服务,它将通过gRPC/mTLS调用Go服务器并在浏览器中显示结果,从存储库的结构开始。代码布局对于像这样的项目,使用单个存储库(monorepo)消除了对共享API模式的需要。每个人对如何组织代码库都有自己的偏好,请记住,protobuf编译器protoc对如何组织代码有自己的想法。proto文件的位置会影响编译后的代码。可能需要对编译器标志进行一些实验才能生成工作代码。将原型文件与代码放在主文件夹之外,这样重新组织代码就不会破坏原型编译。我建议这样的目录结构:tree-L1-d..├──certs├──client├──proto└──servercerts-我们用于自签名证书的公钥基础设施。客户端-Python/Django网络应用程序和gRPC客户端API。这基本上是'django-adminstartprojectclient'的结果。',通过剥离配置,因为不需要数据库。proto-是放置gRPC的protobuf源文件的地方。server-Go中的gRPC服务器。公钥基础设施要开始使用TLS,您需要客户端和服务器证书。要创建自签名证书,我建议使用CloudFlare的PKI工具包CFSSL。首先,您需要创建一个证书颁发机构(CA),用于为服务器和客户端生成TLS证书。这个CA证书也用于建立TLS连接时验证对方证书的真实性。通过JSON文件配置CFSSL,提供生成默认配置模板的命令启动:cdcertscfsslprint-defaultsconfig>ca-config.jsondefaultca-config。Json提供了足够的配置文件来满足我们的需求。让我们生成一个CA证书签名请求配置、证书和私钥:cat>ca-csr.json<client-csr.json<server-csr.json<api_pb2_grpc.tmp&&\mv-fapi_pb2_grpc.tmpapi_pb2_grpc.py安装所需的模块并构建原型文件。protoc-gen-go和protoc-gen-go-grpc默认安装在GOBIN目录下。您可以覆盖GOBIN并将其指向virtualenv的bin目录——这使得之后的清理工作更容易。goinstallgoogle.golang.org/protobuf/cmd/protoc-gen-go@latestgoinstallgoogle.golang.org/grpc/cmd/protoc-gen-go-grpc@latestprotoc-I。--go_out=。--go-grpc_out=.proto/api.proto的编译文件位于server/api目录。Python客户端要将其余代码库与Protobuf/gRPC代码隔离开来,请创建一个简单的包装器:api/client.py。此包装器需要CA证书、客户端证书和密钥才能与提供的地址建立TLS连接。importgrpcfrom.importapi_pb2,api_pb2_grpcclassCerts:root=Nonecert=Nonekey=Nonedef__init__(self,root,cert,key):self.root=open(root,'rb').read()self.cert=open(cert,'rb').read()self.key=open(key,'rb').read()classClient:rpc=Nonedef__init__(self,addr:str,crt:Certs):creds=grpc.ssl_channel_credentials(crt.root,crt.key,crt.cert)channel=grpc.secure_channel(addr,creds)self.rpc=api_pb2_grpc.DiceServiceStub(channel)defroll_die(self)->int:returnsself.rpc.RollDie(api_pb2.RollDieRequest()).value这是怎么做的在Web应用程序的视图中使用此客户端。这里的变量值包含RPC调用的结果。ICONS=["?","?","?","?","?","?","?"]defgrpc(请求):grpc_addr="127.0.0.1:8443"crt=api.Certs('certs/ca.pem','certs/client.pem','certs/client-key.pem')try:value=api.Client(grpc_addr,crt).roll_die()exceptExceptionase:logger.exception(e)returnHttpResponse('Value:'+ICONS[value])现在,如果您尝试通过启动Web应用程序并单击相应的视图来执行此代码,您将收到错误消息。这是预期的-服务器尚未创建。这里有趣的部分是错误-它会说一些关于“连接到所有地址失败”的信息,这并不多。但是设置环境变量GRPC_VERBOSITY=debug会使gRPC输出更加冗长并有助于故障排除。可以在client/settings.py文件中完成,例如:ifDEBUG:os.environ['GRPC_VERBOSITY']='debug'服务器端在server/api/server.go中实现DiceService逻辑。它初始化一个伪随机数生成器,并根据请求返回一个范围从1到6的随机值。//NumberofdotsonadieconstDots=6typeServerstruct{UnimplementedDiceServiceServerrnd*rand.Rand}funcNewServer()*Server{return&Server{rnd:rand.New(rand.NewSource(time.Now().UnixNano())),}}func(s*Server)RollDie(ctxcontext.Context,req*RollDieRequest)(*RollDieResponse,error){//rand.Intn返回savaluein[0,Dots)intervalvalue:=s.rnd.Intn(Dots)+1return&RollDieResponse{Value:int32(value)},nil}服务实现就绪。下一步是向gRPC服务器提供证书并启动它。你可以把它放在这里服务器/服务器。启用mTLS的一个重要时刻是设置tls。配置{ClientAuth:tls.RequireAndVerifyClientCert},它指示服务器请求并验证客户端的证书。secureAddress:="127.0.0.1:8443"serverCert,err:=tls.LoadX509KeyPair("certs/server.pem","certs/server-key.pem")iferr!=nil{log.Printf("failedtoloadservercert/key:%s",err)os.Exit(1)}caCert,err:=ioutil.ReadFile("certs/ca.pem")iferr!=nil{log.Printf("failedtoloadCAcert:%s",err)os.Exit(1)}caCertPool:=x509.NewCertPool()caCertPool.AppendCertsFromPEM(caCert)creds:=credentials.NewTLS(&tls.Config{Certificates:[]tls.Certificate{serverCert},ClientCAs:caCertPool,ClientAuth:tls。RequireAndVerifyClientCert,})secureSrv:=grpc.NewServer(grpc.Creds(creds))log.Printf("StartinggRPCserver,address=%q",secureAddress)lis,err:=net.Listen("tcp",secureAddress)iferr!=nil{log.Printf("failedtolisten:%s",err)os.Exit(1)}api.RegisterDiceServiceServer(secureSrv,api.NewServer())iferr:=secureSrv.Serve(lis);err!=nil{log.Printf("failedtoserve:%s",err)os.Exit(1)}现在使用runserver/server运行服务器。继续,确保web应用程序正在运行并访问它的url-你应该看到RPC请求的结果。gRPC服务器不会记录有关传入请求的任何信息,并且仅通过查看服务器的输出很难判断发生了什么。幸运的是,有一个API可以拦截RPC请求的执行,您可以使用它添加类似于任何HTTP服务器的日志记录。它接近于Django中间件的工作方式。下面是一个简单的日志拦截器。要使用它,您需要将它传递给grpc.NewServer。funcloggingInterceptor(ctxcontext.Context,reqinterface{},info*grpc.UnaryServerInfo,handlergrpc.UnaryHandler)(interface{},error){ts:=time.Now()peer,ok:=peer.FromContext(ctx)if!ok{returnil,status.Errorf(codes.InvalidArgument,"missingpeer")}md,ok:=metadata.FromIncomingContext(ctx)if!ok{returnnil,status.Errorf(codes.InvalidArgument,"missingmetadata")}res,err:=handler(ctx,req)log.Printf("server=%qip=%qmethod=%qstatus=%sduration=%suser-agent=%q",md[":authority"][0],peer.Addr.String(),info.FullMethod,status.FromContextError(err).Code(),time.Since(ts),md["user-agent"][0],)returnres,错误}...secureSrv:=grpc.NewServer(grpc.Creds(creds),grpc.UnaryInterceptor(loggingInterceptor))看来目的达到了。客户端通过gRPC与服务器通信,并且由于mTLS,连接是安全的并且相互验证。但请记住,此服务器非常基础,需要一些工作和强化才能在公共网络上用于生产。让我们把服务器放在Nginx后面另一种方法是把Nginx放在服务器前面,我更喜欢它而不是将Go服务暴露在互联网上。开箱即用,您可以获得所有经过实战检验的功能,如负载平衡和速率限制,它还会减少您需要编写和支持的代码量。Nginx从1.13.10版本开始支持gRPC,并且可以终止、检查和路由gRPC方法调用。因此,让我们在服务器前面添加Nginx,以通过未加密的HTTP/2处理mTLS和代理请求。这个设置有点复杂,下面是图表:让我们从客户端中的另一个视图开始。它将使用不同的端口号。defnginx(请求):nginx_addr="127.0.0.1:9443"crt=api.Certs('certs/ca.pem','certs/client.pem','certs/client-key.pem')try:value=api.Client(nginx_addr,crt).roll_die()exceptExceptionase:logger.exception(e)returnHttpResponse('Value:'+ICONS[value])由于Nginx将围绕TLS完成所有工作,因此不需要gRPC服务器提供证书:insecureAddress:="127.0.0.1:50051"insecureSrv:=grpc.NewServer(grpc.UnaryInterceptor(loggingInterceptor))log.Printf("StartinggRPCserver(h2c),address=%q",insecureAddress)lis,err:=net.Listen("tcp",insecureAddress)iferr!=nil{log.Printf("failedtolisten:%s",err)os.Exit(1)}api.RegisterDiceServiceServer(insecureSrv,api.NewServer())iferr:=insecureSrv.Serve(lis);err!=nil{log.Printf("failedtoserve:%s",err)os.Exit(1)}Nginx配置文件:Nginx.conf此配置禁用妖化并启动记录日志的进程到标准输出。这对于演示目的更方便。NoneBashCSSCC#GoHTMLJavaJavaScriptJSONPHPPowershellPythonRubySQLTypeScriptYAMLCopyevents{worker_connections1024;}#Donotuseitinproduction!daemonoff;master_processoff;http{upstreamgrpcservers{server127.0.0.1:50051;}server{listen9443sslhttp2;error_log/dev/stdout;access_log/dev/stdout;#Server'stlsconfigssl_certificatecerts/server.pem;ssl_certificate_keycerts/server-key.pem;#mTLSpartssl_client_certificatecerts/ca.pem;ssl_verify_clienton;location/{grpc_passgrpc://grpcservers;}}}启用Nginx。nginx-p$(pwd)-cnginx.conf确保所有服务都已启动并访问您之前创建的视图的URL-您应该会看到RPC请求的结果。如果某些东西不起作用-请查看GitHub上的这篇文章以获取代码。【翻译稿件,合作网站转载请注明原译者和出处.com】