Configure automatiquement et exécute le serveur gRPC intégré avec les beans compatibles @GRpcService dans le cadre de l'application Spring-boot (courte vidéo)
Il est conseillé aux utilisateurs Gradle
d'appliquer le plugin :
plugins {
id " io.github.lognet.grpc-spring-boot " version ' 5.1.5 '
}
Le plugin io.github.lognet.grpc-spring-boot gradle simplifie considérablement la configuration du projet.
repositories {
mavenCentral()
// maven { url "https://oss.sonatype.org/content/repositories/snapshots" } // for snapshot builds
}
dependencies {
implementation ' io.github.lognet:grpc-spring-boot-starter:5.1.5 '
}
Par défaut, starter extrait io.grpc:grpc-netty-shaded
comme dépendance transitive, si vous êtes obligé d'utiliser une dépendance grpc-netty
pure :
implementation ( ' io.github.lognet:grpc-spring-boot-starter:5.1.5 ' ) {
exclude group : ' io.grpc ' , module : ' grpc-netty-shaded '
}
implementation ' io.grpc:grpc-netty:1.58.0 ' // (1)
Assurez-vous d'extraire la version qui correspond à la version.
La présence des deux bibliothèques sur le chemin de classe est également prise en charge avec la propriété grpc.netty-server.on-collision-prefer-shaded-netty
.
Si vous utilisez le plug-in Spring Boot Dependency Management, il se peut qu'il n'extrait pas la même version que celle avec laquelle il a été compilé, ce qui entraîne un problème d'incompatibilité binaire.
Dans ce cas, vous devrez définir de force et explicitement la version grpc
à utiliser (voir la matrice de versions ici) :
configurations . all {
resolutionStrategy . eachDependency { details ->
if ( " io.grpc " . equalsIgnoreCase(details . requested . group)) {
details . useVersion " 1.58.0 "
}
}
}
Les notes de version avec matrice de compatibilité peuvent être trouvées ici |
Suivez ce guide pour générer des interfaces de stub et de serveur à partir de vos fichiers .proto
.
Si vous êtes stack avec maven, utilisez ce lien.
Annotez vos implémentations d'interface serveur avec @org.lognet.springboot.grpc.GRpcService
Configurez éventuellement le port du serveur dans votre application.yml/properties
. Le port par défaut est 6565
.
grpc :
port : 6565
Un port aléatoire peut être défini en définissant le port sur 0 .Le port réel utilisé peut ensuite être récupéré en utilisant l'annotation @LocalRunningGrpcPort sur le champ int qui injectera le port en cours d'exécution (explicitement configuré ou sélectionné de manière aléatoire) |
Activez éventuellement la réflexion du serveur (voir https://github.com/grpc/grpc-java/blob/master/documentation/server-reflection-tutorial.md)
grpc :
enableReflection : true
Définissez éventuellement l'ordre des phases de démarrage (par défaut Integer.MAX_VALUE
).
grpc :
start-up-phase : XXX
Définissez éventuellement le nombre de secondes d'attente pour la fin des appels préexistants lors de l'arrêt progressif du serveur. Les nouveaux appels seront rejetés pendant cette période. Une valeur négative équivaut à un délai de grâce infini. La valeur par défaut est 0
(cela signifie ne pas attendre).
grpc :
shutdownGrace : 30
Les propriétés du serveur spécifiques à Netty peuvent être spécifiées sous le préfixe grpc.netty-server
.
En configurant l'une des valeurs grpc.netty-server.xxxx
vous définissez implicitement le transport pour qu'il soit basé sur Netty.
grpc :
netty-server :
keep-alive-time : 30s (1)
max-inbound-message-size : 10MB (2)
primary-listen-address : 10.10.15.23:0 (3)
additional-listen-addresses :
- 192.168.0.100:6767 (4)
on-collision-prefer-shaded-netty : false (5)
Les propriétés de type Duration
peuvent être configurées avec le format de valeur de chaîne décrit ici.
Les propriétés de type DataSize
peuvent être configurées avec la valeur de chaîne décrite ici
Exposé sur un réseau IP externe avec un port personnalisé.
Format de valeur de chaîne de propriétés de type SocketAddress
:
host:port
(si la valeur port
est inférieure à 1, utilise une valeur aléatoire)
host:
(utilise le port grpc par défaut, 6565
)
Exposé sur le réseau IP interne également avec le port prédéfini 6767
.
Si vous avez à la fois des bibliothèques Netty shaded
et pure
dans les dépendances, choisissez le type NettyServerBuilder
qui doit être créé. C'est le type qui sera transmis à GRpcServerBuilderConfigurer
(voir Configuration personnalisée du serveur gRPC), la valeur par défaut est true
(c'est-à-dire io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder
; io.grpc.netty.NettyServerBuilder
si false
)
Le démarreur prend également en charge le in-process server
, qui doit être utilisé à des fins de test :
grpc :
enabled : false (1)
inProcessServerName : myTestServer (2)
Désactive le serveur par défaut ( NettyServer
).
Active le serveur in-process
.
Si vous activez à la fois NettyServer et le serveur in-process , ils partageront tous deux la même instance de HealthStatusManager et GRpcServerBuilderConfigurer (voir Configuration personnalisée du serveur gRPC). |
Dans le projet grpc-spring-boot-starter-demo
vous pouvez trouver des exemples entièrement fonctionnels avec des tests d'intégration.
La définition du service à partir du fichier .proto
ressemble à ceci :
service Greeter {
rpc SayHello ( HelloRequest ) returns ( HelloReply ) {}
}
Notez la classe io.grpc.examples.GreeterGrpc.GreeterImplBase
générée qui étend io.grpc.BindableService
.
Tout ce que vous avez à faire est d'annoter l'implémentation de votre service avec @org.lognet.springboot.grpc.GRpcService
@ GRpcService
public static class GreeterService extends GreeterGrpc . GreeterImplBase {
@ Override
public void sayHello ( GreeterOuterClass . HelloRequest request , StreamObserver < GreeterOuterClass . HelloReply > responseObserver ) {
final GreeterOuterClass . HelloReply . Builder replyBuilder = GreeterOuterClass . HelloReply . newBuilder (). setMessage ( "Hello " + request . getName ());
responseObserver . onNext ( replyBuilder . build ());
responseObserver . onCompleted ();
}
}
Le démarreur prend en charge l'enregistrement de deux types d'intercepteurs : global et par service .
Dans les deux cas, l'intercepteur doit implémenter l'interface io.grpc.ServerInterceptor
.
Par prestation
@ GRpcService ( interceptors = { LogInterceptor . class })
public class GreeterService extends GreeterGrpc . GreeterImplBase {
// ommited
}
LogInterceptor
sera instancié via Spring Factory s'il existe un bean de type LogInterceptor
, ou via un constructeur sans argument dans le cas contraire.
Mondial
@ GRpcGlobalInterceptor
public class MyInterceptor implements ServerInterceptor {
// ommited
}
L'annotation sur la méthode java config factory est également prise en charge :
@ Configuration
public class MyConfig {
@ Bean
@ GRpcGlobalInterceptor
public ServerInterceptor globalInterceptor (){
return new ServerInterceptor (){
@ Override
public < ReqT , RespT > ServerCall . Listener < ReqT > interceptCall ( ServerCall < ReqT , RespT > call , Metadata headers , ServerCallHandler < ReqT , RespT > next ) {
// your logic here
return next . startCall ( call , headers );
}
};
}
}
Le service particulier a également la possibilité de désactiver les intercepteurs globaux :
@ GRpcService ( applyGlobalInterceptors = false )
public class GreeterService extends GreeterGrpc . GreeterImplBase {
// ommited
}
Les intercepteurs globaux peuvent être commandés à l'aide des annotations @Ordered
ou @Priority
de Spring. Suivant la sémantique d'ordre de Spring, les valeurs d'ordre inférieur ont une priorité plus élevée et seront exécutées en premier dans la chaîne d'intercepteur.
@ GRpcGlobalInterceptor
@ Order ( 10 )
public class A implements ServerInterceptor {
// will be called before B
}
@ GRpcGlobalInterceptor
@ Order ( 20 )
public class B implements ServerInterceptor {
// will be called after A
}
Le démarreur utilise des intercepteurs intégrés pour implémenter la gestion des erreurs, l'intégration de Spring Security
, Validation
et Metrics
. Leur ordre peut également être contrôlé par les propriétés ci-dessous :
grpc.recovery.interceptor-order
(erreur de gestion de l'ordre des intercepteurs, par défaut Ordered.HIGHEST_PRECEDENCE
)
grpc.security.auth.interceptor-order
( par défaut Ordered.HIGHEST_PRECEDENCE+1
)
grpc.validation.interceptor-order
( par défaut Ordered.HIGHEST_PRECEDENCE+10
)
grpc.metrics.interceptor-order
( par défaut Ordered.HIGHEST_PRECEDENCE+20
)
Cela vous donne la possibilité de configurer l’ordre souhaité des intercepteurs intégrés et personnalisés.
Continuez à lire !!! Il y a plus
La façon dont fonctionne l'intercepteur grpc est qu'il intercepte l'appel et renvoie l'écouteur d'appel du serveur, qui à son tour peut également intercepter le message de demande, avant de le transmettre au gestionnaire d'appel de service réel :
En définissant la propriété grpc.security.auth.fail-fast
sur false
tous les intercepteurs en aval ainsi que tous les intercepteurs en amont (On_Message) seront toujours exécutés en cas d'échec d'authentification/autorisation
En supposant que interceptor_2
est securityInterceptor
:
En cas d'échec de l'authentification/autorisation avec grpc.security.auth.fail-fast=true
(par défaut) :
En cas d'échec de l'authentification/autorisation avec grpc.security.auth.fail-fast=false
:
Ce démarrage est pris en charge nativement par le projet spring-cloud-sleuth
.
Veuillez continuer à rechercher l'intégration de grpc.
En incluant la dépendance org.springframework.boot:spring-boot-starter-actuator
, le démarreur collectera les métriques du serveur gRPC, décomposées par
method
- méthode de service gRPC FQN (nom complet)
result
- Code d'état de la réponse
address
- adresse locale du serveur (si vous avez exposé des adresses d'écoute supplémentaires, avec la propriété grpc.netty-server.additional-listen-addresses
)
Après avoir configuré l'exportateur de votre choix, vous devriez voir le timer
nommé grpc.server.calls
.
En définissant le bean GRpcMetricsTagsContributor
dans le contexte de votre application, vous pouvez ajouter des balises personnalisées au minuteur grpc.server.calls
.
Vous pouvez également utiliser le bean RequestAwareGRpcMetricsTagsContributor
pour marquer les appels unaires et en streaming .
La démo est ici
Gardez la dispersion faible pour ne pas faire exploser la cardinalité de la métrique. |
RequestAwareGRpcMetricsTagsContributor
peut toujours être exécuté en cas d'échec de l'authentification si l'intercepteur metric
a une priorité plus élevée que l'intercepteur security
et si grpc.security.auth.fail-fast
est défini sur false
.
Ce cas est couvert par ce test.
Assurez-vous de lire le chapitre sur la commande des Intercepteurs. |
Assurez-vous d'inclure les dépendances ci-dessous :
implementation "org.springframework.boot:spring-boot-starter-actuator"
implementation "io.micrometer:micrometer-registry-prometheus"
implementation 'org.springframework.boot:spring-boot-starter-web'
Configuration :
management :
metrics :
export :
prometheus :
enabled : true
endpoints :
web :
exposure :
include : " * "
Les points de terminaison standard /actuator/metrics
et /actuator/prometheus
rendront les métriques grpc.server.calls
(voir la démo ici).
Proposition de suppression du GRPC |
Le démarreur peut être configuré automatiquement pour valider les messages de service gRPC de requête/réponse. Veuillez passer à Implémentation de la validation des messages pour plus de détails sur la configuration.
Le starter définit en interne le bean de type java.util.function.Consumer
qui est pris en compte pour le registre des fonctions lorsque spring-cloud-stream
est sur le chemin de classe, ce qui n'est pas souhaitable ( spring-cloud-stream
enregistre automatiquement le canal si vous avez exactement un bean Consommateur/Fournisseur/Fonction dans le contexte de l'application, vous en avez donc déjà un si vous utilisez ce démarreur avec spring-cloud-stream
).
Selon cela, il est recommandé d'utiliser la propriété spring.cloud.function.definition
dans les applications prêtes pour la production et de ne pas s'appuyer sur la découverte automatique.
Veuillez vous référer à la démo GRPC Kafka Stream, la partie essentielle est cette ligne.
Le démarreur fournit une prise en charge intégrée pour l'authentification et l'autorisation des utilisateurs en tirant parti de l'intégration avec le framework Spring Security.
Veuillez vous référer aux sections sur l'intégration de Spring Security pour plus de détails sur les fournisseurs d'authentification pris en charge et les options de configuration.
La sécurité du transport peut être configurée à l'aide du certificat racine avec son chemin de clé privée :
grpc :
security :
cert-chain : classpath:cert/server-cert.pem
private-key : file:../grpc-spring-boot-starter-demo/src/test/resources/cert/server-key.pem
La valeur des deux propriétés est sous une forme prise en charge par ResourceEditor.
Le côté client doit être configuré en conséquence :
(( NettyChannelBuilder ) channelBuilder )
. useTransportSecurity ()
. sslContext ( GrpcSslContexts . forClient (). trustManager ( certChain ). build ());
Ce démarreur extraira la dépendance io.netty:netty-tcnative-boringssl-static
par défaut pour prendre en charge SSL.
Si vous avez besoin d'un autre support SSL/TLS, veuillez exclure cette dépendance et suivre le Guide de sécurité.
Si un réglage plus détaillé est nécessaire pour la configuration de la sécurité, veuillez utiliser le configurateur personnalisé décrit dans Configuration personnalisée du serveur gRPC. |
Pour intercepter l'instance io.grpc.ServerBuilder
utilisée pour créer le io.grpc.Server
, vous pouvez ajouter un bean qui hérite de org.lognet.springboot.grpc.GRpcServerBuilderConfigurer
à votre contexte et remplacer la méthode configure
.
Plusieurs configurateurs sont également pris en charge.
Au moment de l'invocation de la méthode configure
, tous les services découverts, y compris leurs intercepteurs, avaient été ajoutés au constructeur transmis.
Dans votre implémentation de la méthode configure
, vous pouvez ajouter votre configuration personnalisée :
@ Component
public class MyGRpcServerBuilderConfigurer extends GRpcServerBuilderConfigurer {
@ Override
public void configure ( ServerBuilder <?> serverBuilder ){
serverBuilder
. executor ( YOUR EXECUTOR INSTANCE )
. useTransportSecurity ( YOUR TRANSPORT SECURITY SETTINGS );
(( NettyServerBuilder ) serverBuilder ) // cast to NettyServerBuilder (which is the default server) for further customization
. sslContext ( GrpcSslContexts // security fine tuning
. forServer (...)
. trustManager (...)
. build ())
. maxConnectionAge (...)
. maxConnectionAgeGrace (...);
}
};
}
@ Component
public class MyCustomCompressionGRpcServerBuilderConfigurer extends GRpcServerBuilderConfigurer {
@ Override
public void configure ( ServerBuilder <?> serverBuilder ){
serverBuilder
. compressorRegistry ( YOUR COMPRESSION REGISTRY )
. decompressorRegistry ( YOUR DECOMPRESSION REGISTRY ) ;
}
};
}
Si vous activez à la fois NettyServer et les serveurs in-process , la méthode configure sera invoquée sur la même instance du configurateur.Si vous devez faire la différence entre les serverBuilder transmis, vous pouvez vérifier le type.C'est la limitation actuelle. |
GRpcServerInitializedEvent
est publié au démarrage du serveur, vous pouvez le consommer à l'aide de l'API Spring standard.
À partir de la version 5.1.0
, spring-boot-starter-gradle-plugin intègre le plugin de protocole reactive-grpc de SalesForce :
import org.lognet.springboot.grpc.gradle.ReactiveFeature
plugins {
id " io.github.lognet.grpc-spring-boot "
}
grpcSpringBoot {
reactiveFeature . set( ReactiveFeature . REACTOR ) // or ReactiveFeature.RX
}
Voici les tests et les exemples de services grpc réactifs.
Le démarreur enregistre le GRpcExceptionHandlerInterceptor
qui est chargé de propager l'exception lancée par le service aux gestionnaires d'erreurs.
La méthode de gestion des erreurs peut être enregistrée en faisant annoter le bean @GRpcServiceAdvice
avec des méthodes annotées avec les annotations @GRpcExceptionHandler
.
Ceux-ci sont considérés comme des gestionnaires d'erreurs global
et la méthode avec le paramètre de type d'exception le plus proche par la hiérarchie des types de l'exception levée est invoquée.
La signature du gestionnaire d'erreurs doit suivre le modèle ci-dessous :
Type de retour | Paramètre 1 | Paramètre 2 |
---|---|---|
io.grpc.Status | tout type | GRpcExceptionScope |
@ GRpcServiceAdvice
class MyHandler1 {
@ GRpcExceptionHandler
public Status handle ( MyCustomExcpetion exc , GRpcExceptionScope scope ){
}
@ GRpcExceptionHandler
public Status handle ( IllegalArgumentException exc , GRpcExceptionScope scope ){
}
}
@ GRpcServiceAdvice
class MyHandler2 {
@ GRpcExceptionHandler
public Status anotherHandler ( NullPointerException npe , GRpcExceptionScope scope ){
}
}
Vous pouvez avoir autant de beans advice
et de méthodes de gestion que vous le souhaitez, à condition qu'ils n'interfèrent pas les uns avec les autres et ne créent pas d'ambiguïté de type d'exception gérée.
Le bean de service grpc
est également découvert pour les gestionnaires d'erreurs, ayant une priorité plus élevée que les méthodes globales de gestion des erreurs découvertes dans les beans @GRpcServiceAdvice
. Les méthodes de gestion des erreurs au niveau du service sont considérées comme private
et invoquées uniquement lorsque l'exception est levée par ce service :
class SomeException extends Exception {
}
class SomeRuntimeException extends RuntimeException {
}
@ GRpcService
public class HelloService extends GreeterGrpc . GreeterImplBase {
@ Override
public void sayHello ( GreeterOuterClass . HelloRequest request , StreamObserver < GreeterOuterClass . HelloReply > responseObserver ) {
...
throw new GRpcRuntimeExceptionWrapper ( new SomeException ()) ; // (1)
//or
throw new GRpcRuntimeExceptionWrapper ( new SomeException (), "myHint" ); // (2)
//or
throw new SomeRuntimeException (); //(3)
}
@ GRpcExceptionHandler
public Status privateHandler ( SomeException npe , GRpcExceptionScope scope ){
// INVOKED when thrown from HelloService service
String myHint = scope . getHintAs ( String . class ); // (4)
scope . getResponseHeaders (). put ( Metadata . Key . of ( "custom" , Metadata . ASCII_STRING_MARSHALLER ), "Value" ); // (5)
}
@ GRpcExceptionHandler
public Status privateHandler ( SomeRuntimeException npe , GRpcExceptionScope scope ){
// INVOKED when thrown from HelloService service
}
}
@ GRpcServiceAdvice
class MyHandler {
@ GRpcExceptionHandler
public Status anotherHandler ( SomeException npe , GRpcExceptionScope scope ){
// NOT INVOKED when thrown from HelloService service
}
@ GRpcExceptionHandler
public Status anotherHandler ( SomeRuntimeException npe , GRpcExceptionScope scope ){
// NOT INVOKED when thrown from HelloService service
}
}
Étant donné la nature de l'API du service grpc
qui ne permet pas de lancer une exception vérifiée, le type d'exception d'exécution spécial est fourni pour envelopper l'exception vérifiée. Il est ensuite déballé lors de la recherche de la méthode du gestionnaire.
Lors du lancement de l'exception GRpcRuntimeExceptionWrapper
, vous pouvez également transmettre l'objet hint
qui est ensuite accessible à partir de l'objet scope
dans la méthode handler
.
L'exception d'exécution peut être levée telle quelle et n'a pas besoin d'être encapsulée.
Obtenez l’objet indice.
Envoyez des en-têtes personnalisés au client.
L'échec d'authentification se propage via AuthenticationException
et l'échec d'autorisation - via AccessDeniedException
.
L'échec de la validation est propagé via ConstraintViolationException
: pour une demande ayant échoué - avec Status.INVALID_ARGUMENT
comme indice, et pour une réponse ayant échoué - avec Status.FAILED_PRECONDITION
comme indice.
La démo est ici
Grâce à la prise en charge de la configuration Bean Validation via le descripteur de déploiement XML, il est possible de fournir les contraintes pour les classes générées via XML au lieu d'instrumenter les messages générés avec un compilateur protoc
personnalisé.
Ajoutez la dépendance org.springframework.boot:spring-boot-starter-validation
à votre projet.
Créez META-INF/validation.xml
et le(s) fichier(s) de déclarations de contraintes. (IntelliJ IDEA dispose d'un excellent support de saisie semi-automatique pour autoriser les fichiers XML de contraintes de validation de bean)
Voir également des exemples de la documentation du validateur Hibernate
Vous pouvez trouver la configuration de démonstration et les tests correspondants ici
Notez que les messages request
et response
sont en cours de validation.
Si votre méthode gRPC utilise le même type de message de demande et de réponse, vous pouvez utiliser les groupes de validation org.lognet.springboot.grpc.validation.group.RequestMessage
et org.lognet.springboot.grpc.validation.group.ResponseMessage
pour appliquer une logique de validation différente. :
...
< getter name = " someField " >
<!-- should be empty for request message -->
< constraint annotation = " javax.validation.constraints.Size " >
< groups >
< value >org.lognet.springboot.grpc.validation.group.RequestMessage</ value > (1)
</ groups >
< element name = " min " >0</ element >
< element name = " max " >0</ element >
</ constraint >
<!-- should NOT be empty for response message -->
< constraint annotation = " javax.validation.constraints.NotEmpty " >
< groups >
< value >org.lognet.springboot.grpc.validation.group.ResponseMessage</ value > (2)
</ groups >
</ constraint >
</ getter >
...
Appliquer cette contrainte uniquement pour le message request
Appliquer cette contrainte uniquement pour le message response
Notez également la contrainte cross-field personnalisée et son utilisation :
< bean class = " io.grpc.examples.GreeterOuterClass$Person " >
< class >
< constraint annotation = " org.lognet.springboot.grpc.demo.PersonConstraint " />
</ class >
<!-- ... -->
</ bean >
Comme décrit dans le chapitre Ordre des intercepteurs, vous pouvez donner à l'intercepteur validation
une priorité plus élevée que l'intercepteur security
et définir la propriété grpc.security.auth.fail-fast
sur false
.
Dans ce scénario, si l'appel est à la fois non authentifié et invalide, le client obtiendra l'état de réponse Status.INVALID_ARGUMENT
au lieu de Status.PERMISSION_DENIED/Status.UNAUTHENTICATED
. La démo est ici
Bien qu'il soit toujours possible d'annoter vos méthodes rpc avec @Transactional
(avec spring.aop.proxy-target-class=true
si elle n'est pas activée par défaut), il y a de fortes chances que vous obteniez un comportement imprévisible. Considérez ci-dessous l'implémentation de la méthode grpc :
@ GRpcService
class MyGrpcService extends ...{
@ Autowired
private MyJpaRepository repo ;
@ Transactional //(1)
public void rpcCall ( Req request , StreamOvserver < Res > observer ) {
Res response = // Database operations via repo
observer . onNext ( response ); //(2)
observer . onCompleted ();
} //(3)
}
La méthode est annotée comme @Transactional
, Spring validera la transaction à un moment donné après le retour des méthodes
La réponse est renvoyée à l'appelant
Méthodes renvoyées, transaction finalement validée.
Théoriquement, et comme vous pouvez le voir - en pratique, il y a un petit laps de temps pendant lequel le client (si la latence du réseau est minime et que votre serveur grpc encourage le changement de contexte juste après <2>) peut essayer d'accéder à la base de données via un autre appel grpc avant la transaction est validée.
La solution pour surmonter cette situation est d'externaliser la logique transactionnelle dans une classe de service distincte :
@ Service
class MyService {
@ Autowired
private MyJpaRepository repo ;
@ Transactional //(1)
public Res doTransactionalWork (){
// Database operations via repo
return result ;
} //(2)
}
@ GRpcService
class MyGrpcService extends ...{
@ Autowired
private MyService myService ;
public void rpcCall ( Req request , StreamOvserver < Res > observer ) {
Res response = myService . doTransactionalWork ();
observer . onNext ( response ); //(3)
observer . onCompleted ();
}
}
La méthode de service est transactionnelle
La transaction est finalement validée.
Répondez une fois la transaction validée.
En suivant cette approche, vous dissociez également la couche transport et la logique métier qui peuvent désormais être testées séparément.
Schème | Dépendances |
---|---|
Basique |
|
Porteur |
|
Coutume |
|
La configuration de sécurité GRPC suit les mêmes principes et API que la configuration de sécurité Spring WEB, elle est activée par défaut si vous avez une dépendance org.springframework.security:spring-security-config
dans votre chemin de classe.
Vous pouvez utiliser l'annotation @Secured
sur les services/méthodes pour protéger vos points de terminaison, ou en utilisant l'API et en remplaçant les valeurs par défaut (qui précèdent l'annotation @Secured
) :
@ Configuration
class MySecurityCfg extends GrpcSecurityConfigurerAdapter {
@ Override
public void configure ( GrpcSecurity builder ) throws Exception {
MethodsDescriptor <?,?> adminMethods = MyServiceGrpc . getSomeMethod ();
builder
. authorizeRequests ()
. methods ( adminMethods ). hasAnyRole ( "admin" )
. anyMethodExcluding ( adminMethods ). hasAnyRole ( "user" )
. withSecuredAnnotation ();( 1 )
}
}
ou combinez API
avec les annotations @Secured
.
Cette configuration par défaut sécurise les méthodes/services GRPC annotés avec l'annotation org.springframework.security.access.annotation.@Secured
.
Laisser la valeur de l'annotation vide ( @Secured({})
) signifie : authenticate
uniquement, aucune autorisation ne sera effectuée.
Si le bean JwtDecoder
existe dans votre contexte, il enregistrera également JwtAuthenticationProvider
pour gérer la validation de la revendication d'authentification.
BasicAuthSchemeSelector
et BearerTokenAuthSchemeSelector
sont également automatiquement enregistrés pour prendre en charge l'authentification avec nom d'utilisateur/mot de passe et jeton de porteur.
En définissant grpc.security.auth.enabled
sur false
, la sécurité GRPC peut être désactivée.
La personnalisation de la configuration de sécurité GRPC se fait en étendant GrpcSecurityConfigurerAdapter
(divers exemples de configuration et scénarios de test sont disponibles ici.)
@ Configuration
public class GrpcSecurityConfiguration extends GrpcSecurityConfigurerAdapter {
@ Autowired
private JwtDecoder jwtDecoder ;
@ Override
public void configure ( GrpcSecurity builder ) throws Exception {
builder . authorizeRequests ()( 1 )
. methods ( GreeterGrpc . getSayHelloMethod ()). hasAnyAuthority ( "SCOPE_profile" )( 2 )
. and ()
. authenticationProvider ( JwtAuthProviderFactory . withAuthorities ( jwtDecoder ));( 3 )
}
}
Récupérer l'objet de configuration d'autorisation
MethodDefinition
de la méthode sayHello
est autorisée pour les utilisateurs authentifiés disposant de l'autorité SCOPE_profile
.
Utilisez JwtAuthenticationProvider
pour valider la revendication de l'utilisateur (jeton BEARER
) par rapport au serveur de ressources configuré avec la propriété spring.security.oauth2.resourceserver.jwt.issuer-uri
.
Il est possible de connecter votre propre fournisseur d'authentification sur mesure en implémentant l'interface AuthenticationSchemeSelector
.
@ Configuration
public class GrpcSecurityConfiguration extends GrpcSecurityConfigurerAdapter {
@ Override
public void configure ( GrpcSecurity builder ) throws Exception {
builder . authorizeRequests ()
. anyMethod (). authenticated () //(1)
. and ()
. authenticationSchemeSelector ( new AuthenticationSchemeSelector () { //(2)
@ Override
public Optional < Authentication > getAuthScheme ( CharSequence authorization ) {
return new MyAuthenticationObject (); // (3)
}
})
. authenticationProvider ( new AuthenticationProvider () { // (4)
@ Override
public Authentication authenticate ( Authentication authentication ) throws AuthenticationException {
MyAuthenticationObject myAuth = ( MyAuthenticationObject ) authentication ;
//validate myAuth
return MyValidatedAuthenticationObject ( withAuthorities ); //(5)
}
@ Override
public boolean supports ( Class <?> authentication ) {
return MyAuthenticationObject . class . isInstance ( authentication );
}
});
}
}
Sécurisez toutes les méthodes de services.
Enregistrez votre propre AuthenticationSchemeSelector
.
Basé sur l'en-tête d'autorisation fourni - renvoie l'objet Authentication
en tant que revendication (pas encore authentifié)
Enregistrez votre propre AuthenticationProvider
qui prend en charge la validation de MyAuthenticationObject
Valider authentication
fournie et renvoyer l'objet Authentication
validé et authentifié
AuthenticationSchemeSelector
peut également être enregistré en définissant le bean Spring dans le contexte de votre application :
@ Bean
public AuthenticationSchemeSelector myCustomSchemeSelector (){
return authHeader ->{
// your logic here
};
}
La section de prise en charge de la configuration côté client explique comment transmettre le schéma d'autorisation personnalisé et la réclamation auprès du client GRPC.
À partir de la version 4.5.9
vous pouvez également utiliser les annotations standard @PreAuthorize
et @PostAuthorize
sur les méthodes de service grpc et les types de service grpc.
Type d'appel | Référence de l'objet d'entrée | Référence de l'objet de sortie | Échantillon |
---|---|---|---|
Unaire | Par nom de paramètre | | @ Override
@ PreAuthorize ( "#person.age<12" )
@ PostAuthorize ( "returnObject.description.length()>0" )
public void unary ( Person person , StreamObserver < Assignment > responseObserver ) {
} |
Flux d'entrée, | | | @ Override
@ PreAuthorize ( "#p0.getAge()<12" )
@ PostAuthorize ( "returnObject.description.length()>0" )
public StreamObserver < Person > inStream ( StreamObserver < Assignment > responseObserver ) {
} |
Demande unique, | Par nom de paramètre | | @ Override
@ PreAuthorize ( "#person.age<12" )
@ PostAuthorize ( "returnObject.description.length()>0" )
public void outStream ( Person person , StreamObserver < Assignment > responseObserver ) {
} |
Flux Bidi | | | @ Override
@ PreAuthorize ( "#p0.age<12" )
@ PostAuthorize ( "returnObject.description.length()>0" )
public StreamObserver < Person > bidiStream ( StreamObserver < Assignment > responseObserver ) {
} |
Pour obtenir un objet Authentication
dans la mise en œuvre de la méthode sécurisée , veuillez utiliser l'extrait ci-dessous
final Authentication auth = GrpcSecurity . AUTHENTICATION_CONTEXT_KEY . get ();
À partir de 4.5.6
, l'objet Authentication
peut également être obtenu via l'API Spring standard :
final Authentication auth = SecurityContextHolder . getContext (). getAuthentication ();
En ajoutant la dépendance io.github.lognet:grpc-client-spring-boot-starter
à votre application client Java grpc, vous pouvez facilement configurer les informations d'identification par canal ou par appel :
class MyClient {
public void doWork (){
final AuthClientInterceptor clientInterceptor = new AuthClientInterceptor (( 1 )
AuthHeader . builder ()
. bearer ()
. binaryFormat ( true )( 3 )
. tokenSupplier ( this :: generateToken )( 4 )
);
Channel authenticatedChannel = ClientInterceptors . intercept (
ManagedChannelBuilder . forAddress ( "host" , 6565 ). build (), clientInterceptor ( 2 )
);
// use authenticatedChannel to invoke GRPC service
}
private ByteBuffer generateToken (){ ( 4 )
// generate bearer token against your resource server
}
}
Créer un intercepteur client
Canal d'interception
Activez/désactivez le format binaire :
Lorsque true
, l’en-tête d’authentification est envoyé avec la clé Authorization-bin
à l’aide du marshaller binaire.
Lorsque false
, l’en-tête d’authentification est envoyé avec la clé Authorization
à l’aide du marshaller ASCII.
Fournir une fonction de générateur de jetons (veuillez vous référer par exemple.)
class MyClient {
public void doWork (){
AuthCallCredentials callCredentials = new AuthCallCredentials ( ( 1 )
AuthHeader . builder (). basic ( "user" , "pwd" . getBytes ())
);
final SecuredGreeterGrpc . SecuredGreeterBlockingStub securedFutureStub = SecuredGreeterGrpc . newBlockingStub ( ManagedChannelBuilder . forAddress ( "host" , 6565 ));( 2 )
final String reply = securedFutureStub
. withCallCredentials ( callCredentials )( 3 )
. sayAuthHello ( Empty . getDefaultInstance ()). getMessage ();
}
}
Créer des informations d'identification d'appel avec le schéma de base
Créer un coupon de service
Joindre les informations d'identification de l'appel à l'appel
AuthHeader
pourrait également être construit avec un schéma d'autorisation sur mesure :
AuthHeader
. builder ()
. authScheme ( "myCustomAuthScheme" )
. tokenSupplier (()-> generateMyCustomToken ())
Le démarreur enregistre l'implémentation par défaut de HealthServiceImpl.
Vous pouvez fournir le vôtre en enregistrant le bean ManagedHealthStatusService dans le contexte de votre application.
Si vous avez org.springframework.boot:spring-boot-starter-actuator
et org.springframework.boot:spring-boot-starter-web
dans le chemin de classe, le démarreur exposera :
Indicateur de santé grpc
sous /actuator/health
endpoint.
/actuator/grpc
point de terminaison.
Cela peut être contrôlé par les points de terminaison standard et la configuration de l’état.
À partir de la version 3.3.0
, le démarreur enregistrera automatiquement le serveur grpc en cours d'exécution dans le registre Consul si org.springframework.cloud:spring-cloud-starter-consul-discovery
est dans le chemin de classe et spring.cloud.service-registry.auto-registration.enabled
n’est PAS défini sur false
.
Le nom du service enregistré sera préfixé par grpc-
, c'est-à-dire grpc-${spring.application.name}
pour ne pas interférer avec le nom de service Web enregistré standard si vous choisissez d'exécuter à la fois des serveurs Grpc
et Web
intégrés.
ConsulDiscoveryProperties
sont liés aux propriétés de configuration préfixées par spring.cloud.consul.discovery
, puis les valeurs sont écrasées par les propriétés préfixées grpc.consul.discovery
(si définies). Cela vous permet d'avoir une configuration de découverte consul distincte pour les services rest
et grpc
si vous choisissez d'exposer les deux à partir de votre application.
spring :
cloud :
consul :
discovery :
metadata :
myKey : myValue (1)
tags :
- myWebTag (2)
grpc :
consul :
discovery :
tags :
- myGrpcTag (3)
Les services rest
et grpc
sont enregistrés avec les métadonnées myKey=myValue
Les services de repos sont enregistrés auprès de myWebTag
Les services Grpc sont enregistrés auprès de myGrpcTag
Définir spring.cloud.consul.discovery.register-health-check
(ou grpc.consul.discovery.register-health-check
) sur true
enregistrera le service de vérification de l'état GRPC auprès de Consul.
Il existe 4 modes d'inscription pris en charge :
SINGLE_SERVER_WITH_GLOBAL_CHECK
(par défaut)
Dans ce mode, le serveur grpc en cours d'exécution est enregistré en tant que service unique avec une seule vérification grpc
avec serviceId
vide.
Veuillez noter que l'implémentation par défaut ne fait rien et renvoie simplement le statut SERVING
. Vous souhaiterez peut-être fournir votre implémentation de contrôle de santé personnalisée pour ce mode.
SINGLE_SERVER_WITH_CHECK_PER_SERVICE
Dans ce mode, le serveur grpc en cours d'exécution est enregistré en tant que service unique avec une vérification pour chaque service grpc
découvert.
STANDALONE_SERVICES
Dans ce mode, chaque service grpc découvert est enregistré en tant que service unique avec une seule vérification. Chaque service enregistré est étiqueté par son propre nom de service.
NOOP
- aucun service grpc enregistré. Ce mode est utile si vous proposez à la fois des services rest
et grpc
dans votre application, mais pour une raison quelconque, seuls les services rest
doivent être enregistrés auprès de Consul.
grpc :
consule :
registration-mode : SINGLE_SERVER_WITH_CHECK_PER_SERVICE
Lors de la création de services prêts pour la production, il est conseillé d'avoir un projet distinct pour l'API gRPC de votre (vos) service(s) qui contient uniquement des classes proto-générées à la fois pour une utilisation côté serveur et côté client.
Vous ajouterez ensuite ce projet en tant que dépendance implementation
à vos projets gRPC client
et gRPC server
.
Pour intégrer Eureka
suivez simplement le grand guide de Spring.
Vous trouverez ci-dessous les parties essentielles des configurations pour les projets serveur et client.
Ajoutez eureka starter comme dépendance de votre projet de serveur avec les classes générées à partir de fichiers proto
:
dependencies {
implementation( ' org.springframework.cloud:spring-cloud-starter-eureka ' )
implementation project( " :yourProject-api " )
}
Configurez le serveur gRPC pour qu'il s'enregistre auprès d'Eureka.
spring :
application :
name : my-service-name (1)
ServiceId
d'Eureka est par défaut le nom de l'application Spring, fournissez-le avant que le service ne s'enregistre auprès d'Eureka.
grpc :
port : 6565 (1)
eureka :
instance :
nonSecurePort : ${grpc.port} (2)
client :
serviceUrl :
defaultZone : http://${eureka.host:localhost}:${eureka.port:8761}/eureka/ (3)
Spécifiez le numéro de port sur lequel gRPC écoute.
Enregistrez le port du service eureka pour qu'il soit identique à grpc.port
afin que le client sache où envoyer les demandes.
Spécifiez l'URL du registre afin que le service s'enregistre auprès de lui-même.
Exposez le service gRPC dans le cadre de l'application Spring Boot.
@ SpringBootApplication
@ EnableEurekaClient
public class EurekaGrpcServiceApp {
@ GRpcService
public static class GreeterService extends GreeterGrpc . GreeterImplBase {
@ Override
public void sayHello ( GreeterOuterClass . HelloRequest request , StreamObserver < GreeterOuterClass . HelloReply > responseObserver ) {
}
}
public static void main ( String [] args ) {
SpringApplication . run ( DemoApp . class , args );
}
}
Ajoutez eureka starter comme dépendance de votre projet client avec les classes générées à partir de fichiers proto
:
dependencies {
implementation( ' org.springframework.cloud:spring-cloud-starter-eureka ' )
implementation project( " :yourProject-api " )
}
Configurez le client pour trouver le registre du service eureka :
eureka :
client :
register-with-eureka : false (1)
service-url :
defaultZone : http://${eureka.host:localhost}:${eureka.port:8761}/eureka/ (2)
false
si ce projet n'est pas destiné à servir de service à un autre client.
Spécifiez l'URL du registre afin que ce client sache où rechercher le service requis.
@ EnableEurekaClient
@ SpringBootApplication
public class GreeterServiceConsumerApplication {
public static void main ( String [] args ) {
SpringApplication . run ( GreeterServiceConsumerApplication . class , args );
}
}
Utilisez EurekaClient pour obtenir les coordonnées de l'instance de service gRPC d'Eureka et consommer le service :
@ EnableEurekaClient
@ Component
public class GreeterServiceConsumer {
@ Autowired
private EurekaClient client ;
public void greet ( String name ) {
final InstanceInfo instanceInfo = client . getNextServerFromEureka ( "my-service-name" , false ); //(1)
final ManagedChannel channel = ManagedChannelBuilder . forAddress ( instanceInfo . getIPAddr (), instanceInfo . getPort ())
. usePlaintext ()
. build (); //(2)
final GreeterServiceGrpc . GreeterServiceFutureStub stub = GreeterServiceGrpc . newFutureStub ( channel ); //(3)
stub . greet ( name ); //(4)
}
}
Obtenez les informations sur l’instance my-service-name
.
Créez channel
en conséquence.
Créez un stub à l'aide du channel
.
Invoquez le service.
Apache2.0