来聊一聊gRPC的认证 | 周末学习

1,052 阅读8分钟

[TOC]

gRPC认证

本文已参与周末学习计划,点击链接查看详情:juejin.cn/post/696572…

我们再来回顾一下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安装

解压源代码

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

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

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