乐趣区

关于openssl:gRPC来聊一聊gRPC认证

[TOC]

gRPC 认证

咱们再来回顾一下 gRPC 的根本构造

gRPC 是一个典型的 C / S 模型,须要开发客户端 和 服务端,客户端与服务端须要达成协议,应用某一个确认的传输协定来传输数据,gRPC 通常默认是应用 protobuf 来作为传输协定,当然也是能够应用其余自定义的。

那么,客户端与服务端要通信之前,客户端如何晓得本人的数据是发给哪一个明确的服务端呢?反过来,服务端是不是也须要有一种形式来弄个分明本人的数据要返回给谁呢?

那么就不得不提 gRPC 的认证

认证形式

此处说到的认证,不是用户的身份认证 而是指多个 server 和 多个 client 之间,如何辨认对方是谁,并且能够平安的进行数据传输

  • SSL/TLS 认证形式(采纳 http2 协定)
  • 基于 Token 的认证形式(基于平安连贯)
  • 不采纳任何措施的连贯,这是不平安的连贯(默认采纳 http1)
  • 自定义的身份认证,gRPC 提供了接口用于扩大自定义认证形式

明天就和大家分享一下 SSL/TLS 认证形式 基于 Token 的认证形式,这里再来回顾一下上一篇讲到的

gRPC 音讯传输的四种类型

  • 申请 - 响应式
  • 服务端流式音讯,客户端申请一次,服务端会一一系列的数据,即数据流
  • 客户端流式音讯,客户端用数据流申请,服务端做响应
  • 双向流的形式,即单方都是流式数据

简略的例子:

service Example{rpc ReqAndRsp(Req) returns (Response)
    rpc ReqAndStream(Req) returns (Stream Response)
    rpc StreamAndRsp(Stream Request) returns (Response)
    rpc BidStream(Stream Request) returns (Stream response)
}

SSL/TLS 认证形式

那么什么是 SSL/TLS?

TLS(Transport Layer Security) 是 SSL(Secure Socket Layer) 的后续版本,它们是用于在互联网两台计算机之间用于身份验证和加密的一种协定。

基于 SSL/TLS 的通道加密是 gRPC 罕用的伎俩,那么个别咱们都是如何使用他的,他的架构个别会是啥样的?

GRPC 默认是基于 HTTP/ 2 的 TLS 对客户端和服务端替换的 所有数据 进行加密传输的

那么 HTTP 2 默认就有加密吗?

HTTP 2 协定默认是没有加密的,它只是事后定义好了 TLS 的轮廓,是 TLS 保障安全性,TLS 做的加密

HTTP 2 有啥个性?

这里简略说一下,HTTP 2 较之前的版本有如下 4 个重要的变动:

  • 二进制分帧

将所有传输的信息宰割为更小的音讯和帧,并对它们采纳二进制格局的编码

  • 多路 io 复用

在共享 TCP 链接的根底上同时发送申请和响应,http 音讯被合成为独立的帧,乱序发送,服务端依据标识符和首部将音讯从新组装起来

  • 头部压缩
  • 服务器推送 server push

服务器能够额定的向客户端推送资源,而无需客户端明确的申请

SSL/TLS 加密的根本做法是啥?

SSL/TLS 通过将称为 X.509 证书的数字文档 将网站和公司的实体信息绑定到加密密钥来进行工作。

每一个密钥对 (key pairs) 都有一个 公有密钥 (private key) 私有密钥(public key),公有密钥是独有的,个别位于服务器上,用于解密由公共密钥加密过的信息;

私有密钥是私有的,与服务器进行交互的每个人都能够持有私有密钥,用公钥加密的信息只能由公有密钥来解密。

简略来说就是

SSL/TLS 协定,客户端向服务器端索要公钥,而后用公钥加密信息,服务器收到密文后,用本人的私钥解密。

SSL/TLS 协定提供啥服务呢?

  • 认证用户和服务器,确保数据发送到正确的客户端和服务器;
  • 加密数据以避免数据中途被窃取;
  • 保护数据的完整性,确保数据在传输过程中不被扭转;

SSL/TLS 协定提供的平安通道有哪些个性呢?

  • 机密性:SSL 协定应用密钥加密通信数据。
  • 可靠性:服务器和客户端都会被认证,客户端的认证是可选的。
  • 完整性:SSL 协定会对传送的数据进行完整性检查。

说了这么多,咱们来演示一下 gRPC 的 SSL/TLS 协定如何实际吧

必要环境搭建

OpenSSL 装置

  • 官网下载地址:openssl-3.0.0-alpha17.tar.gz>https://www.openssl.org/sourc…

解压源代码

tar xzvf openssl-3.0.0-alpha17.tar.gz

进入源代码目录

cd openssl-3.0.0-alpha17

编译和装置

./Configure
make
sudo make install

装置完结后,应用 openssl version 查看 openssl 版本号

