gRPC(七)进阶:自定义身份验证

前言

个人网站:https://linzyblog.netlify.app/
示例代码已经上传到github:点击跳转
gRPC官方文档:点击跳转
在前面的章节中,我们介绍了两种可全局认证的方法:

而在实际需求中,常常会对某些模块的 RPC 方法做特殊认证或校验,而gRPC也专门提供了这类特殊认证的接口。

一、概述

gRPC为每个gRPC方法调用提供了Token认证支持,可以基于用户传入的Token判断用户是否登陆、以及权限等,实现Token认证的前提是,需要定义一个结构体,并实现credentials.PerRPCCredentials接口。

1、credentials.PerRPCCredentials 接口

类型定义:

type PerRPCCredentials interface {
	// 返回需要认证的必要信息
	GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
	// 是否使用安全链接(TLS)
	RequireTransportSecurity() bool
}

在 gRPC 中默认定义了 PerRPCCredentials,是 gRPC 默认提供用于自定义认证的接口,它的作用是将所需的安全认证信息添加到每个 RPC 方法的上下文中。其包含 2 个方法:

  • GetRequestMetadata:获取当前请求认证所需的元数据(metadata),以 map 的形式返回本次调用的授权信息,ctx 是用来控制超时的
  • RequireTransportSecurity:是否需要基于 TLS 认证进行安全传输,如果返回 true 则说明该 Credentials 需要在一个有 TLS 认证的安全连接上传输,如果当前连接并没有使用 TLS 则会报错:
transport: cannot send secure credentials on an insecure connection

2、实现流程

  • 在发出请求之前,gRPC 会将 Credentials(认证凭证)存放在 metadata(元数据)中进行传递。
  • 在真正发起调用之前,gRPC 会通过 GetRequestMetadata函数,将用户定义的 Credentials(认证凭证)提取出来,并添加到 metadata(元数据)中,随着请求一起传递到服务端。
  • 然后服务端从 metadata 中取出 Credentials 进行有效性校验。

二、实现自定义身份验证

具体分为以下两步:

  • 1)客户端请求时带上 Credentials;
  • 2)服务端取出 Credentials,并验证有效性,一般配合拦截器使用(这里我们使用两种方法,拦截器以及RPC方法)。

1、目录结构

go-grpc-example
├─client
│  ├─token_client
│  │   └──client.go
├─pkg
│  ├─token
│  │   └──token.go
├─proto
│  ├─token
│  │   └──token.proto
└─server
    ├─token_server
	│  └──server.go

2、编写IDL

在 proto/token 文件夹下的 token.proto 文件中,写入如下内容:

syntax = "proto3";

option go_package = "./proto/token;token";
package tokenservice;

// 验证参数
message TokenValidateParam {
  string token = 1;
  int32 uid = 2;
}

// 请求参数
message Request {
  string name = 1;
}

// 请求返回
message Response {
  int32 uid = 1;
  string name = 2;
}

// 服务
service TokenService {
  rpc Token(Request) returns (Response);
}

在Makefile文件中写入:

