• 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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

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

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在Makefile文件中写入:

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

    make token指令生成Go代码:

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

    在这里插入图片描述

    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)
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    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
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

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

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

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

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    // token信息
    auth := tokenAuth.TokenAuth{
    	token.TokenValidateParam{
    		Token: "81dc9bdb52d04dc20036dbd8313ed055",
    		Uid:   1234,
    	},
    }
    opts = append(opts, grpc.WithPerRPCCredentials(&auth))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    修改token信息为:

    // token信息
    	auth := tokenAuth.TokenAuth{
    		token.TokenValidateParam{
    			Token: "81dc9bdb52d0ed0585",
    			Uid:   1234,
    		},
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    测试一下:

    # 启动服务端
    $ 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验证失败
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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())
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

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

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

    三、小结

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

  • 相关阅读:
    铁威马NAS之如何利用docker安装Jellyfin媒体服务器
    揭秘源代码加密方案,完美支持Jenkins代码自动化部署发布
    CentOS 7实现类似于Kali Linux中的自动补全功能
    品味Spring Cache设计之美
    git常用命令
    谷歌在以色列的路口装上了 AI 红绿灯
    “蔚来杯“2022牛客暑期多校训练营4
    纯科普:亚马逊常见认证要求汇总
    如何解决前端传递数据给后端时精度丢失问题
    【PAT甲级 - C++题解】1137 Final Grading
  • 原文地址:https://blog.csdn.net/weixin_46618592/article/details/127745921