gRPC 是一个高性能的开源 RPC 框架,支持多种编程语言,并且基于 HTTP/2 协议提供了双向流和头部压缩等特性。在构建分布式系统和微服务架构时,安全认证是至关重要的一环。通过利用 gRPC 的 Metadata 和拦截器机制,可以实现灵活且强大的认证解决方案。
利用Metadata和Interceptor可以实现gRPC的auth认证,下面将教你用两种方法实现:
1.利用Metadata和Interceptor
0.目录树如下
以grpc入门的代码作为示例:
.
├── client
│ └── client.go
├── proto
│ ├── helloworld_grpc.pb.go
│ ├── helloworld.pb.go
│ └── helloworld.proto
└── server
└── server.go
1. proto文件
syntax = "proto3";
option go_package = "grpc/proto";
service Hello {
rpc Hello(HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string response = 1;
}
利用命令生成对应的go文件
cd proto/
protoc meta.proto --go_out=. --go-grpc_out=.
2.server/server.go
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"net"
"go-try/grpc_auth/proto"
)
type HelloServer struct {
proto.UnimplementedHelloServer
}
func (s *HelloServer) Hello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
return &proto.HelloResponse{
Response: "Hello " + request.Name,
}, nil
}
var interceptor = func(ctx context.Context, req any,
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
fmt.Println("接收到了一个新的请求")
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return resp, status.Error(codes.Unauthenticated, "无token认证信息")
}
appid, ok1 := md["appid"]
appkey, ok2 := md["appkey"]
if !ok1 || !ok2 || appid[0] != "10010" || appkey[0] != "auth token" {
return resp, status.Error(codes.Unauthenticated, "token认证失败")
}
fmt.Println("appid: ", appid, " appkey: ", appkey)
response, _ := handler(ctx, req)
fmt.Println("请求已经完成")
return response, nil
}
func main() {
opts := grpc.UnaryInterceptor(interceptor)
server := grpc.NewServer(opts)
proto.RegisterHelloServer(server, &HelloServer{})
listen, err := net.Listen("tcp", "0.0.0.0:8080")
if err != nil {
panic("faild to listen : " + err.Error())
}
err = server.Serve(listen)
if err != nil {
panic("faild to start grpc : " + err.Error())
}
}
3.client/client.go
package main
import (
"context"
"fmt"
"google.golang.org/grpc/metadata"
"time"
"google.golang.org/grpc"
"go-try/grpc_auth/proto"
)
var interceptor = func(ctx context.Context, method string,
req interface{}, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
// 记录请求开始的时间
start := time.Now()
// 创建元数据
md := metadata.New(map[string]string{
"appid": "10010",
"appkey": "auth token",
})
// 将元数据添加到上下文中
ctx = metadata.NewOutgoingContext(context.Background(), md)
// 调用方法
err := invoker(ctx, method, req, reply, cc, opts...)
// 打印请求耗时
fmt.Printf("请求耗时: %s\n", time.Since(start))
// 返回调用方法的错误
return err
}
func main() {
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
conn, err := grpc.NewClient("0.0.0.0:8080", opts...)
if err != nil {
panic(err)
}
defer conn.Close()
cli := proto.NewHelloClient(conn)
msg, err := cli.Hello(context.Background(), &proto.HelloRequest{Name: "zs"})
if err != nil {
panic(err)
}
fmt.Println(msg)
}
4.分别运行server.go和client.go
server端运行结果如下:
接收到了一个新的请求
appid: [10010] appkey: [auth token]
请求已经完成
如果我们将client.go文件中的appid修改:
md := metadata.New(map[string]string{
"appid": "10010",
"appkey": "auth token",
})
修改为
md := metadata.New(map[string]string{
"appid": "100101",
"appkey": "auth token",
})
再次分别运行server.go和client.go
server端运行结果会显示:
接收到了一个新的请求
client端运行结果会显示报错:
请求耗时: 3.539849ms
panic: rpc error: code = Unauthenticated desc = token认证失败
由此说明,利用Metadata和Interceptor已经实现了较为简单的auth认证。
2.利用go中grpc自带的函数
1.修改client/client.go代码
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"go-try/grpc_auth/proto"
)
type customCredential struct{}
func (cc *customCredential) GetRequestMetadata(ctx context.Context,
uri ...string) (map[string]string, error) {
return map[string]string{
"appid": "10010",
"appkey": "auth token",
}, nil
}
// RequireTransportSecurity indicates whether the credentials requires
// transport security.
func (cc *customCredential) RequireTransportSecurity() bool {
return false
}
func main() {
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
//opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
opts = append(opts, grpc.WithPerRPCCredentials(&customCredential{}))
conn, err := grpc.NewClient("0.0.0.0:8080", opts...)
if err != nil {
panic(err)
}
defer conn.Close()
cli := proto.NewHelloClient(conn)
msg, err := cli.Hello(context.Background(), &proto.HelloRequest{Name: "zs"})
if err != nil {
panic(err)
}
fmt.Println(msg)
}
2.分别运行server.go和client.go
server端运行结果如下:
接收到了一个新的请求
appid: [10010] appkey: [auth token]
请求已经完成
如果我们将client.go文件中的appid修改:
md := metadata.New(map[string]string{
"appid": "10010",
"appkey": "auth token",
})
修改为
md := metadata.New(map[string]string{
"appid": "100101",
"appkey": "auth token",
})
再次分别运行server.go和client.go
server端运行结果会显示:
接收到了一个新的请求
client端运行结果会显示报错:
请求耗时: 3.539849ms
panic: rpc error: code = Unauthenticated desc = token认证失败
由此说明,利用go中grpc自带的WithPerRPCCredentials()
也可以实现较为简单的auth认证。
总结
客户端实现流程
客户端通过创建一个自定义的拦截器,在每个 RPC 调用之前将认证令牌(例如 OAuth 令牌)添加到请求的 Metadata 中。这样可以确保每个请求都包含了有效的认证信息,并且可以在需要时自动处理认证逻辑,提高了安全性和代码的复用性。
服务器实现流程
服务器则通过设置一个拦截器来截取每个传入的 RPC 请求,并验证其中的认证信息。通过解析传入请求的 Metadata,服务器可以检查认证令牌的有效性或者其他自定义的认证机制。如果认证失败,服务器可以返回相应的错误代码,如 codes.Unauthenticated
,以保护服务不受未经授权的访问。
通过这种方式,gRPC 提供了一种强大而灵活的认证机制,可以根据具体需求定制和扩展,确保在分布式系统中的通信安全和可靠性。