token:
	protoc --go_out=. --go-grpc_out=. ./proto/token/*.proto

make token指令生成Go代码:

make token
protoc --go_out=. --go-grpc_out=. ./proto/token/*.proto

在这里插入图片描述

3、编写基础模板和空定义

我们先把基础的模板和空定义写出来在进行完善

1)server.go

const Address = "127.0.0.1:8888"

type TokenService struct {
	token.UnimplementedTokenServiceServer
}

func main() {
	listen, err := net.Listen("tcp", Address)
	if err != nil {
		fmt.Println("start error:", err)
		return
	}

	var opts []grpc.ServerOption
	
	server := grpc.NewServer(opts...)
	token.RegisterTokenServiceServer(server, &TokenService{})

	fmt.Println("服务启动成功....")
	server.Serve(listen)
}

2)client.go

const Address = "127.0.0.1:8888"

func main() {
	var opts []grpc.DialOption
	
	conn, err := grpc.Dial(Address, opts...)
	if err != nil {
		fmt.Println("grpc.Dial error:", err)
		return
	}
	defer conn.Close()
	// 实例化客户端
	client := token.NewTokenServiceClient(conn)

	// 调用具体方法
	token, err := client.Token(context.Background(), &token.Request{Name: "linzy"})
	if err != nil {
		fmt.Println("client.Token error:", err)
		return
	}
	fmt.Println("return result:", token)
}

4、实现PerRPCCredentials 接口

我们在 pkg/token 目录里的 token.go 文件内实现PerRPCCredentials 接口的方法:

const IsTLS = false

// 定义一个认证的结构体,这里是因为我在porto写好了一个数据结构
// 也可以自定义认证字段
type TokenAuth struct {
	token.TokenValidateParam
}

func (x *TokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	// 将 Credentials(认证凭证)存放在 metadata(元数据)中进行传递。
	return map[string]string{
		"uid":   strconv.FormatInt(int64(x.GetUid()), 10),
		"token": x.GetToken(),
	}, nil
}

func (x *TokenAuth) RequireTransportSecurity() bool {
	return IsTLS
}

5、实现认证功能

我们已经实现了客户端请求时带上 Credentials 凭证,后面就需要实现服务端的功能,在获取授权信息并校验有效性。

1)实现拦截器认证

pkg/Interceptor 目录下的 Interceptor.go 文件内写入以下内容:

// 用一元拦截器实现认证
func ServerInterceptorCheckToken() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
		handler grpc.UnaryHandler) (resp interface{}, err error) {
		// 验证token
		_, err = CheckToken(ctx)
		if err != nil {
			fmt.Println("Interceptor 拦截器内token认证失败\n")
			return nil, err
		}
		fmt.Println("Interceptor 拦截器内token认证成功\n")
		return handler(ctx, req)
	}
}

// 验证
func CheckToken(ctx context.Context) (*token.Response, error) {
	// 取出元数据
	md, b := metadata.FromIncomingContext(ctx)
	if !b {
		return nil, status.Error(codes.InvalidArgument, "token信息不存在")
	}

	var token, uid string
	// 取出token
	tokenInfo, ok := md["token"]
	if !ok {
		return nil, status.Error(codes.InvalidArgument, "token不存在")
	}

	token = tokenInfo[0]

	// 取出uid
	uidTmp, ok := md["uid"]
	if !ok {
		return nil, status.Error(codes.InvalidArgument, "uid不存在")
	}
	uid = uidTmp[0]

	//验证
	sum := md5.Sum([]byte(uid))
	md5Str := fmt.Sprintf("%x", sum)
	if md5Str != token {
		fmt.Println("md5Str:", md5Str)
		fmt.Println("uid:", uid)
		fmt.Println("token:", token)
		return nil, status.Error(codes.InvalidArgument, "token验证失败")
	}
	return nil, nil
}

gPRC 传输的时候把授权信息存放在 metada 的,所以需要先获取 metadata。通过metadata.FromIncomingContext可以从 ctx 中取出本次调用的 metadata,然后再从 md 中取出授权信息并校验即可。

在server.go文件内添加拦截器:

opts = append(opts, grpc.UnaryInterceptor(Interceptor.ServerInterceptorCheckToken()))

2)实现RPC方法认证

实现了校验有效性我们就需要在 server.go 服务端实现Token RPC的方法进行授权认证:

type TokenService struct {
	token.UnimplementedTokenServiceServer
	tokenAuth.TokenAuth
}

func (u TokenService) Token(ctx context.Context, r *token.Request) (*token.Response, error) {
	// 验证token
	_, err := Interceptor.CheckToken(ctx)
	if err != nil {
		fmt.Println("Token RPC方法内token认证失败\n")
		return nil, err
	}
	fmt.Printf("%v Token RPC方法内token认证成功\n", r.GetName())
	return &token.Response{Name: r.GetName()}, nil
}

同样的在client.go 文件内输入token信息,并调用grpc.WithPerRPCCredentials:

// token信息
auth := tokenAuth.TokenAuth{
	token.TokenValidateParam{
		Token: "81dc9bdb52d04dc20036dbd8313ed055",
		Uid:   1234,
	},
}
opts = append(opts, grpc.WithPerRPCCredentials(&auth))

6、启动 & 请求

输入一个正确的token:

# 启动服务端
$ go run server.go
API server listening at: 127.0.0.1:52505
服务启动成功....
Interceptor 拦截器内token认证成功

linzy Token RPC方法内token认证成功

# 启动客户端
$ go run client.go 
API server listening at: 127.0.0.1:52545
return result: name:"linzy"

修改token信息为:

// token信息
	auth := tokenAuth.TokenAuth{
		token.TokenValidateParam{
			Token: "81dc9bdb52d0ed0585",
			Uid:   1234,
		},
	}

测试一下:

# 启动服务端
$ go run server.go
API server listening at: 127.0.0.1:52505
服务启动成功....
md5Str: 81dc9bdb52d04dc20036dbd8313ed055
uid: 1234
token: 81dc9bdb52d0ed0585
Interceptor 拦截器内token认证失败

# 启动客户端
$ go run client.go 
API server listening at: 127.0.0.1:52857
client.Token error: rpc error: code = InvalidArgument desc = token验证失败

7、实现RequireTransportSecurity()方法

身份认证功能已经完成,但是我们gRPC通信还是明文传输,对于如此重要的信息肯定要建立安全连接,所以要实现 RequireTransportSecurity 方法。

方法实现很简单,我们只需要建立安全连接的时候,返回一个true就行,使用我们之前的证书进行TLS连接即可。

具体可以看我的上一篇《通过TLS建立安全连接》

server.go添加以下内容:

if tokenAuth.IsTLS {
	// TLS认证
	// 根据服务端输入的证书文件和密钥构造 TLS 凭证
	c, err := credentials.NewServerTLSFromFile("./conf/server_side_TLS/server.pem", "./conf/server_side_TLS/server.key")
	if err != nil {
		log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
	}
	opts = append(opts, grpc.Creds(c))
}

client.go添加以下内容:

if tokenAuth.IsTLS {
	//打开tls 走tls认证
	// 根据客户端输入的证书文件和密钥构造 TLS 凭证。
	// 第二个参数 serverNameOverride 为服务名称。
	c, err := credentials.NewClientTLSFromFile("./conf/server_side_TLS/server.pem", "go-grpc-example")
	if err != nil {
		log.Fatalf("credentials.NewClientTLSFromFile err: %v", err)
	}
	opts = append(opts, grpc.WithTransportCredentials(c))
} else {
	opts = append(opts, grpc.WithInsecure())
}

我们只需要修改token.go文件内的IsTLS变量就可以实现是否使用安全链接(TLS)。

启动 & 请求之后我们抓个包看一下是否已经建立安全链接了了。

在这里插入图片描述
在这里插入图片描述

三、小结

1)实现credentials.PerRPCCredentials接口就可以把数据当做 gRPC 中的 Credential 在添加到 metadata 中,跟着请求一起传递到服务端;
2)服务端从 ctx 中解析 metadata,然后从 metadata 中获取 授权信息并进行验证;
3)可以借助 Interceptor 实现全局身份验证。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lin钟一

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值