Dans le paysage moderne des systèmes distribués, les microservices dépendent fortement de communications efficaces et sécurisées. gRPC de Google est devenu la norme de facto pour la communication interne entre services en raison de ses hautes performances, de son typage fort et de son support natif pour HTTP/2. Cependant, la construction d'une architecture de microservices résiliente nécessite plus que la simple définition de messages protobuf. Elle exige que les préoccupations transversales telles que la sécurité, l'observabilité et la gestion des erreurs soient gérées de manière cohérente sur tous les services.
C'est ici que les intercepteurs gRPC entrent en jeu. Agissant comme des middleware, les intercepteurs vous permettent d'intercepter le cycle de vie du serveur et du client gRPC, vous permettant d'injecter de la logique pour l'authentification, la journalisation, les métriques et les nouvelles tentatives sans encombrer votre logique métier principale. Dans cet article, nous explorerons comment implémenter deux intercepteurs critiques en Go : un pour l'authentification et un autre pour la journalisation détaillée des requêtes.
Comprendre les intercepteurs gRPC
Les intercepteurs dans gRPC sont des fonctions qui entourent les appels RPC. Ils s'exécutent avant que le gestionnaire réel ne s'exécute (côté serveur) ou après que le client a fait une requête (côté client). Ce modèle est analogue aux middleware dans les frameworks HTTP comme Gin ou Echo, mais il opère au niveau du transport de l'RPC.
Pour les RPC unaires côté serveur (requête-réponse standard), la signature de l'intercepteur en Go ressemble à ceci :
func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// Logique pré-gestionnaire
resp, err := handler(ctx, req)
// Logique post-gestionnaire
return resp, err
}
En enchaînant plusieurs intercepteurs, vous pouvez créer un pipeline puissant. Par exemple, vous pourriez avoir un intercepteur de journalisation qui enveloppe un intercepteur d'authentification, qui à son tour enveloppe votre logique métier réelle.
Mise en œuvre d'un intercepteur d'authentification
La sécurité est primordiale. Un modèle courant consiste à valider un JWT (JSON Web Token) transmis dans les métadonnées de la requête gRPC avant d'autoriser l'accès aux points de terminaison protégés. L'intercepteur suivant montre comment extraire le jeton de l'en-tête Authorization, valider sa signature et injecter les revendications utilisateur dans le contexte pour les gestionnaires en aval.
func AuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing token")
}
token := tokens[0]
// Supposons que validateJWT retourne les revendications ou une erreur
claims, err := validateJWT(token)
if err != nil {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
// Injecter l'ID utilisateur dans le contexte
ctx = context.WithValue(ctx, "userID", claims.UserID)
return handler(ctx, req)
}
Cette approche garantit que chaque point de terminaison protégé peut récupérer l'utilisateur authentifié simplement en appelant ctx.Value("userID"), maintenant ainsi la logique métier propre et focalisée.
Mise en œuvre d'un intercepteur de journalisation des requêtes
L'observabilité est tout aussi cruciale que la sécurité. Dans une architecture de microservices, le suivi du flux des requêtes entre différents services est essentiel pour le débogage et la surveillance des performances. Un intercepteur de journalisation peut capturer l'heure de début, le nom de la méthode, la charge utile de la requête (assainie) et le statut de la réponse.
func LoggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
log.Printf("Started %s", info.FullMethod)
resp, err := handler(ctx, req)
if err != nil {
log.Printf("Finished %s in %v with error: %v", info.FullMethod, time.Since(start), err)
return resp, err
}
log.Printf("Finished %s in %v", info.FullMethod, time.Since(start))
return resp, nil
}
Cet intercepteur simple fournit un aperçu immédiat du temps que prennent vos points de terminaison pour répondre et aide à identifier rapidement les requêtes en échec. Dans les environnements de production, vous amélioreriez probablement ceci en intégrant des bibliothèques de journalisation structurées comme Zap ou Logrus et en envoyant des métriques à Prometheus.
Rassembler le tout
Pour utiliser ces intercepteurs, vous les enregistrez lors de l'initialisation de votre serveur gRPC. Vous pouvez les passer en tant qu'options à grpc.NewServer(). L'ordre est important : le premier intercepteur de la liste est le premier à s'exécuter, ce qui signifie qu'il enveloppe le second, et ainsi de suite.
server := grpc.NewServer(
grpc.UnaryInterceptor(LoggingInterceptor),
grpc.UnaryInterceptor(AuthInterceptor),
)
Conclusion
La mise en œuvre d'intercepteurs gRPC en Go est une bonne pratique pour construire des microservices maintenables et sécurisés. En séparant les préoccupations telles que l'authentification et la journalisation dans des intercepteurs réutilisables, vous gardez la logique principale de votre application propre et assurez un comportement cohérent sur tous vos services. Que vous sécurisiez votre API avec des JWT ou surveilliez les performances avec des journaux détaillés, les intercepteurs fournissent le mécanisme flexible et puissant nécessaire pour gérer efficacement les préoccupations transversales.