WEBKT

gRPC 拦截器鉴权实战:客户端与服务端双管齐下,元数据安全护航

37 0 0 0

gRPC 拦截器鉴权实战:客户端与服务端双管齐下,元数据安全护航

1. 为什么选择 gRPC 拦截器进行鉴权?

2. gRPC 拦截器的工作原理

3. 实战:基于 gRPC 拦截器的鉴权流程

3.1 定义 gRPC 服务接口

3.2 实现服务端拦截器

3.3 配置服务端拦截器

3.4 实现客户端拦截器

3.5 配置客户端拦截器

4. 总结

5. 最佳实践与注意事项

6. 扩展阅读

gRPC 拦截器鉴权实战:客户端与服务端双管齐下,元数据安全护航

在微服务架构中,gRPC 因其高性能、强类型和支持多种编程语言而备受青睐。然而,随着服务数量的增加,API 安全问题也日益凸显。如何有效地对 gRPC 服务进行鉴权和授权,成为保障系统安全的关键一环。本文将深入探讨如何利用 gRPC 拦截器(Interceptor)实现请求的认证与授权,包括客户端拦截器和服务端拦截器,以及如何在拦截器中访问和验证请求的元数据(Metadata),并结合实际案例,助你打造坚不可摧的 gRPC API 防线。

1. 为什么选择 gRPC 拦截器进行鉴权?

传统的鉴权方式,例如在每个 gRPC 方法中编写重复的鉴权逻辑,不仅繁琐,而且容易出错。gRPC 拦截器提供了一种优雅且高效的解决方案,具有以下优势:

  • 集中式鉴权:将鉴权逻辑从业务代码中分离出来,统一管理,降低代码冗余。
  • 可复用性:拦截器可以应用于多个 gRPC 服务和方法,提高代码复用率。
  • 灵活性:可以根据不同的业务需求,定制不同的拦截器。
  • 易维护性:当鉴权策略发生变化时,只需修改拦截器代码,无需修改业务代码。

2. gRPC 拦截器的工作原理

gRPC 拦截器本质上是一种 AOP(面向切面编程)思想的体现,它允许我们在 gRPC 方法执行前后插入自定义的逻辑,而无需修改原始方法的代码。gRPC 拦截器分为客户端拦截器和服务端拦截器两种:

  • 客户端拦截器(Client Interceptor):在客户端发起 gRPC 请求前执行,可以用于添加认证信息、监控请求耗时等。
  • 服务端拦截器(Server Interceptor):在服务端接收到 gRPC 请求后执行,可以用于验证用户身份、检查权限等。

3. 实战:基于 gRPC 拦截器的鉴权流程

接下来,我们以一个简单的用户管理服务为例,演示如何使用 gRPC 拦截器实现鉴权。

场景描述

  • 用户管理服务包含 CreateUserGetUserUpdateUserDeleteUser 四个方法。
  • 只有具有管理员权限的用户才能调用 CreateUserDeleteUser 方法。
  • 所有已登录用户都可以调用 GetUserUpdateUser 方法。

技术选型

  • gRPC:作为服务间通信框架。
  • Protocol Buffers:用于定义 gRPC 服务接口。
  • JWT(JSON Web Token):用于生成和验证用户身份。
  • Go:作为示例代码的编程语言(其他语言实现类似)。

3.1 定义 gRPC 服务接口

首先,使用 Protocol Buffers 定义用户管理服务的接口:

syntax = "proto3";

package user;

option go_package = "./user";

message User {
  string id = 1;
  string name = 2;
  string email = 3;
  string role = 4; // "admin" or "user"
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}

message CreateUserResponse {
  User user = 1;
}

message GetUserRequest {
  string id = 1;
}

message GetUserResponse {
  User user = 1;
}

message UpdateUserRequest {
  User user = 1;
}

message UpdateUserResponse {
  User user = 1;
}

message DeleteUserRequest {
  string id = 1;
}

