Tenantcontextresolutie en middleware
Bepaal de huidige tenant uit subdomeinen of tokens en geef de context door aan elke dependency.
Tenantcontextresolutie en middleware is een gratis Bootcamp backendontwikkeling met FastAPI-les op CoddyKit. Dit is les 2 van 4. Je kunt 3 lessen uit dit leerpad gratis volledig lezen — daarna ontgrendelt CoddyKit PRO alle lessen, plus praktische oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Bootcamp backendontwikkeling met FastAPI. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Bootcamp backendontwikkeling met FastAPI bevat in totaal 4 lessen.
Waarom tenantcontext belangrijk is
In een multi-tenant SaaS bedient één FastAPI-proces veel klanten. Elk verzoek moet precies aan één tenant worden gebonden; als dat misgaat, lekken gegevens tussen organisaties.
Het kernprobleem: tegen de tijd dat een query diep in een service of repository wordt uitgevoerd, moet de code weten voor welke tenant deze handelt. We lossen dit op met het bepalen van de tenantcontext:
- Identificeren — bepaal de tenant aan de hand van het binnenkomende verzoek (subdomein of token).
- Valideren — controleer of de tenant bestaat en actief is.
- Doorgeven — geef die identiteit door zodat elke volgende dependency deze kan lezen.
In deze les bouwen we die pijplijn stap voor stap op.
Twee strategieën om de tenant te bepalen
Er zijn twee gebruikelijke manieren om de tenant van een verzoek te achterhalen:
- Op basis van een subdomein:
acme.app.comverwijst naar tenantacme. Lees dit uit deHost-header. Ideaal voor browsersessies en branding per tenant. - Op basis van een token: een JWT bevat een
tenant_id(vaak als claim). Ideaal voor API-clients en mobiele apps zonder subdomein.
Volwassen systemen ondersteunen beide, met een duidelijke regel voor voorrang. Een gebruikelijke keuze: vertrouw eerst op de tokenclaim (die is ondertekend) en val daarna terug op het subdomein. Welke keuze je ook maakt, documenteer deze en dwing haar consequent af.
De tenant uit een subdomein parseren
Het subdomein komt uit de Host-header. We verwijderen het bekende basisdomein en nemen het meest linkse label. Gereserveerde labels zoals www en api moeten worden afgewezen, zodat ze nooit als tenants worden behandeld.
Deze helper bevat alleen logica voor tekenreeksen en is daarom eenvoudig afzonderlijk met eenheidstests te testen:
BASE_DOMAIN = "app.com"
RESERVED = {"www", "api", "admin", ""}
def tenant_from_host(host: str):
# Strip port if present: 'acme.app.com:8000' -> 'acme.app.com'
host = host.split(":")[0].lower().strip()
if not host.endswith(BASE_DOMAIN):
return None
prefix = host[: -len(BASE_DOMAIN)].rstrip(".")
if not prefix:
return None
label = prefix.split(".")[0]
if label in RESERVED:
return None
return label
for h in ["acme.app.com:8000", "www.app.com", "app.com", "globex.eu.app.com"]:
print(h, "->", tenant_from_host(h))De tenant uit een JWT-claim halen
Voor API-clients zit de tenantidentiteit als een ondertekende claim in het toegangstoken. Nadat je de handtekening hebt geverifieerd, lees je de claim tenant_id uit.
Omdat het token is ondertekend, is deze waarde betrouwbaar en hoort deze meestal voorrang te krijgen op het subdomein. Het onderstaande fragment laat de vorm voor decoderen en uitlezen zien (de verificatie wordt geïllustreerd; gebruik in productie je echte geheim en algoritme):
import base64
import json
def read_unverified_claims(token: str) -> dict:
# A JWT is header.payload.signature, each base64url-encoded.
payload_b64 = token.split(".")[1]
padding = "=" * (-len(payload_b64) % 4)
raw = base64.urlsafe_b64decode(payload_b64 + padding)
return json.loads(raw)
# Demo payload: {"sub": "user-7", "tenant_id": "acme"}
demo = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyLTciLCJ0ZW5hbnRfaWQiOiJhY21lIn0.sig"
claims = read_unverified_claims(demo)
print("tenant_id:", claims.get("tenant_id"))Context opslaan met contextvars
Zodra de tenant is bepaald, moet deze overal in het verzoek beschikbaar zijn zonder hem als argument door elke functie door te geven. Python's contextvars is hiervoor het juiste hulpmiddel: elk verzoek krijgt zijn eigen geïsoleerde waarde en het werkt correct bij gelijktijdige uitvoering met asyncio.
We verpakken de onbewerkte ContextVar in een kleine accessor die een fout veroorzaakt als er geen tenant is ingesteld. Zo wordt ontbrekende context direct en duidelijk gemeld:
from contextvars import ContextVar
_current_tenant: ContextVar[str] = ContextVar("current_tenant")
def set_current_tenant(tenant_id: str) -> None:
_current_tenant.set(tenant_id)
def get_current_tenant() -> str:
try:
return _current_tenant.get()
except LookupError:
raise RuntimeError("No tenant in context")
set_current_tenant("acme")
print("active tenant:", get_current_tenant())De middleware voor het bepalen van de tenant
Middleware is de juiste plek om de tenant te bepalen: deze wordt vóór elke route-handler uitgevoerd en ziet het onbewerkte verzoek. Hier combineren we het token en het subdomein met een prioriteitsregel, stellen we de contextvariabele in en weigeren we vroegtijdig anoniem verkeer waarvoor een tenant vereist is.
Let op de try/finally die het token van de contextvariabele reset, zodat context niet tussen verzoeken op dezelfde worker blijft hangen:
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
class TenantMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
tenant = resolve_from_token(request) or tenant_from_host(
request.headers.get("host", "")
)
if tenant is None:
return JSONResponse({"detail": "Tenant not identified"}, status_code=400)
token = _current_tenant.set(tenant)
try:
request.state.tenant_id = tenant
return await call_next(request)
finally:
_current_tenant.reset(token)
# app.add_middleware(TenantMiddleware)Controleren of de tenant bestaat
Een identificatie bepalen is niet hetzelfde als erop vertrouwen. Een verzoek kan bijvoorbeeld ghost.app.com bevatten voor een tenant die is verwijderd of opgeschort. Voordat je echt werk uitvoert, moet je het volgende valideren:
- De tenant bestaat in je register.
- De tenant is actief (niet opgeschort en niet achterstallig als je facturering als voorwaarde gebruikt).
Bepaal de identiteit in middleware, maar voer het opzoeken in de database uit in een dependency, zodat je het per verzoek kunt cachen en hergebruiken. Retourneer 404 voor onbekende tenants en 403 voor opgeschorte tenants, zodat je niet prijsgeeft welke tenants bestaan.
De tenant beschikbaar maken via een dependency
Route-handlers horen de contextvariabele niet rechtstreeks te lezen. Stel in plaats daarvan een FastAPI-dependency beschikbaar die het gevalideerde tenantrecord retourneert. Zo heb je één plek om het bestaan en de actieve status af te dwingen en wordt de vereiste gedocumenteerd in de routesignatuur.
De dependency leest de id die door de middleware is ingesteld, laadt de tenant en veroorzaakt anders duidelijke HTTP-fouten:
from fastapi import Depends, HTTPException
async def get_tenant(tenant_id: str = Depends(get_current_tenant)):
tenant = await tenant_repo.find_by_slug(tenant_id)
if tenant is None:
raise HTTPException(status_code=404, detail="Unknown tenant")
if not tenant.is_active:
raise HTTPException(status_code=403, detail="Tenant suspended")
return tenant
@router.get("/projects")
async def list_projects(tenant=Depends(get_tenant)):
return await project_repo.list_for(tenant.id)Context doorgeven aan de database
De grootste winst van één bron van waarheid voor de tenant is automatische gegevensafbakening. Er zijn twee veelgebruikte patronen:
- Filteren op toepassingsniveau: elke aanroep van een repository voegt
WHERE tenant_id = :tidtoe en leest de id uitget_current_tenant(). - Postgres Row-Level Security (RLS): stel per verzoek een sessievariabele in en laat beleidsregels de isolatie in de database zelf afdwingen.
Met RLS voer je aan het begin van elk gebruik van een verbinding SET app.tenant_id uit, zodat zelfs een vergeten filter geen gegevens tussen tenants kan laten lekken:
from contextlib import asynccontextmanager
@asynccontextmanager
async def tenant_scoped_session(session_factory):
tenant_id = get_current_tenant()
async with session_factory() as session:
# Bind tenant for the lifetime of this connection's RLS policies.
await session.execute(
text("SET app.tenant_id = :tid"), {"tid": tenant_id}
)
yield sessionAchtergrondtaken verliezen hun context
Een subtiele valkuil: contextvariabelen zijn gekoppeld aan de huidige uitvoeringscontext. Code die na het antwoord wordt uitgevoerd (een Celery-taak, een asyncio.create_task die zonder de context is gestart, of een taak in een threadpool) ziet de tenant niet, tenzij je deze expliciet doorgeeft.
Vuistregel: leg de tenant-id vast terwijl je nog binnen het verzoek werkt en geef deze vervolgens als expliciet argument door aan de achtergrondtaak. Vertrouw er nooit op dat de contextvariabele de verzoekgrens overleeft.
def enqueue_report(background_tasks, tenant=Depends(get_tenant)):
# Capture the id NOW; the worker runs outside this request's context.
tenant_id = tenant.id
background_tasks.add_task(build_report, tenant_id=tenant_id)
return {"status": "queued"}
async def build_report(tenant_id: str):
# Re-establish context inside the task before touching the DB.
set_current_tenant(tenant_id)
await generate(tenant_id)Volgorde van middleware en tests
De volgorde is belangrijk. De tenant-middleware moet vóór autorisatie en logboekregistratie worden uitgevoerd, zodat die lagen de tenant al kunnen zien. In Starlette/FastAPI wordt middleware die als laatste is toegevoegd als eerste uitgevoerd (deze vormt de buitenste laag). Voeg tenantbepaling dus na CORS toe, maar zorg dat deze in de uitvoering vóór authenticatie komt.
Laat voor tests verzoeken door de echte middlewarestapel lopen en controleer de isolatie:
- Twee verzoeken met verschillende subdomeinen mogen nooit elkaars rijen zien.
- Een onbekend subdomein retourneert
404; een opgeschorte tenant retourneert403. - Een ontbrekende
Hostzonder token retourneert400.
def test_subdomain_isolation(client):
r1 = client.get("/projects", headers={"Host": "acme.app.com"})
r2 = client.get("/projects", headers={"Host": "globex.app.com"})
assert r1.status_code == 200 and r2.status_code == 200
assert r1.json() != r2.json()
def test_unknown_tenant_404(client):
r = client.get("/projects", headers={"Host": "ghost.app.com"})
assert r.status_code == 404Korte controle
Test je begrip van het doorgeven van context gedurende de levenscyclus van een verzoek.
Samenvatting
Je hebt een complete tenantcontextpijplijn gebouwd voor multi-tenant FastAPI:
- Bepaal de tenant aan de hand van een JWT-claim (voorkeur, ondertekend) of het subdomein in de
Host-header, met een duidelijke prioriteitsregel. - Sla deze op in een
contextvardie door middleware wordt ingesteld en in eenfinally-blok wordt gereset, zodat deze nooit tussen verzoeken lekt. - Valideer het bestaan en de actieve status in een dependency en retourneer precies
404of403. - Geef de tenant door aan de database via filters per aanroep of sessievariabelen van Postgres RLS voor extra bescherming.
- Let op achtergrondtaken en threads: geef de tenant-id expliciet door en stel de context opnieuw in, omdat contextvariabelen de verzoekgrens niet overleven.
Eén bron van waarheid voor de tenant, die in elke laag wordt afgedwongen, voorkomt dat een SaaS gegevens laat lekken.
Leer Bootcamp backendontwikkeling met FastAPI met een AI-tutor — gratis
Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.
- Cursussen
- 21
- Lessen
- 84
Veelgestelde vragen
Is de les “Tenantcontextresolutie en middleware” gratis?
Ja — je kunt hier op het web alle 3 lessen van het leerpad Bootcamp backendontwikkeling met FastAPI, waaronder “Tenantcontextresolutie en middleware”, gratis volledig lezen. Daarna ontgrendelt CoddyKit PRO alle lessen, plus interactieve oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. De cursus Bootcamp backendontwikkeling met FastAPI bevat in totaal 4 lessen.
Wat leer ik in “Tenantcontextresolutie en middleware”?
Bepaal de huidige tenant uit subdomeinen of tokens en geef de context door aan elke dependency. Je oefent met Bootcamp backendontwikkeling met FastAPI door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.
Heb ik ervaring nodig om met Bootcamp backendontwikkeling met FastAPI te beginnen?
Ervaring vooraf is niet nodig. Bootcamp backendontwikkeling met FastAPI op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 2 van 4.
Hoe lang duurt de les “Tenantcontextresolutie en middleware”?
De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.
Kan ik code schrijven en uitvoeren in deze les over Bootcamp backendontwikkeling met FastAPI?
Ja. Elke les over Bootcamp backendontwikkeling met FastAPI bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.
Alle lessen in deze cursus
- Strategieën en afwegingen voor tenantisolatie
- Tenantcontextresolutie en middleware
- Beveiliging en partitionering op rijniveau
- Gebruiksmeting, quota's en billinghooks