ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

grpc-自定义token认证

2022-05-15 18:31:09  阅读:232  来源: 互联网

标签:自定义 err grpc ctx Token token string


grpc-自定义token认证

一、前言

前面篇章的gRPC都是明文传输的,容易被篡改数据。本章将介绍如何为gRPC添加安全机制,Token认证。

官方grpc-eg

源码

二、新建proto文件

主要是定义我们服务的方法以及数据格式,创建simple.proto文件。

1.定义发送消息的信息

message SimpleRequest{
    // 定义发送的参数,采用驼峰命名方式,小写加下划线,如:student_name
    string data = 1;//发送数据
}

2.定义响应信息

message SimpleResponse{
    // 定义接收的参数
    // 参数类型 参数名 标识号(不可重复)
    int32 code = 1;  //状态码
    string value = 2;//接收值
}

3.定义服务方法Route

// 定义我们的服务(可定义多个服务,每个服务可定义多个接口)
service Simple{
    rpc Route (SimpleRequest) returns (SimpleResponse){};
}

4.编译proto文件

syntax = "proto3";// 协议为proto3

//option go_package = "path;name";
//path 表示生成的go文件的存放地址,会自动生成目录的。
//name 表示生成的go文件所属的包名

//  生成pb.go命令:  protoc -I ./ --go_out=plugins=grpc:.\08tokensecurity\proto\  .\08tokensecurity\proto\simple.proto

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

// 定义我们的服务(可定义多个服务,每个服务可定义多个接口)
service Simple{
  rpc Route (SimpleRequest) returns (SimpleResponse){};
}

// 定义发送请求信息
message SimpleRequest{
  // 定义发送的参数,采用驼峰命名方式,小写加下划线,如:student_name
  // 参数类型 参数名 标识号(不可重复)
  string data = 1;
}

// 定义响应信息
message SimpleResponse{
  // 定义接收的参数
  // 参数类型 参数名 标识号(不可重复)
  int32 code = 1;
  string value = 2;
}


编译

// 指令编译方法,进入go-grpc-example项目,运行
go-grpc-example> protoc -I ./ --go_out=plugins=grpc:.\07tlssecurity\proto\  .\07tlssecurity\proto\simple.proto

三、Token认证

客户端发请求时,添加Token到上下文context.Context中,服务器接收到请求,先从上下文中获取Token验证,验证通过才进行下一步处理。

客户端请求添加Token到上下文中

type PerRPCCredentials interface {
    GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
    RequireTransportSecurity() bool
}

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

  • GetRequestMetadata:获取当前请求认证所需的元数据
  • RequireTransportSecurity:是否需要基于 TLS 认证进行安全传输
  • 每次调用服务端方法,都会被再次调用

接下来我们实现这两个方法

// Token token认证
type Token struct {
	AppID     string
	AppSecret string
}

// GetRequestMetadata 获取当前请求认证所需的元数据(metadata)
func (t *Token) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	// 设置一个种子
	rand.Seed(time.Now().UnixNano())
	// Intn返回一个取值范围在[0,n)的伪随机int值
	num := rand.Intn(100) + 1 // 随机1-100
	rangeSeed := strconv.Itoa(num)
	log.Println("GetRequestMetadata 每次访问服务端方法都会被调用 添加自定义认证", rangeSeed)

	return map[string]string{"app_id": t.AppID, "app_secret": t.AppSecret, "range_seed": rangeSeed}, nil
}

// RequireTransportSecurity 是否需要基于 TLS 认证进行安全传输,返回false不进行TLS验证
func (t *Token) RequireTransportSecurity() bool {
	return true
}

然后再客户端中调用Dial时添加自定义验证方法进去

//构建Token
token := auth.Token{
    AppID:     "grpc_token",
    AppSecret: "123456",
}
// 连接服务器
conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(&token))

客户端完整代码

package main

import (
	"context"
	"go-grpc-example/08tokensecurity/pkg/auth"
	pb "go-grpc-example/08tokensecurity/proto"
	"log"

	"google.golang.org/grpc/credentials/insecure"

	"google.golang.org/grpc"
)

/*
@author RandySun
@create 2022-05-08-17:29
*/
// Address 连接地址
const Address string = ":8001"

var grpcClient pb.SimpleClient

func main() {
	//从输入的证书文件中为客户端构造TLS凭证

	//构建Token
	token := auth.Token{
		AppID:     "grpc_token",
		AppSecret: "123456",
	}
	// 连接服务器
	conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithPerRPCCredentials(&token))
	if err != nil {
		log.Fatalf("net.Connect err: %v", err)
	}
	defer conn.Close()

	// 建立gRPC连接
	grpcClient = pb.NewSimpleClient(conn)
	// 每次请求方法都会调用 grpc.WithPerRPCCredentials(&token) 获取token
	route()
	route()
}

// route 调用服务端Route方法
func route() {
	// 创建发送结构体
	req := pb.SimpleRequest{
		Data: "grpc",
	}
	// 调用我们的服务(Route方法)
	// 同时传入了一个 context.Context ,在有需要时可以让我们改变RPC的行为,比如超时/取消一个正在运行的RPC
	res, err := grpcClient.Route(context.Background(), &req)
	if err != nil {
		log.Fatalf("Call Route err: %v", err)
	}
	// 打印返回值
	log.Println(res)
}