message DeleteUserResponse {
  bool success = 1;
}

service UserService {
  rpc CreateUser (CreateUserRequest) returns (CreateUserResponse) {};
  rpc GetUser (GetUserRequest) returns (GetUserResponse) {};
  rpc UpdateUser (UpdateUserRequest) returns (UpdateUserResponse) {};
  rpc DeleteUser (DeleteUserRequest) returns (DeleteUserResponse) {};
}

3.2 实现服务端拦截器

接下来,我们创建一个服务端拦截器,用于验证用户的身份和权限:

package interceptor
import (
"context"
"fmt"
"log"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// AuthInterceptor 鉴权拦截器
type AuthInterceptor struct {
jwtManager *JWTManager
accessibleRoles map[string][]string
}
// NewAuthInterceptor 创建鉴权拦截器
func NewAuthInterceptor(jwtManager *JWTManager, accessibleRoles map[string][]string) *AuthInterceptor {
return &AuthInterceptor{
jwtManager: jwtManager,
accessibleRoles: accessibleRoles,
}
}
// JWTManager JWT 管理器
type JWTManager struct {
secretKey string
tokenDuration int
}
// NewJWTManager 创建 JWT 管理器
func NewJWTManager(secretKey string, tokenDuration int) *JWTManager {
return &JWTManager{
secretKey: secretKey,
tokenDuration: tokenDuration,
}
}
// Unary returns a server interceptor function to authenticate and authorize unary RPC
func (interceptor *AuthInterceptor) Unary() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
log.Println("--> unary interceptor: ", info.FullMethod)
if err := interceptor.authorize(ctx, info.FullMethod);
err != nil {
return nil, err
}
return handler(ctx, req)
}
}
// Stream returns a server interceptor function to authenticate and authorize stream RPC
func (interceptor *AuthInterceptor) Stream() grpc.StreamServerInterceptor {
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
log.Println("--> stream interceptor: ", info.FullMethod)
if err := interceptor.authorize(stream.Context(), info.FullMethod);
err != nil {
return err
}
return handler(srv, stream)
}
}
func (interceptor *AuthInterceptor) authorize(ctx context.Context, method string) error {
accessibleRoles, ok := interceptor.accessibleRoles[method]
if !ok {
// everyone can access
return nil
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return status.Errorf(codes.Unauthenticated, "metadata is not provided")
}
values := md["authorization"]
if len(values) == 0 {
return status.Errorf(codes.Unauthenticated, "authorization token is not provided")
}
authorizationHeader := values[0]
fields := strings.Split(authorizationHeader, " ")
if len(fields) < 2 {
return status.Errorf(codes.Unauthenticated, "invalid authorization header format")
}
authorizationType := strings.ToLower(fields[0])
if authorizationType != "bearer" {
return status.Errorf(codes.Unauthenticated, "unsupported authorization type: %s", authorizationType)
}
token := fields[1]
claims, err := interceptor.jwtManager.Verify(token)
if err != nil {
return status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
}
for _, role := range accessibleRoles {
if role == claims.Role {
return nil
}
}
return status.Errorf(codes.PermissionDenied, "no permission to access this RPC")
}
// Verify 验证 JWT Token
func (manager *JWTManager) Verify(accessToken string) (*UserClaims, error) {
claims, err := ParseToken(accessToken, manager.secretKey)
if err != nil {
return nil, fmt.Errorf("could not parse token: %v", err)
}
return claims, nil
}
// UserClaims 用户声明
type UserClaims struct {
Username string
Role string
jwt.RegisteredClaims
}
// ParseToken 解析 Token
func ParseToken(accessToken string, secretKey string) (*UserClaims, error) {
token, err := jwt.ParseWithClaims(accessToken, &UserClaims{}, func(token *jwt.Token) (i interface{}, err error) {
return []byte(secretKey), nil
})
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
}
claims, ok := token.Claims.(*UserClaims)
if !ok {
return nil, fmt.Errorf("invalid token claims")
}
return claims, nil
}

