Bestemmelse af tenant-kontekst og middleware
Find den aktuelle tenant ud fra subdomæner eller tokens, og viderefør konteksten gennem alle dependencies.
Bestemmelse af tenant-kontekst og middleware er en gratis Bootcamp i FastAPI-backendudvikling-lektion på CoddyKit. Dette er lektion 2 af 4. Du kan læse alle 3 lektioner i dette læringsspor gratis i deres fulde længde — derefter låser CoddyKit PRO alle lektioner op samt praktiske øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Den er en del af læringsforløbet i Bootcamp i FastAPI-backendudvikling, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Bootcamp i FastAPI-backendudvikling-kurset indeholder 4 lektioner i alt.
Hvorfor tenant-kontekst er vigtig
I en multi-tenant SaaS betjener én FastAPI-proces mange kunder. Hver forespørgsel skal afgrænses til præcis én tenant, og en fejl her lækker data på tværs af organisationer.
Det grundlæggende problem er, at koden skal vide, hvilken tenant den arbejder på vegne af, når en forespørgsel kører dybt inde i en tjeneste eller et repository. Vi løser dette med bestemmelse af tenant-kontekst:
- Identificér tenanten ud fra den indgående forespørgsel (underdomæne eller token).
- Validér, at tenanten findes og er aktiv.
- Videregiv identiteten, så alle efterfølgende afhængigheder kan læse den.
Denne lektion opbygger dette behandlingsforløb trin for trin.
To strategier til bestemmelse
Der er to almindelige måder at finde tenanten for en forespørgsel på:
- Underdomænebaseret:
acme.app.comknyttes til tenantacme. Læs den fraHost-headeren. Det er velegnet til browsersessioner og branding pr. tenant. - Tokenbaseret: En JWT indeholder en
tenant_id(ofte som en claim). Ideelt til API-klienter og mobilapps, hvor der ikke findes et underdomæne.
Modne systemer understøtter begge med en tydelig prioriteringsregel. Et almindeligt valg er først at stole på tokenets claim (den er signeret) og derefter falde tilbage til underdomænet. Uanset hvad du vælger, skal du dokumentere det og håndhæve det konsekvent.
Analyse af tenant fra et underdomæne
Underdomænet kommer fra Host-headeren. Vi fjerner det kendte basisdomæne og tager det venstre yderste segment. Reserverede segmenter som www og api skal afvises, så de aldrig behandles som tenants.
Denne hjælper består udelukkende af strenglogik, så den er nem at enhedsteste isoleret:
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))Udtrækning af tenant fra en JWT-claim
For API-klienter ligger tenant-identiteten i access-tokenet som en signeret claim. Når signaturen er verificeret, læser du tenant_id-claimen.
Fordi tokenet er signeret, er denne værdi troværdig og bør normalt have forrang frem for subdomænet. Uddraget nedenfor viser formen for afkodning og læsning (verificering er illustreret; i produktion skal du bruge din rigtige hemmelighed og 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"))Lagring af kontekst med contextvars
Når tenant er fastlagt, skal den kunne nås overalt i forespørgslen uden at blive sendt videre som argument til hver eneste funktion. Pythons contextvars er det rigtige værktøj: hver forespørgsel får sin egen isolerede værdi, og det fungerer korrekt ved samtidig asyncio-kørsel.
Vi pakker den rå ContextVar ind i en lille accessor, der kaster en fejl, hvis der ikke er angivet nogen tenant, så manglende kontekst bliver til en tydelig fejl så tidligt som muligt:
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())Middleware til fastlæggelse
Middleware er det rigtige sted at fastlægge tenant: den kører før alle route-handlere og ser den rå forespørgsel. Her kombinerer vi token og subdomæne med en prioritetsregel, angiver contextvar'en og afviser tidligt anonym trafik, når en tenant er påkrævet.
Bemærk try/finally, der nulstiller contextvar-tokenet, så kontekst aldrig siver mellem forespørgsler på den samme worker:
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)Validering af, at tenant findes
At fastlægge en identifikator er ikke det samme som at stole på den. En forespørgsel kan indeholde ghost.app.com for en tenant, der er blevet slettet eller suspenderet. Før du udfører det egentlige arbejde, skal du validere:
- At tenant findes i dit register.
- At den er aktiv (ikke suspenderet og ikke i restance, hvis du håndhæver betalingskrav).
Fastlæg identiteten i middleware, men slå tenant op i databasen via en dependency, så resultatet kan caches pr. forespørgsel og genbruges. Returnér 404 for ukendte tenants og 403 for suspenderede tenants, så du ikke afslører, hvilke tenants der findes.
Eksponering af tenant via en dependency
Route-handlere bør ikke læse contextvar'en direkte. Eksponér i stedet en FastAPI-dependency, der returnerer den validerede tenant-post. Det giver dig ét sted, hvor eksistens og aktiv status håndhæves, og dokumenterer kravet i route-signaturen.
Dependency'en læser det id, som middleware har angivet, indlæser tenant og kaster ellers rene HTTP-fejl:
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)Videregivelse af kontekst til databasen
Den største gevinst ved én sandhedskilde for tenant er automatisk dataafgrænsning. To almindelige mønstre er:
- Filtrering på applikationsniveau: hvert repository-kald tilføjer
WHERE tenant_id = :tidog læser id'et fraget_current_tenant(). - Postgres Row-Level Security (RLS): angiv en sessionsvariabel pr. forespørgsel, så politikker håndhæver isolering direkte i databasen.
Med RLS kører du SET app.tenant_id i starten af hver forbindelses anvendelse, så selv et glemt filter ikke kan krydse tenant-grænser:
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 sessionBaggrundsopgaver mister kontekst
En subtil fælde er, at contextvars er knyttet til den aktuelle udførelseskontekst. Kode, der kører efter svaret (en Celery-opgave, en asyncio.create_task, der er startet uden konteksten, eller et job i en tråd-pool), kan ikke se tenant, medmindre du sender den eksplicit med.
En god tommelfingerregel er at gemme tenant-id'et, mens du stadig er inde i forespørgslen, og derefter give det videre til baggrundsenheden som et eksplicit argument. Stol aldrig på, at contextvar'en overlever grænsen for forespørgslen.
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)Middleware-rækkefølge og test
Rækkefølgen er vigtig. Tenant-middleware skal køre før autorisation og logning, så disse lag allerede kan se tenant. I Starlette/FastAPI kører middleware, der tilføjes sidst, først (den omslutter det yderste lag), så tilføj tenant-fastlæggelse efter CORS, men sørg for, at den udføres før auth.
Til test skal du sende forespørgsler gennem den rigtige middleware-stak og kontrollere isoleringen:
- To forespørgsler med forskellige subdomæner må aldrig kunne se hinandens rækker.
- Et ukendt subdomæne returnerer
404; en suspenderet tenant returnerer403. - En manglende
Hostuden token returnerer400.
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 == 404Hurtigt tjek
Test din forståelse af kontekstvideregivelse gennem forespørgslens livscyklus.
Opsummering
Du har opbygget et komplet kontekstforløb for tenants i multi-tenant FastAPI:
- Fastlæg tenant ud fra en JWT-claim (foretrukket og signeret) eller subdomænet i
Host-headeren med en tydelig prioritetsregel. - Gem den i en
contextvar, der angives af middleware og nulstilles i enfinally-blok, så den aldrig lækker mellem forespørgsler. - Validér eksistens og aktiv status i en dependency, og returnér præcist
404og403. - Videregiv den til databasen via filtre pr. kald eller Postgres RLS-sessionsvariabler som ekstra beskyttelse.
- Vær opmærksom på baggrundsopgaver og tråde: send tenant-id'et eksplicit med, og opret konteksten igen, fordi contextvars ikke overlever grænsen for forespørgslen.
Én sandhedskilde for tenant, håndhævet i hvert lag, er det, der forhindrer en SaaS i at lække data.
Lær Bootcamp i FastAPI-backendudvikling med en AI-underviser — gratis
Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.
- Kurser
- 21
- Lektioner
- 84
Ofte stillede spørgsmål
Er lektionen “Bestemmelse af tenant-kontekst og middleware” gratis?
Ja — alle 3 lektioner i læringssporet Bootcamp i FastAPI-backendudvikling, inklusive “Bestemmelse af tenant-kontekst og middleware”, kan læses gratis i deres fulde længde her på webstedet. Derefter låser CoddyKit PRO alle lektioner op samt interaktive øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Bootcamp i FastAPI-backendudvikling-kurset indeholder 4 lektioner i alt.
Hvad lærer jeg i “Bestemmelse af tenant-kontekst og middleware”?
Find den aktuelle tenant ud fra subdomæner eller tokens, og viderefør konteksten gennem alle dependencies. Du øver dig i Bootcamp i FastAPI-backendudvikling med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.
Skal jeg have erfaring for at begynde på Bootcamp i FastAPI-backendudvikling?
Der kræves ingen tidligere erfaring. Bootcamp i FastAPI-backendudvikling på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 2 af 4.
Hvor lang tid tager lektionen “Bestemmelse af tenant-kontekst og middleware”?
De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.
Kan jeg skrive og køre kode i denne Bootcamp i FastAPI-backendudvikling-lektion?
Ja. Alle Bootcamp i FastAPI-backendudvikling-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.
Alle lektioner i dette kursus
- Strategier og afvejninger for tenant-isolering
- Bestemmelse af tenant-kontekst og middleware
- Row-level security og datapartitionering
- Forbrugsmåling, kvoter og billing-hooks