若报错如下信息:

openssl: error while loading shared libraries: libssl.so.3: cannot open shared object file: No such file or directory

通常状况下只须要建一个软链接,链接过来即可

sudo ln -s /usr/local/lib/libssl.so.3 /usr/lib/libssl.so.3
sudo ln -s /usr/local/lib/libcrypto.so.3 /usr/lib/libcrypto.so.3

TLS 证书制作

如下是一张生成 key 的简略形式,不实用 go1.15 之后的版本,go1.15 曾经弃用了 x509

# 制作私钥
openssl genrsa -out server.key 2048

openssl ecparam -genkey -name secp384r1 -out server.key

# 自签名公钥,设置无效工夫 365 天
openssl req -new -x509 -sha256 -key server.key -out server.pem -days 365

一个 DEMO

开始应用上述生成的 key

.
├── client
│ ├── keys
│ │ ├── server.key
│ │ └── server.pem
│ ├── main.go
│ ├── myclient
│ └── protoc
│ └── hi
│ ├── hi.pb.go
│ └── hi.proto
└── server

├── keys
│   ├── server.key
│   └── server.pem
├── main.go
├── myserver
└── protoc
    └── hi
        ├── hi.pb.go
        └── hi.proto

hi.proto

将 proto 编译成 pb.go 文件

protoc --go_out=plugins=grpc:. hi.proto

syntax = "proto3"; // 指定 proto 版本
package hi;     // 指定默认包名

// 指定 golang 包名
option go_package = "hi";

// 定义 Hi 服务
service Hi {
// 定义 SayHi 办法
rpc SayHi(HiRequest) returns (HiResponse) {}}

// HiRequest 申请构造
message HiRequest {string name = 1;}

// HiResponse 响应构造
message HiResponse {string message = 1;}

server/main.go

package main

import (
   "fmt"
   "log"
   "net"

   pb "myserver/protoc/hi"

   "golang.org/x/net/context"
   "google.golang.org/grpc"
   "google.golang.org/grpc/credentials" // 引入 grpc 认证包
)

const (
   // Address gRPC 服务地址
   Address = "127.0.0.1:9999"
)

// 定义 HiService 并实现约定的接口
type HiService struct{}

// HiService Hello 服务
var HiSer = HiService{}

// SayHi 实现 Hi 服务接口
func (h HiService) SayHi(ctx context.Context, in *pb.HiRequest) (*pb.HiResponse, error) {resp := new(pb.HiResponse)
   resp.Message = fmt.Sprintf("Hi %s.", in.Name)

   return resp, nil
}

func main() {log.SetFlags(log.Ltime | log.Llongfile)
   listen, err := net.Listen("tcp", Address)
   if err != nil {log.Panicf("Failed to listen: %v", err)
   }

   // TLS 认证
   creds, err := credentials.NewServerTLSFromFile("./keys/server.pem", "./keys/server.key")
   if err != nil {log.Panicf("Failed to generate credentials %v", err)
   }

   // 实例化 grpc Server, 并开启 TLS 认证
   s := grpc.NewServer(grpc.Creds(creds))

   // 注册 HelloService
   pb.RegisterHiServer(s, HiSer)

   log.Println("Listen on" + Address + "with TLS")

   s.Serve(listen)
}

client/main.go

package main

import (
   "log"
   pb "myclient/protoc/hi" // 引入 proto 包

   "golang.org/x/net/context"
   "google.golang.org/grpc"
   "google.golang.org/grpc/credentials" // 引入 grpc 认证包
   "google.golang.org/grpc/grpclog"
)

const (
   // Address gRPC 服务地址
   Address = "127.0.0.1:9999"
)

func main() {log.SetFlags(log.Ltime | log.Llongfile)
   // TLS 连贯  记得把 xxx 改成你写的服务器地址
   creds, err := credentials.NewClientTLSFromFile("./keys/server.pem", "xiaomotong")
   if err != nil {log.Panicf("Failed to create TLS credentials %v", err)
   }

   conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds))
   if err != nil {grpclog.Fatalln(err)
   }
   defer conn.Close()

   // 初始化客户端
   c := pb.NewHiClient(conn)

   // 调用办法
   req := &pb.HiRequest{Name: "gRPC"}
   res, err := c.SayHi(context.Background(), req)
   if err != nil {log.Panicln(err)
   }

   log.Println(res.Message)
}

如果你的 go 是在 1.15 版本以上的,请从新生成 key,参考文档 openssl 证书生成 记录(GO1.15 版本以上)

生成后,放到我的项目响应的地位,编译运行即可成果如下:

服务端:

客户端:

基于 Token 的认证形式

将上述 TLS 实际 DEMO 进行优化,加上 Token 机制,须要做如下 2 点 改变

  • 客户端,实现 credentials 包的接口,GetRequestMetadataRequireTransportSecurity
  • 服务端在 metadata 验证客户端的信息

