在文章的开头,感谢b站up主码神之路提供的课程,这是他的[主页]。(https://space.bilibili.com/473844125)
RPC概念
RPC是远程过程调用(Remote Procedure Call)的缩写形式。是一种通过网络从远程计算机上请求服务,而不需要了解底层网络技术的协议。RPC它假定某些协议的存在,例如TCP/UDP等,为通信程序之间携带信息数据。在OSI网络七层模型中,RPC跨越了传输层和应用层,RPC使得开发包括网络分布式多程序在内的应用程序更加容易。是一个统称。
RPC包含了客户端(Client)和服务端(Server)
业界主流的 RPC 框架整体上分为三类:
- 支持多语言的 RPC 框架,比较成熟的有 Google 的 gRPC、Apache(Facebook)的 Thrift;
- 只支持特定语言的 RPC 框架,例如新浪微博的 Motan;
- 支持服务治理等服务化特性的分布式服务框架,其底层内核仍然是 RPC 框架, 例如阿里的 Dubbo。
认识gRPC
gRPC让我们可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更创建分布式应用和服务。
gRPC简单使用
项目结构
check.proto
syntax = "proto3"; option go_package = "../service"; package service; message StudentId { int32 S_id = 1; } message MentorId { int32 M_id = 1; } service CheckService { rpc GetM_idByS_id(StudentId) returns(MentorId); }
check.go
package service import "context" var CheckService = &Check{} type Check struct { StudentId MentorId } func (c *Check) GetMIdBySId(cont context.Context,s *StudentId) (*MentorId, error) { return &MentorId{MId: s.SId + 37},nil }
grpc_service.go
package main import ( "google.golang.org/grpc" "log" "net" "protobuf/helloWorld/service" ) func main() { rpcserver := grpc.NewServer() //注册服务 service.RegisterCheckServiceServer(rpcserver,service.CheckService) // 指定端口 listen, err := net.Listen("tcp", ":8082") if err != nil { log.Fatalln("监听端口出错,err =",err) } // 启动服务 err = rpcserver.Serve(listen) if err != nil { log.Fatalln("启动服务出错,err =",err) } }
grpc_client.go
package main import ( "context" "fmt" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "log" "protobuf/helloWorld/service" ) func main() { conn, err := grpc.Dial(":8082", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalln("连接失败,err =",err) } defer conn.Close() // 创建客户端 client := service.NewCheckServiceClient(conn) mentorId, err := client.GetMIdBySId(context.Background(), &service.StudentId{SId: 1}) if err != nil { log.Fatalln("程序调用失败,err =",err) } fmt.Println("mentorId =",mentorId.MId) }
认证
TLS
TLS(Transport Layer Security,安全传输层),TLS是建立在传输TCP协议上的协议,服务于应用层,它的前身是SSL(Secure Socket Layer,安全套接字层),它实现了将应用层的报文进行加密后再交由TCP进行传输的功能。
TLS协议主要解决如下三个网络安全问题。
- 保密性(message privacy),保密通过加密encryption实现,所有信息都加密传输,第三方无法嗅探。
- 完整性,通过MAC校验机制,一旦被篡改,通信双方会立刻发现。
- 认证,双方认证,双方都可以配备证书,防止身份被冒充。
openssl使用
安装
下载地址:https://slproweb.com/products/Win32OpenSSL.html
如果是win系统的话,安装5MB的那个就行。
将安装好的文件的bin目录添加到环境变量。
生成文件
现在项目下建一个存放证书的目录certs,下面的命令都将在该目录下执行
- 生成私钥文件
openssl genrsa -des3 -passout pass:123456 -out ca.key 2048
- 创建证书请求
openssl req -new -key ca.key -out ca.csr
- 生成ca.crt
openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt
因为是要在go语言里使用,所以到这里还没有结束
先找到openssl.cnf(全局搜索找到这个文件,这里因为我的电脑不好搜了半年),将其拷贝到创建的certs目录下。
如果没有可以在网上找一个如:https://blog.csdn.net/wzfgd/article/details/109805158(感谢二流人物博主资源提供)
- 打开copy_extensions = copy
- 打开req_extensions = v3_req
- 找到[ v3_req ],添加subjectAltName = @alt_names
- 添加新的标签[ alt_names ],和标签字段
[ alt_names ] DNS.1 = *.qzrj.club
- 生成证书秘钥server.key
openssl genpkey -algorithm RSA -out server.key
- 通过私钥server.key生成证书请求文件server.csr
openssl req -new -nodes -key server.key -out server.csr -days 3650 -config ./openssl.cnf -extensions v3_req
- 生成SAN证书
openssl x509 -req -days 365 -in server.csr -out server.pem -CA ca.crt -CAkey ca.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
单向认证
单向验证是客户端验证服务端是否可信,客户端会有一个信任库,如果一个服务端的证书不在该信任库中就代表该服务端不可信。
TCP连接建立好后,对于HTTP而言,服务器就可以发数据给客户端。但是对于HTTPS,它还要运行SSL/TLS协议,SSL/TLS协议分两层,第一层是记录协议,主要用于传输数据的加密压缩;第二层是握手协议,它建立在第一层协议之上,主要用于数据传输前的双方身份认证、协商加密算法、交换密钥。
第一步:客户端发起ClientHello
客户端向指定域名的服务器发起https请求,请求内容包括:
1)客户端支持的SSL/TLS协议版本列表
2)支持的对称加密算法列表
3)客户端生成的随机数A
第二步:服务端回应SeverHello
服务器收到请求后,回应客户端,回应的内容主要有:
1)SSL/TLS版本。服务器会在客户端支持的协议和服务器自己支持的协议中,选择双方都支持的SSL/TLS的最高版本,作为双方使用的SSL/TLS版本。如果客户端的SSL/TLS版本服务器都不支持,则不允许访问
2)与1类似,选择双方都支持的最安全的加密算法。
3)从服务器密钥库中取出的证书
4)服务器端生成的随机数B
第三步:客户端回应
客户端收到后,检查证书是否合法,主要检查下面4点:
1、检查证书是否过期
2、检查证书是否已经被吊销。
有CRL和OCSP两种检查方法。CRL即证书吊销列表,证书的属性里面会有一个CRL分发点属性,如下图所示(CSDN的证书),这个属性会包含了一个url地址,证书的签发机构会将被吊销的证书列表展现在这个url地址中;OCSP是在线证书状态检查协议,客户端直接向证书签发机构发起查询请求以确认该证书是否有效。
3、证书是否可信。
客户端会有一个信任库,里面保存了该客户端信任的CA(证书签发机构)的证书,如果收到的证书签发机构不在信任库中,则客户端会提示用户证书不可信。
- 若客户端是浏览器,各个浏览器都会内置一些可信任的证书签发机构列表,在浏览器的设置中可以看到。
如果不在信任表中,则浏览器会出现类似下面的警告页面,提示你不安全。(当然,你可以选择继续访问)
- 若客户端是程序,例如Java中,需要程序配置信任库文件,以判断证书是否可信,如果没设置,则默认使用jdk自带的证书库(jre\lib\security\cacerts,默认密码changeit)。如果证书或签发机构的证书不在信任库中,则认为不安全,程序会报错。(你可以在程序中设置信任所有证书,不过这样并不安全)。
4、检查收到的证书中的域名与请求的域名是否一致。
若客户端是程序,这一项可配置不检查。若为浏览器,则会出现警告,用户也可以跳过。
证书验证通过后,客户端使用特定的方法又生成一个随机数c,这个随机数有专门的名称“pre-master key”。接着,客户端会用证书的公钥对“pre-master key”加密,然后发给服务器。
第四步,服务器的最后回应
服务器使用密钥库中的私钥解密后,得到这个随机数c。此时,服务端和客户端都拿到了随机数a、b、c,双方通过这3个随机数使用相同的DH密钥交换算法计算得到了相同的对称加密的密钥。这个密钥就作为后续数据传输时对称加密使用的密钥。
服务器回应客户端,握手结束,可以采用对称加密传输数据了。
这里注意几点:
1、整个验证过程,折腾了半天,其实是为了安全地得到一个双方约定的对称加密密钥,当然,过程中也涉及一些身份认证过程。既然刚开始时,客户端已经拿到了证书,里面包含了非对称加密的公钥,为什么不直接使用非对称加密方案呢,这是因为非对称加密计算量大、比较耗时,而对称加密耗时少。
2、对称加密的密钥只在这次连接中断前有效,从而保证数据传输安全。
3、为什么要用到3个随机数,1个不行吗?这是因为客户端和服务端都不能保证自己的随机数是真正随机生成的,这样会导致数据传输使用的密钥就不是随机的,时间长了,就很容易被破解。如果使用客户端随机数、服务端随机数、pre-master key随机数这3个组合,就能十分接近随机。
4、什么是信任库和密钥库。信任库前面已经说了,它是用来存放客户端信任的CA的证书。在程序交互中,需要确保你访问的服务器的证书在你的信任库里面。密钥库是用来存放服务器的私钥和证书。
5、中间人攻击问题。前面过程说明中,有一点,客户端是验证有问题的时候,是可以选择继续的。对浏览器而言,用户可以选择继续访问;对程序而言,有些系统为了处理简单,会选择信任所有证书,这样就给中间人攻击提供了漏洞。
中间人攻击时,它想办法拦截到客户端与服务器之间的通信。在客户端向服务器发信息时,中间人首先伪装成客户端,向真正的服务器发消息,获得真正的证书,接着伪装成服务器将自己的伪证书发给客户端。服务器向客户端发消息时,中间人伪装成客户端,接收消息,然后再伪装成服务器向客户端发消息。最后验证过程完成后,客户端的真实对称密钥被中间人拿到,而真正的服务器拿到的是中间人提供的伪密钥。后续数据传输过程中的数据就会被中间人窃取。
双向认证
单向验证过程中,客户端会验证自己访问的服务器,服务器对来访的客户端身份不做任何限制。如果服务器需要限制客户端的身份,则可以选择开启服务端验证,这就是双向验证。从这个过程中我们不难发现,使用单向验证还是双向验证,是服务器决定的。
一般而言,我们的服务器都是对所有客户端开放的,所以服务器默认都是使用单向验证。如果你使用的是Tomcat服务器,在配置文件server.xml中,配置Connector节点的clientAuth属性即可。若为true,则使用双向验证,若为false,则使用单向验证。如果你的服务,只允许特定的客户端访问,那就需要使用双向验证了。
一般而言,我们的服务器都是对所有客户端开放的,所以服务器默认都是使用单向验证。如果你使用的是Tomcat服务器,在配置文件server.xml中,配置Connector节点的clientAuth属性即可。若为true,则使用双向验证,若为false,则使用单向验证。如果你的服务,只允许特定的客户端访问,那就需要使用双向验证了。
双向验证基本过程与单向验证相同,不同在于:
1)第二步服务器第一次回应客户端的SeverHello消息中,会要求客户端提供“客户端的证书”
2)第三步客户端验证完服务器证书后的回应内容中,会增加两个信息:
1、客户端的证书
2、客户端证书验证消息(CertificateVerify message):客户端将之前所有收到的和发送的消息组合起来,并用hash算法得到一个hash值,然后用客户端密钥库的私钥对这个hash进行签名,这个签名就是CertificateVerify message
说明:这里关于客户端私钥的使用,网上有很多文章认为:在协商对称加密方案时,服务端先用客户端公钥加密服务器选定的对称加密方案,客户端收到后使用私钥解密得到。首先,对称加密方案就那么几种,逐个试试就能试出来,没必要为了这个增加一个客户端和服务端的交互过程。而这里关于CertificateVerify message的说法参考了维基百科关于“Transport Layer Security”一文中"Client-authenticated TLS handshake"的描述。链接:https://en.wikipedia.org/wiki/Transport_Layer_Security#Client-authenticated_TLS_handshake(中国局域网打不开,需要配VPN)
3)服务器收到客户端证书后:
a)确认这个证书是否在自己的信任库中(当然也会校验是否过期等信息),如果验证不通过则会拒绝连接;
b)用客户端证书中的公钥去验证收到的证书验证消息中的签名。这一步的作用是为了确认证书确实是客户端的。
所以,在双向验证中,客户端需要用到密钥库,保存自己的私钥和证书,并且证书需要提前发给服务器,由服务器放到它的信任库中。
单双向认证总结
1、单向验证中,如果是你客户端,你需要拿到服务器的证书,并放到你的信任库中;如果是服务端,你要生成私钥和证书,并将这两个放到你的密钥库中,并且将证书发给所有客户端。
2、双向验证中,如果你是客户端,你要生成客户端的私钥和证书,将它们放到密钥库中,并将证书发给服务端,同时,在信任库中导入服务端的证书。如果你是服务端,除了在密钥库中保存服务器的私钥和证书,还要在信任库中导入客户端的证书。
3、再次强调,使用单向验证还是双向验证,是服务器决定的。
4、https的验证过程,不管是单向还是双向,只有四步,网上很多关于https验证过程的文章中,写了来来回回七八上十步。要真是这样,访问一个https地址,时间全花在了交互上了。
token认证
grpc_service.go
// 实现Token认证,需要合法的用户名和密码 // 实现一个拦截器 var authInterceptor grpc.UnaryServerInterceptor authInterceptor = func( ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (resp interface{}, err error) { err = Auth(ctx) if err != nil { return } // 继续处理 return handler(ctx,req) } rpcserver := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(authInterceptor)) // .... func Auth(ctx context.Context) error { // 拿到传输的用户名和密码 md, b := metadata.FromIncomingContext(ctx) if !b { log.Fatal("解析Context出错") } // 获取username和password var username string var password string if val, ok := md["username"]; ok { username = val[0] } if val, ok := md["password"]; ok { password = val[0] } if username != "admin" || password != "admin" { return status.Errorf(codes.Unauthenticated,"token不合法") } return nil }
auth.go
package auth import "context" type Authentication struct { Username string Password string } func (a Authentication) GetRequestMetadata(context.Context, ...string) ( map[string]string, error, ) { return map[string]string{"username":a.Username, "password":a.Password},nil } func (a Authentication) RequireTransportSecurity() bool { return true }
grpc_client.go
// 携带token信息 token := auth2.Authentication{ Username: "admin", Password: "admin", } conn, err := grpc.Dial(":8082", grpc.WithTransportCredentials(file), grpc.WithPerRPCCredentials(token)) if err != nil { log.Fatalln("连接失败,err =",err) }
improt
向一个 .proto 文件中引入另一个 .proto 文件。
// 从执行 protoc 命令的目录算起 import "pbfile/user.proto";
如果在导入的文件中没有使用到引入文件中的内容,那么就会产生如下错误
Import pbfile/xxx.proto is unused.
程序是不允许导入了还不用,就一直吊着人家别的包的这种渣男行为。
Any
任意类型
// 使用any类型,需要导入这个 import "google/protobuf/any.proto"; message Test { string msg = 1; } message MentorId { int32 M_id = 1; google.protobuf.Any data = 2; }
grpc_service.gp
var test Test test.Msg = "1" any, _ := anypb.New(&test) return &MentorId{MId: s.SId + 37,Data: any},nil
grpc_client.go
fmt.Println("mentorId =",mentorId.MId,mentorId.Data)
stream
客户端流
客户端不断发送请求,但服务端只有一个响应
定义
// 客户端流 rpc UpdateM_idByS_idClientStream(stream StudentId) returns(MentorId);
check.go
func (c *Check) UpdateMIdBySIdClientStream(stream CheckService_UpdateMIdBySIdClientStreamServer) error { // 计数,记录已经接受到几次请求 count := 0 // 接受客户端发送的信息 for { recv, err := stream.Recv() if err != nil { return nil } fmt.Println("接收到客户端发送信息,msg =",recv.SId) count++ if count > 10 { mid := &MentorId{MId: 123} err = stream.SendAndClose(mid) if err != nil { return nil } } } }
grpc_client.go
func main() { //.... // 客户端流 stream, err := client.UpdateMIdBySIdClientStream(context.Background()) if err != nil { log.Fatal("远程调用失败",err) } var rsp = make(chan struct{},1) go proRequest(stream,rsp) select { case <- rsp: recv, err := stream.CloseAndRecv() if err != nil { log.Fatal(err) } fmt.Println("接收到服务端的响应,",recv.MId) } //.... } func proRequest(stream service.CheckService_UpdateMIdBySIdClientStreamClient, rsp chan struct{}) { req := service.StudentId{ SId: 321, } count := 0 for true { err := stream.Send(&req) if err != nil { log.Fatal(err) } time.Sleep(time.Second) count++ if count > 10 { rsp <- struct{}{} break } } }
服务端流
客户端只发送一次请求,服务端不停的响应
定义
// 服务端流 rpc AddM_idByS_idServiceStream(StudentId) returns(stream MentorId);
check.go
//AddMIdBySIdServiceStream 服务端流 func (c *Check) AddMIdBySIdServiceStream(sid *StudentId,stream CheckService_AddMIdBySIdServiceStreamServer) error { count := 0 for true { // 这个sid就是从客户端传过来的值,这里只是简单的处理返回 err := stream.Send(&MentorId{MId: sid.SId+123}) if err != nil { log.Fatal("服务端发送数据失败,err =",err) } time.Sleep(time.Second) count++ if count > 10 { return nil } } return nil }
grpc_client.go
// 服务端流 req := service.StudentId{ SId: 123, } stream, err := client.AddMIdBySIdServiceStream(context.Background(), &req) if err != nil { log.Fatal("获取服务端流失败,err =",err) } for true { recv, err := stream.Recv() if err != nil { if err == io.EOF { fmt.Println("客户端数据接受完成") err := stream.CloseSend() if err != nil { log.Fatal(err) } return } log.Fatal(err) } fmt.Println("接受服务端响应信息",recv.MId) } }
双向流
客户端和服务端可以不停的请求和响应
定义
// 双向流 rpc SayHiStream(stream StudentId) returns(stream MentorId);
check.go
// SayHiStream 双向流 func (c *Check) SayHiStream(stream CheckService_SayHiStreamServer) error { for true { recv, err := stream.Recv() if err != nil { return nil } fmt.Println("服务端接收到的消息,",recv.SId) time.Sleep(time.Second) err = stream.Send(&MentorId{MId: recv.SId + 1}) if err != nil { log.Fatal("服务端发送消息失败,err =",err) } } return nil }
grpc_client
// 双向流 req := service.StudentId{ SId: 1, } stream, err := client.SayHiStream(context.Background()) if err != nil { log.Fatal("获取服务端流失败,err =",err) } for true { err = stream.Send(&req) if err != nil { log.Fatal("客户端发送信息失败,err =",err) } time.Sleep(time.Second) recv, err := stream.Recv() if err != nil { if err == io.EOF { err := stream.CloseSend() if err != nil { log.Fatal("客户端关闭流失败,err =",err) } return } log.Fatal("客户端接收信息失败,err =",err) } fmt.Println("客户端接收到的信息为,",recv.MId) req.SId = recv.MId } }