grpc-自定义token认证
grpc-自定义token认证
一、前言
前面篇章的gRPC都是明文传输的,容易被篡改数据。本章将介绍如何为gRPC添加安全机制,Token认证。
二、新建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
四、总结
本篇介绍如何为gRPC添加自定义认证,从而让gRPC更安全。添加gRPC拦截器,从而省略在每个方法前添加Token检测代码,使代码更简洁。
参考:gRPC官方文档中文版
在当下的阶段,必将由程序员来主导,甚至比以往更甚。