代码构造与上一个 DEMO 统一,别离在客户端和服务端的代码中退出相应的逻辑即可。

如下是 credentials 包中待实现接口:

又一个 DEMO

client/main.go

增加如下逻辑即可

package main

import (
    "log"
    pb "myclient/protoc/hi" // 引入 proto 包

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials" // 引入 grpc 认证包
    "google.golang.org/grpc/grpclog"
)

const (
    // Address gRPC 服务地址
    Address = "127.0.0.1:9999"
)

// ====== 增加的逻辑  START ==============
var IsTls = true

// myCredential 自定义认证
type myCredential struct{}

// GetRequestMetadata 实现自定义认证接口
func (c myCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {return map[string]string{
        "appid":  "myappid",
        "appkey": "mykey",
    }, nil
}

// RequireTransportSecurity 自定义认证是否开启 TLS
func (c myCredential) RequireTransportSecurity() bool {return IsTls}
// ====== 增加的逻辑  END  ==============

func main() {log.SetFlags(log.Ltime | log.Llongfile)
    
    
// ====== 增加的逻辑  START ==============
    var err error
    var opts []grpc.DialOption

    if IsTls {
        // 关上 tls 走 tls 认证
        creds, err := credentials.NewClientTLSFromFile("./keys/server.pem", "小魔童")
        if err != nil {log.Panicf("Failed to create TLS mycredentials %v", err)
        }
        opts = append(opts, grpc.WithTransportCredentials(creds))
    }else{opts = append(opts, grpc.WithInsecure())
    }

    opts = append(opts, grpc.WithPerRPCCredentials(new(myCredential)))
    
    // TLS 连贯  记得把 xxx 改成你写的服务器地址, 能够默认写 127.0.0.1
    conn, err := grpc.Dial(Address, opts...)
    if err != nil {grpclog.Fatalln(err)
    }
// ====== 增加的逻辑  END  ==============
    
    defer conn.Close()

    // 初始化客户端
    c := pb.NewHiClient(conn)

    // 调用办法
    req := &pb.HiRequest{Name: "gRPC"}
    res, err := c.SayHi(context.Background(), req)
    if err != nil {log.Panicln(err)
    }

    log.Println(res.Message)
}

server/main.go

增加如下逻辑即可

package main

import (
   "fmt"
   "google.golang.org/grpc/codes"
   "google.golang.org/grpc/metadata"
   "log"
   "net"

   pb "myserver/protoc/hi"

   "golang.org/x/net/context"
   "google.golang.org/grpc"
   "google.golang.org/grpc/credentials" // 引入 grpc 认证包
)

const (
   // Address gRPC 服务地址
   Address = "127.0.0.1:9999"
)

// 定义 helloService 并实现约定的接口
type HiService struct{}

// HiService Hello 服务
var HiSer = HiService{}

// SayHello 实现 Hello 服务接口
func (h HiService) SayHi(ctx context.Context, in *pb.HiRequest) (*pb.HiResponse, error) {

// ====== 增加的逻辑  START ==============
    
   // 解析 metada 中的信息并验证
   md, ok := metadata.FromIncomingContext(ctx)
   if !ok {return nil, grpc.Errorf(codes.Unauthenticated, "no token")
   }

   var (
      appId  string
      appKey string
   )

   // md 是一个 map[string][]string 类型的
   if val, ok := md["appid"]; ok {appId = val[0]
   }

   if val, ok := md["appkey"]; ok {appKey = val[0]
   }

   if appId != "myappid" || appKey != "mykey" {return nil, grpc.Errorf(codes.Unauthenticated, "token invalide: appid=%s, appkey=%s", appId, appKey)
   }

// ====== 增加的逻辑  END ==============

   resp := new(pb.HiResponse)
   resp.Message = fmt.Sprintf("Hi %s.", in.Name)

   return resp, nil
}

func main() {log.SetFlags(log.Ltime | log.Llongfile)
   listen, err := net.Listen("tcp", Address)
   if err != nil {log.Panicf("Failed to listen: %v", err)
   }

   // TLS 认证
   creds, err := credentials.NewServerTLSFromFile("./keys/server.pem", "./keys/server.key")
   if err != nil {log.Panicf("Failed to generate credentials %v", err)
   }

   // 实例化 grpc Server, 并开启 TLS 认证
   s := grpc.NewServer(grpc.Creds(creds))

   // 注册 HelloService
   pb.RegisterHiServer(s, HiSer)

   log.Println("Listen on" + Address + "with TLS")

   s.Serve(listen)
}

好了,本次就到这里,下一次分享 gRPC 的 interceptor

技术是凋谢的,咱们的心态,更应是凋谢的。拥抱变动,背阴而生,致力向前行。

我是 小魔童哪吒,欢送点赞关注珍藏,下次见~

退出移动版