Go gRPC服务授权优化:如何避免各方法的重复代码?
Great question—this is a super common pain point when building gRPC services with auth, and there are a few solid patterns to eliminate that repetitive permission-check code. Let’s break down the most practical approaches:
1. Centralize Authorization in a Dedicated gRPC Interceptor (For Generic Permissions)
Since you’re already using a grpc.UnaryServerInterceptor to parse JWTs, you can extend this (or add a separate dedicated interceptor) to handle permission checks globally. The key here is attaching metadata to your gRPC methods (via proto method options) to define what permissions are required, then letting the interceptor validate those against the JWT claims in the context.
Step 1: Define a Custom Proto Method Option
First, create a proto extension to mark which permissions a method needs:
import "google/protobuf/descriptor.proto"; extend google.protobuf.MethodOptions { string required_permission = 50001; // Use a unique field number } service MyService { rpc GetSomething(GetSomethingRequest) returns (GetSomethingResponse) { option (required_permission) = "resource:read"; } rpc UpdateSomething(UpdateSomethingRequest) returns (UpdateSomethingResponse) { option (required_permission) = "resource:write"; } }
Step 2: Update Your Interceptor to Validate Permissions
In your Go interceptor, extract the method’s required permission from the proto metadata and check it against the JWT claims in the context:
import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/reflect/protoreflect" ) // Map your proto extension to a method option key var requiredPermissionKey = protoreflect.MethodOptionKey("required_permission") func AuthorizationInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { // Extract JWT claims from context (your existing logic) claims, ok := ctx.Value(ctxKey).(MyStruct) if !ok { return nil, status.Errorf(codes.Unauthenticated, "invalid authentication") } // Get the method's custom permission option methodDesc := info.Method methodOpts := methodDesc.Options.(protoreflect.MethodOptions) permValue := methodOpts.Get(requiredPermissionKey) if permValue.IsValid() { requiredPerm := permValue.String() // Check if the user has the required permission (adapt your existing logic here) if !claims.HasPermission(requiredPerm) { return nil, status.Errorf(codes.PermissionDenied, "insufficient permissions") } } // Pass through to the handler if authorized return handler(ctx, req) }
Register this interceptor alongside your auth interceptor (order matters—auth first, then authorization):
grpcServer := grpc.NewServer( grpc.UnaryInterceptor(grpc.ChainUnaryInterceptors( AuthInterceptor, // Parses JWT into context AuthorizationInterceptor, // Checks permissions )), )
This way, you never have to write permission checks in your service methods again—just mark the required permissions in your proto, and the interceptor handles the rest.
2. Use Method Wrappers for Request-Specific Checks
If your permission logic depends on request data (like req.ID in your GetSomething example), a global interceptor might not be enough (since it can’t easily access request fields generically). Instead, use a decorator pattern to wrap your service methods with permission checks.
Example Wrapper for Your GetSomething Method
type MyServiceServer struct { pb.UnimplementedMyServiceServer } // Inner business logic with no permission checks func (s *MyServiceServer) getSomethingInternal(ctx context.Context, req *pb.GetSomethingRequest) (*pb.GetSomethingResponse, error) { // Your core business logic here return &pb.GetSomethingResponse{}, nil } // Wrapped method that handles permission checks func (s *MyServiceServer) GetSomething(ctx context.Context, req *pb.GetSomethingRequest) (*pb.GetSomethingResponse, error) { claims, ok := ctx.Value(ctxKey).(MyStruct) if !ok { return nil, status.Errorf(codes.Unauthenticated, "invalid authentication") } // Reuse your existing hasAccessTo logic if !hasAccessTo(ctx, req.ID) { return nil, status.Errorf(codes.PermissionDenied, "no access to resource %s", req.ID) } return s.getSomethingInternal(ctx, req) }
For cleaner organization, you can even create a reusable wrapper function that takes your service method and a permission check function:
type GetSomethingHandler func(ctx context.Context, req *pb.GetSomethingRequest) (*pb.GetSomethingResponse, error) func withResourceAccessCheck(handler GetSomethingHandler) GetSomethingHandler { return func(ctx context.Context, req *pb.GetSomethingRequest) (*pb.GetSomethingResponse, error) { claims, ok := ctx.Value(ctxKey).(MyStruct) if !ok { return nil, status.Errorf(codes.Unauthenticated, "invalid authentication") } if !hasAccessTo(ctx, req.ID) { return nil, status.Errorf(codes.PermissionDenied, "no access to resource %s", req.ID) } return handler(ctx, req) } } // Then use it in your server: func (s *MyServiceServer) GetSomething(ctx context.Context, req *pb.GetSomethingRequest) (*pb.GetSomethingResponse, error) { return withResourceAccessCheck(s.getSomethingInternal)(ctx, req) }
3. Code Generation for Large-Scale Services
If you have a lot of service methods with varying permission rules, consider using a protoc plugin to auto-generate the permission-check wrappers. You can define more detailed auth rules in your proto annotations (e.g., which resources a method can access, required roles), then the plugin generates the Go code that wraps your service methods with the necessary checks.
This is especially useful for large teams or services with complex auth requirements—it eliminates manual repetition entirely and ensures consistency across your codebase.
内容的提问来源于stack exchange,提问作者Sergii Getman

