Routage dynamique basé sur les informations d'identification utilisateur avec API Gateway
April 9, 2023
Routage dynamique basé sur les revendications JWT avec Apache APISIX et Okta
Le routage dynamique est une fonctionnalité puissante de la plupart des passerelles API modernes qui vous permet de router les requêtes entrantes en temps réel vers différents services backend en fonction de divers critères tels que les en-têtes HTTP, les paramètres de requête, ou même le corps de la requête.
En tirant parti des plugins intégrés existants d'Apache APISIX, les développeurs peuvent également créer des règles de routage dynamique basées sur diverses informations d'identification utilisateur telles que les jetons d'accès, les clés API ou les identifiants utilisateur. Dans cet article, nous explorerons les avantages de l'adoption du routage dynamique basé sur les attributs d'authentification avec Apache APISIX et vous montrerons un exemple de configuration sur comment router dynamiquement les requêtes client vers les services backend responsables en fonction de la revendication du jeton JWT.
Objectifs d'apprentissage
Vous apprendrez les éléments suivants tout au long de l'article :
- Routage dynamique du trafic avec une passerelle API.
- Pourquoi avons-nous besoin d'un routage dynamique basé sur les informations d'identification utilisateur ?
- Routage dynamique basé sur les revendications du jeton JWT avec Apache APISIX.
Passerelle API : Routage dynamique du trafic
Le routage dynamique du trafic avec la passerelle API peut être utilisé dans une large gamme d'applications et de scénarios pour optimiser les performances, améliorer la sécurité et garantir que les utilisateurs ont accès aux ressources appropriées.
En routant dynamiquement le trafic, un système peut équilibrer la charge entre différents serveurs ou services. Cela peut aider à garantir une haute disponibilité en routant le trafic vers les services ou serveurs disponibles. Si un service ou un serveur tombe en panne, le trafic peut être automatiquement rerouté vers un autre service ou serveur disponible.
Le routage dynamique peut également être utilisé pour router le trafic en fonction de la géolocalisation de l'utilisateur. Cela peut aider à garantir que les utilisateurs sont connectés au serveur ou service le plus proche, améliorant ainsi les temps de réponse et réduisant la latence.
Passerelle API : Routage dynamique basé sur l'identité de l'utilisateur
Souvent, nous souhaitons router le trafic vers des services spécifiques, des chemins ou afficher uniquement les données liées à l'utilisateur en fonction de l'identité fournie par l'utilisateur. Par exemple, dans les applications multi-locataires, différents locataires peuvent avoir accès à différents services ou ressources. Dans ce cas, la passerelle API peut router le trafic uniquement vers les ressources appropriées du locataire en fonction des informations d'identification de l'utilisateur. Ou dans les applications mobiles, elle peut router le trafic vers des services spécifiques en fonction du type d'appareil ou du système d'exploitation.
Une des approches courantes consiste à utiliser des jetons JWT pour authentifier et autoriser les requêtes aux API. Cela signifie que nous pouvons créer des règles de routage complexes avec la passerelle API qui prennent en compte les revendications présentes dans le jeton JWT et utilisent ces informations pour décider où transférer la requête ou quelles données afficher. Cette approche est particulièrement utile lorsque vous avez plusieurs utilisateurs dans le système qui nécessitent différents niveaux de contrôle d'accès.
Démo : Routage dynamique basé sur les revendications du jeton JWT
Dans cette démo, nous utilisons l'API backend publique existante appelée Conference API avec des informations sur les sessions, les intervenants et les sujets de conférence. En réalité, il peut s'agir de votre service backend. Supposons que nous voulions filtrer et récupérer uniquement les sessions appartenant à un intervenant spécifique qui est connecté au système en utilisant ses informations d'identification telles qu'un jeton JWT. Par exemple, https://conferenceapi.azurewebsites.net/speaker/1/sessions
la requête affiche uniquement les sessions d'un intervenant avec un identifiant unique et cet identifiant unique provient de la revendication du jeton JWT dans le cadre de sa charge utile. Regardez la structure de la charge utile du jeton décodé ci-dessous, il y a également un champ speakerId
inclus :
Dans ce scénario, nous envoyons des requêtes à la même Route à la passerelle API et elle calcule l'URI dynamique à partir de l'en-tête d'autorisation et transfère la requête vers l'URI (voir le diagramme ci-dessous pour comprendre le flux). Pour ce faire, nous allons implémenter un routage dynamique au niveau de la passerelle API Apache APISIX basé sur la revendication du jeton JWT en utilisant les plugins suivants :
- Le plugin openid-connect qui interagit avec le fournisseur d'identité (IdP) et peut intercepter les requêtes non authentifiées à temps pour les applications backend. En tant que fournisseur d'identité, nous utilisons Okta qui émet un jeton JWT avec notre revendication personnalisée et valide le jeton JWT. Ou vous pouvez utiliser d'autres IdP tels que Keycloak, et Ory Hydra, ou vous pouvez même utiliser le plugin jwt pour créer un jeton JWT, et authentifier et autoriser les requêtes.
- Le plugin serverless-pre-function pour écrire un code de fonction Lua personnalisé qui intercepte la requête, décode, analyse une revendication de jeton JWT et stocke la valeur de la revendication dans un nouvel en-tête personnalisé pour prendre des décisions d'autorisation supplémentaires.
- Le plugin proxy-rewrite, une fois que nous avons la revendication dans l'en-tête, nous utilisons ce plugin comme mécanisme de transfert de requête pour déterminer quel chemin URI doit être utilisé pour récupérer les sessions spécifiques à l'intervenant en fonction de la variable d'en-tête Nginx dans notre cas, il s'agit de
speakerId
qui change dynamiquement pour créer différents chemins/speaker/$http_speakerId/sessions
. Le plugin transférera la requête vers la ressource associée dans l'API Conference.
Une fois que nous avons compris ce que nous allons couvrir tout au long de la démo, vérifions les prérequis pour commencer à configurer le scénario ci-dessus et compléter le tutoriel.
Prérequis
- Docker est utilisé pour installer etcd et APISIX conteneurisés.
- curl est utilisé pour envoyer des requêtes à APISIX pour configurer la route, l'amont et les configurations de plugins. Vous pouvez également utiliser des outils simples comme Postman pour interagir avec l'API.
- Apache APISIX est installé dans votre environnement cible. APISIX peut être facilement installé et démarré avec le guide de démarrage rapide.
- Assurez-vous que votre compte OKTA est créé, que vous avez enregistré une nouvelle application (vous pouvez suivre ce guide Configuration d'Okta), ajoutez une revendication personnalisée à un jeton en utilisant le tableau de bord Okta, et demandez un jeton qui contient la revendication personnalisée appelée
speakerId
.
Configurer le service backend (amont)
Vous devrez configurer le service backend pour l'API Conference vers lequel vous souhaitez router les requêtes. Cela peut être fait en ajoutant un serveur amont dans Apache APISIX via l'API Admin.
curl "http://127.0.0.1:9180/apisix/admin/upstreams/1" -X PUT -d '
{
"name": "Conferences API upstream",
"desc": "Register Conferences API as the upstream",
"type": "roundrobin",
"scheme": "https",
"nodes": {
"conferenceapi.azurewebsites.net:443": 1
}
}'
Créer une configuration de plugin
Ensuite, nous configurons un nouvel objet de configuration de plugin. Nous utiliserons 3 plugins openid-connect, serverless-pre-function et proxy-rewrite respectivement comme nous avons discuté des cas d'utilisation de chaque plugin précédemment. Vous devez remplacer uniquement les attributs du plugin openid-connect
(ClienID, Secret, Discovery et Introspection endpoints) par vos propres détails Okta avant d'exécuter la commande curl.
curl "http://127.0.0.1:9180/apisix/admin/plugin_configs/1" -X PUT -d '
{
"plugins": {
"openid-connect":{
"client_id":"{YOUR_OKTA_CLIENT_ID}",
"client_secret":"{YOUR_OKTA_CLIENT_SECRET}",
"discovery":"https://{YOUR_OKTA_ISSUER}/oauth2/default/.well-known/openid-configuration",
"scope":"openid",
"bearer_only":true,
"realm":"master",
"introspection_endpoint_auth_method":"https://{YOUR_OKTA_ISSUER}/oauth2/v1/introspect",
"redirect_uri":"https://conferenceapi.azurewebsites.net/"
},
"proxy-rewrite": {
"uri": "/speaker/$http_speakerId/sessions",
"host":"conferenceapi.azurewebsites.net"
},
"serverless-pre-function": {
"phase": "rewrite",
"functions" : ["return function(conf, ctx)
-- Import neccessary libraries
local core = require(\"apisix.core\")
local jwt = require(\"resty.jwt\")
-- Retrieve the JWT token from the Authorization header
local jwt_token = core.request.header(ctx, \"Authorization\")
if jwt_token ~= nil then
-- Remove the Bearer prefix from the JWT token
local _, _, jwt_token_only = string.find(jwt_token, \"Bearer%s+(.+)\")
if jwt_token_only ~= nil then
-- Decode the JWT token
local jwt_obj = jwt:load_jwt(jwt_token_only)
if jwt_obj.valid then
-- Retrieve the value of the speakerId claim from the JWT token
local speakerId_claim_value = jwt_obj.payload.speakerId
-- Store the speakerId claim value in the header variable
core.request.set_header(ctx, \"speakerId\", speakerId_claim_value)
end
end
end
end
"]}
}
}'
Dans la configuration ci-dessus, la partie la plus difficile à comprendre peut être le code de fonction personnalisé que nous avons écrit en Lua à l'intérieur du plugin serverless-pre-function
:
return function(conf, ctx)
-- Import neccessary libraries
local core = require(\"apisix.core\")
local jwt = require(\"resty.jwt\")
-- Retrieve the JWT token from the Authorization header
local jwt_token = core.request.header(ctx, \"Authorization\")
if jwt_token ~= nil then
-- Remove the Bearer prefix from the JWT token
local _, _, jwt_token_only = string.find(jwt_token, \"Bearer%s+(.+)\")
if jwt_token_only ~= nil then
-- Decode the JWT token
local jwt_obj = jwt:load_jwt(jwt_token_only)
if jwt_obj.valid then
-- Retrieve the value of the speakerId claim from the JWT token
local speakerId_claim_value = jwt_obj.payload.speakerId
-- Store the speakerId claim value in the header variable
core.request.set_header(ctx, \"speakerId\", speakerId_claim_value)
end
end
end
end
En gros, ce plugin sera exécuté avant les deux autres plugins et il fait ce qui suit :
- Récupère le jeton JWT de l'en-tête Authorization.
- Supprime le préfixe "Bearer " du jeton JWT.
- Décode le jeton JWT en utilisant la bibliothèque resty.jwt.
- Récupère la valeur de la revendication "speakerId" du jeton JWT décodé.
- Enfin, il stocke la valeur de la revendication "speakerId" dans la variable d'en-tête speakerId.
Configurer une nouvelle Route
Cette étape consiste à configurer une nouvelle route qui utilise la configuration de plugin, et à configurer la route pour qu'elle fonctionne avec l'amont (en référençant leurs ID) que nous avons créés dans les étapes précédentes :
curl "http://127.0.0.1:9180/apisix/admin/routes/1" -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"name":"Conferences API speaker sessions route",
"desc":"Create a new route in APISIX for the Conferences API speaker sessions",
"methods": ["GET"],
"uri": "/sessions",
"upstream_id":"1",
"plugin_config_id":1
}'
Dans la configuration ci-dessus, nous avons défini les règles de correspondance de route telles que seules les requêtes HTTP GET vers l'URI /sessions
seront routées vers le service backend correct.
Obtenir un jeton d'Okta
Après avoir configuré l'amont, les plugins et la route côté APISIX, nous demandons maintenant un jeton d'Okta qui contient notre revendication personnalisée speakerId
. Vous pouvez suivre le guide qui inclut des informations sur la construction d'une URL pour demander un jeton avec Okta ou simplement utiliser l'URL résultante ci-dessous avec votre émetteur Okta et votre ID client :
https://{YOUR_OKTA_ISSUER}/oauth2/default/v1/authorize?client_id={YOUR_OKTA_CLIENT_ID}
&response_type=id_token
&scope=openid
&redirect_uri=https%3A%2F%2Fconferenceapi.azurewebsites.net
&state=myState
&nonce=myNonceValue
Après avoir collé la requête dans votre navigateur, le navigateur est redirigé vers la page de connexion de votre Okta et génère un ID Token.
https://conferenceapi.azurewebsites.net/#id_token={TOKEN_WILL_BE_HERE}
Notez que le processus de récupération d'un jeton peut être différent pour d'autres fournisseurs d'identité.
Pour vérifier le jeton ID retourné, vous pouvez copier la valeur et la coller dans n'importe quel décodeur JWT (par exemple, https://token.dev).
Tester le routage dynamique
Enfin, nous pouvons maintenant vérifier que la requête est routée vers le bon chemin URI (avec les sessions spécifiques à l'intervenant) en fonction des critères de correspondance et de la revendication du jeton JWT en exécutant une autre commande curl simple :
curl -i -X "GET [http://127.0.0.1:9080/sessions](http://127.0.0.1:9080/sessions)" -H "Authorization: Bearer {YOUR_OKTA_JWT_TOKEN}"
Voilà, le résultat est comme prévu. Si nous définissons speakerId à 1 dans la revendication JWT d'Okta, Apisix a routé la requête vers le chemin URI pertinent et a renvoyé toutes les sessions de cet intervenant dans la réponse.
{
"collection": {
"version": "1.0",
"links": [],
"items": [
{
"href": "https://conferenceapi.azurewebsites.net/session/114",
"data": [
{
"name": "Title",
"value": "\r\n\t\t\tIntroduction to Windows Azure Part I\r\n\t\t"
},
{
"name": "Timeslot",
"value": "04 December 2013 13:40 - 14:40"
},
{
"name": "Speaker",
"value": "Scott Guthrie"
}
],
"links": [
{
"rel": "http://tavis.net/rels/speaker",
"href": "https://conferenceapi.azurewebsites.net/speaker/1"
},
{
"rel": "http://tavis.net/rels/topics",
"href": "https://conferenceapi.azurewebsites.net/session/114/topics"
}
]
},
{
"href": "https://conferenceapi.azurewebsites.net/session/121",
"data": [
{
"name": "Title",
"value": "\r\n\t\t\tIntroduction to Windows Azure Part II\r\n\t\t"
},
{
"name": "Timeslot",
"value": "04 December 2013 15:00 - 16:00"
},
{
"name": "Speaker",
"value": "Scott Guthrie"
}
],
}
]
}
}
Points à retenir
- Avec une passerelle API, vous pouvez router le trafic vers différents services backend en fonction de divers critères.
- Le routage dynamique peut être réalisé en fonction des attributs utilisateur spécifiés dans l'en-tête, la requête ou le corps de la requête.
- Vous pouvez créer des règles de routage complexes qui prennent en compte les revendications présentes dans le jeton JWT, et garantir que seules les requêtes autorisées sont autorisées à accéder à votre API.