gRPC 拦截器鉴权实战:客户端与服务端双管齐下,元数据安全护航
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 拦截器实现鉴权。
场景描述:
- 用户管理服务包含
CreateUser
、GetUser
、UpdateUser
、DeleteUser
四个方法。 - 只有具有管理员权限的用户才能调用
CreateUser
和DeleteUser
方法。 - 所有已登录用户都可以调用
GetUser
和UpdateUser
方法。
技术选型:
- 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
结构体包含JWTManager
和accessibleRoles
两个字段。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.UnaryInterceptor
和grpc.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 服务保驾护航!