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

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

前言


个人网站: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

8680ec1ef891420f92a646442de25b56.png


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)。


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


5abf2a84b8d54924ad19c314dd687534.png

476bbad2a9874586870eb0fbebba048e.png


三、小结


1)实现credentials.PerRPCCredentials接口就可以把数据当做 gRPC 中的 Credential 在添加到 metadata 中,跟着请求一起传递到服务端;


2)服务端从 ctx 中解析 metadata,然后从 metadata 中获取 授权信息并进行验证;


3)可以借助 Interceptor 实现全局身份验证。

目录
相关文章
|
10月前
|
存储 JSON 数据建模
数据建模怎么做?一文讲清数据建模全流程
本文深入解析了数据建模的全流程,聚焦如何将模糊的业务需求转化为可落地的数据模型,涵盖需求分析、模型设计、实施落地与迭代优化四大核心环节,帮助数据团队提升建模效率与模型实用性。
|
5月前
|
存储 人工智能 搜索推荐
不懂向量数据库?别怕!一文讲清8大主流工具,手把手教你做选择
向量数据库是AI应用的“超级记忆中枢”,能将文本、图像等转化为数学指纹并快速检索相似内容。本文通俗解析8大主流向量数据库,涵盖托管型、开源型与嵌入式三类,助你根据场景选型,轻松构建智能搜索、推荐系统与RAG应用。
5051 6
|
消息中间件 存储 开发工具
消息队列 MQ产品使用合集之C++如何使用Paho MQTT库进行连接、发布和订阅消息
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
|
存储 Go
Go中make和new的区别
在 Go 语言中,`make` 和 `new` 都用于分配内存,但功能不同。`make` 用于初始化切片、映射和通道,并返回初始化后的对象;`new` 分配内存并返回指向零值的指针,适用于任何类型。`make` 返回的是数据结构本身,而 `new` 返回指针。`make` 完整初始化特定数据结构,`new` 只初始化为零值。
560 0
|
安全 5G 网络安全
gRPC(五)进阶:通过TLS建立安全连接
发生会话密钥交换。在此过程中,客户端和服务器必须就密钥达成一致,以建立安全会话确实在客户端和服务器之间的事实——而不是在中间试图劫持会话的东西。
2839 1
gRPC(五)进阶:通过TLS建立安全连接
|
消息中间件 测试技术 领域建模
DDD - 一文读懂DDD领域驱动设计
DDD - 一文读懂DDD领域驱动设计
51567 6
|
JavaScript 前端开发 API
新一代前端框架Vue 4.0的特性及应用
【2月更文挑战第2天】随着前端技术的不断发展,Vue作为一款优秀的前端框架在市场上得到了广泛的应用和认可。本文将介绍新一代前端框架Vue 4.0的特性及其在实际项目中的应用,帮助开发者更好地了解并应用这一技术。
2078 2
|
Shell 数据安全/隐私保护
部署Alist
快速安装、更新和卸载Alist的命令行脚本:`curl -fsSL "https://alist.nn.ci/v3.sh" | bash -s {install|update|uninstall}`。默认安装路径为`/opt/alist`。管理密码使用`chmod +x ./alist; ./alist admin {random|set NEW_PASSWORD}`。附图展示界面。(235字符)
1389 0
|
SQL 消息中间件 关系型数据库
ClickHouse(10)ClickHouse合并树MergeTree家族表引擎之ReplacingMergeTree详细解析
`ReplacingMergeTree`是ClickHouse的一种表引擎,用于数据去重。与`MergeTree`不同,它在合并分区时删除重复行,但不保证无重复。去重基于`ORDER BY`列,在ver列未指定时保留最新行,否则保留ver值最大者。数据处理策略包括延迟合并导致的不确定性及按分区去重。`CREATE TABLE`语法中,`ReplacingMergeTree`需要指定可选的`ver`列。相关系列文章提供了更深入的解析。
1439 0

热门文章

最新文章