代码解释

  • AuthInterceptor 结构体包含 JWTManageraccessibleRoles 两个字段。
  • JWTManager 用于验证 JWT Token 的有效性。
  • accessibleRoles 定义了每个 gRPC 方法允许访问的角色。
  • Unary 方法返回一个 grpc.UnaryServerInterceptor 函数,用于处理 unary RPC。
  • Stream 方法返回一个 grpc.StreamServerInterceptor 函数,用于处理 stream RPC。
  • authorize 方法从 Context 中提取 Metadata,验证 JWT Token,并检查用户是否具有访问该方法的权限。

3.3 配置服务端拦截器

在 gRPC 服务端启动时,配置该拦截器:

package main
import (
"context"
"fmt"
"log"
"net"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
"example.com/user/interceptor"
pb "example.com/user/user"
)
const (
port = ":50051"
)
// server is used to implement user.UserService.
type server struct {
pb.UnimplementedUserServiceServer
}
// CreateUser implements user.UserService/CreateUser
func (s *server) CreateUser(ctx context.Context, in *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
log.Printf("Received: %v", in.GetName())
user := &pb.User{
Id: "123",
Name: in.GetName(),
Email: in.GetEmail(),
Role: "user",
}
return &pb.CreateUserResponse{
User: user,
}, nil
}
// GetUser implements user.UserService/GetUser
func (s *server) GetUser(ctx context.Context, in *pb.GetUserRequest) (*pb.GetUserResponse, error) {
log.Printf("Received: %v", in.GetId())
user := &pb.User{
Id: in.GetId(),
Name: "test",
Email: "test@example.com",
Role: "user",
}
return &pb.GetUserResponse{
User: user,
}, nil
}
// UpdateUser implements user.UserService/UpdateUser
func (s *server) UpdateUser(ctx context.Context, in *pb.UpdateUserRequest) (*pb.UpdateUserResponse, error) {
log.Printf("Received: %v", in.GetUser().GetName())
user := in.GetUser()
return &pb.UpdateUserResponse{
User: user,
}, nil
}
// DeleteUser implements user.UserService/DeleteUser
func (s *server) DeleteUser(ctx context.Context, in *pb.DeleteUserRequest) (*pb.DeleteUserResponse, error) {
log.Printf("Received: %v", in.GetId())
return &pb.DeleteUserResponse{
Success: true,
}, nil
}
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
secretKey := os.Getenv("SECRET_KEY")
if secretKey == "" {
secretKey = "secret"
}
jwtManager := interceptor.NewJWTManager(secretKey, 15*60)
authInterceptor := interceptor.NewAuthInterceptor(jwtManager, accessibleRoles())
s := grpc.NewServer(
grpc.UnaryInterceptor(authInterceptor.Unary()),
grpc.StreamInterceptor(authInterceptor.Stream()),
)
pb.RegisterUserServiceServer(s, &server{})
reflection.Register(s)
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis);
err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
func accessibleRoles() map[string][]string {
const userServicePath = "/user.UserService/"
return map[string][]string{
userServicePath + "CreateUser": {"admin"},
userServicePath + "UpdateUser": {"admin", "user"},
userServicePath + "GetUser": {"admin", "user"},
userServicePath + "DeleteUser": {"admin"},
}
}
// GenerateToken 生成 JWT Token
func GenerateToken(username string, role string, secretKey string, tokenDuration int) (string, error) {
claims := &interceptor.UserClaims{
Username: username,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(tokenDuration) * time.Second)),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, err := token.SignedString([]byte(secretKey))
if err != nil {
return "", fmt.Errorf("failed to sign token: %w", err)
}
return signedToken, nil
}

代码解释

  • accessibleRoles 函数定义了每个 gRPC 方法允许访问的角色。
  • grpc.NewServer 函数的 grpc.UnaryInterceptorgrpc.StreamInterceptor 选项用于注册服务端拦截器。