服务端验证Token

首先需要从上下文中获取元数据,然后从元数据中解析Token进行验证

// Check 验证token
func Check(ctx context.Context) error {
	//从上下文中获取元数据
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return status.Errorf(codes.Unauthenticated, "获取Token失败")
	}
	var (
		appID     string
		appSecret string
	)
	if value, ok := md["app_id"]; ok {
		appID = value[0]
	}
	if value, ok := md["app_secret"]; ok {
		appSecret = value[0]
	}
	if appID != "grpc_token" || appSecret != "123456" {
		return status.Errorf(codes.Unauthenticated, "Token无效: app_id=%s, app_secret=%s", appID, appSecret)
	}
	return nil
}

// Route 实现Route方法
func (s *SimpleService) Route(ctx context.Context, req *pb.SimpleRequest) (*pb.SimpleResponse, error) {
    //检测Token是否有效
	if err := Check(ctx); err != nil {
		return nil, err
	}
	res := pb.SimpleResponse{
		Code:  200,
		Value: "hello " + req.Data,
	}
	return &res, nil
}
  • metadata.FromIncomingContext:从上下文中获取元数据

服务端代码中,每个服务的方法都需要添加Check(ctx)来验证Token,这样十分麻烦。gRPC拦截器,能很好地解决这个问题。gRPC拦截器功能类似中间件,拦截器收到请求后,先进行一些操作,然后才进入服务的代码处理。

服务端添加拦截器

//普通方法:一元拦截器(grpc.UnaryInterceptor)
var interceptor grpc.UnaryServerInterceptor
interceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    //拦截普通方法请求,验证Token
    err = Check(ctx)
    if err != nil {
        return
    }
    // 继续处理请求
    return handler(ctx, req)
}

服务端完整代码

package main

import (
	"context"
	pb "go-grpc-example/08tokensecurity/proto"
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

/*
@author RandySun
@create 2022-05-08-17:29
*/
// SimpleService 定义我们的服务
type SimpleService struct {
}

// Route 实现Route方法
func (s *SimpleService) Route(ctx context.Context, req *pb.SimpleRequest) (*pb.SimpleResponse, error) {
	// 添加拦截器后,方法里省略Token认证
	// //检测Token是否有效
	// if err := Check(ctx); err != nil {
	// 	return nil, err
	// }
	res := pb.SimpleResponse{
		Code:  200,
		Value: "hello " + req.Data,
	}
	return &res, nil
}

// Check 验证token
func Check(ctx context.Context) error {
	//从上下文中获取元数据
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return status.Errorf(codes.Unauthenticated, "获取Token失败")
	}
	var (
		appID     string
		appSecret string
	)
	log.Println("Check md: ", md)

	if value, ok := md["app_id"]; ok {
		appID = value[0]
	}
	if value, ok := md["app_secret"]; ok {
		appSecret = value[0]
	}
	if appID != "grpc_token" || appSecret != "123456" {
		return status.Errorf(codes.Unauthenticated, "Token无效: app_id=%s, app_secret=%s", appID, appSecret)
	}
	return nil
}

const (
	// Address 监听地址
	Address string = ":8001"
	// NetWork 网络通信协议
	NetWork string = "tcp"
)

func main() {
	// 监听本地端口
	listener, err := net.Listen(NetWork, Address)
	if err != nil {
		log.Fatalf("net.Listen err: %V", err)
	}

	//普通方法:一元拦截器(grpc.UnaryInterceptor)
	var interceptor grpc.UnaryServerInterceptor
	interceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		//拦截普通方法请求,验证Token
		err = Check(ctx)
		if err != nil {
			return
		}
		// 继续处理请求
		return handler(ctx, req)
	}

	// 创建grpc服务实例
	grpcServer := grpc.NewServer(grpc.UnaryInterceptor(interceptor))

	// 在grpc服务器注册我们的服务
	pb.RegisterSimpleServer(grpcServer, &SimpleService{})
	log.Println(Address, "net.Listing whth TLS and token...")

	//用服务器 Serve() 方法以及我们的端口信息区实现阻塞等待,直到进程被杀死或者 Stop() 被调用
	err = grpcServer.Serve(listener)

	if err != nil {
		log.Fatalf("grpcService.Serve err:%v", err)
	}
	log.Println("grpcService.Serve run success")
}

  • grpc.UnaryServerInterceptor:为一元拦截器,只会拦截简单RPC方法。流式RPC方法需要使用流式拦截器grpc.StreamInterceptor进行拦截。

客户端发起请求,当Token不正确时候,会返回

Call Route err: rpc error: code = Unauthenticated desc = Token无效: app_id=grpc_token, app_secret=12345

image-20220508181615246

image-20220508181539324

四、总结

本篇介绍如何为gRPC添加自定义认证,从而让gRPC更安全。添加gRPC拦截器,从而省略在每个方法前添加Token检测代码,使代码更简洁。

参考:gRPC官方文档中文版

标签:自定义,err,grpc,ctx,Token,token,string
来源: https://www.cnblogs.com/randysun/p/16273947.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有