3.4 实现客户端拦截器

客户端拦截器的主要职责是在发起请求时,将 JWT Token 添加到 Metadata 中:

package interceptor
import (
"context"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// AuthInterceptor 客户端鉴权拦截器
type AuthInterceptor struct {
jwtToken string
}
// NewAuthInterceptor 创建客户端鉴权拦截器
func NewAuthInterceptor(jwtToken string) *AuthInterceptor {
return &AuthInterceptor{
jwtToken: jwtToken,
}
}
// Unary returns a client interceptor to inject authorization to unary call
func (interceptor *AuthInterceptor) Unary() grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
log.Println("--> client interceptor: ", method)
newCtx := context.Background()
md := metadata.Pairs("authorization", "bearer "+interceptor.jwtToken)
ctx = metadata.NewOutgoingContext(newCtx, md)
return invoker(ctx, method, req, reply, cc, opts...)
}
}
// Stream returns a client interceptor to inject authorization to stream call
func (interceptor *AuthInterceptor) Stream() grpc.StreamClientInterceptor {
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
log.Println("--> client interceptor: ", method)
newCtx := context.Background()
md := metadata.Pairs("authorization", "bearer "+interceptor.jwtToken)
ctx = metadata.NewOutgoingContext(newCtx, md)
return streamer(ctx, desc, cc, method, opts...)
}
}

代码解释

  • AuthInterceptor 结构体包含 jwtToken 字段,用于存储 JWT Token。
  • Unary 方法返回一个 grpc.UnaryClientInterceptor 函数,用于处理 unary RPC。
  • Stream 方法返回一个 grpc.StreamClientInterceptor 函数,用于处理 stream RPC。
  • 在拦截器中,我们将 JWT Token 添加到 Metadata 的 authorization 字段中,并使用 bearer 格式。

3.5 配置客户端拦截器

在 gRPC 客户端发起请求时,配置该拦截器:

package main
import (
"context"
"log"
"os"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"example.com/user/interceptor"
pb "example.com/user/user"
)
const (
address = "localhost:50051"
)
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
secretKey := os.Getenv("SECRET_KEY")
if secretKey == "" {
secretKey = "secret"
}
token, err := GenerateToken("test", "admin", secretKey, 15*60)
if err != nil {
log.Fatalf("failed to generate token: %v", err)
}
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUnaryInterceptor(interceptor.NewAuthInterceptor(token).Unary()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewUserServiceClient(conn)
r, err := c.CreateUser(context.Background(), &pb.CreateUserRequest{Name: "test", Email: "test@example.com"})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetUser().GetName())
}

代码解释

  • grpc.Dial 函数的 grpc.WithUnaryInterceptor 选项用于注册客户端拦截器。

4. 总结

通过以上步骤,我们成功地使用 gRPC 拦截器实现了鉴权功能。客户端拦截器负责将 JWT Token 添加到 Metadata 中,服务端拦截器负责验证用户的身份和权限。这种方式不仅简化了鉴权逻辑,还提高了代码的可复用性和可维护性。

5. 最佳实践与注意事项

  • 选择合适的鉴权方案:根据实际业务需求,选择合适的鉴权方案,例如 JWT、OAuth 2.0 等。
  • 保护 Secret Key:Secret Key 是 JWT Token 的签名密钥,必须妥善保管,避免泄露。
  • 细粒度权限控制:根据不同的业务需求,实现细粒度的权限控制,例如基于角色的访问控制(RBAC)。
  • 监控与日志:对鉴权过程进行监控和日志记录,以便及时发现和解决问题。
  • 性能优化:避免在拦截器中执行耗时的操作,例如频繁访问数据库。

6. 扩展阅读

希望本文能够帮助你更好地理解和应用 gRPC 拦截器,为你的 gRPC 服务保驾护航!

安全卫士 gRPC拦截器鉴权

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9754