Bootcamp i backendutveckling med FastAPI · Lektion

Identifiering av klientkontext och middleware

Identifiera den aktuella klienten från subdomäner eller tokens och vidarebefordra kontexten genom varje beroende.

Lektion 2 av 413 steg

Identifiering av klientkontext och middleware är en gratis lektion i Bootcamp i backendutveckling med FastAPI på CoddyKit. Detta är lektion 2 av 4. Du kan läsa vilka 3 lektioner som helst i den här lärvägen kostnadsfritt i sin helhet – därefter låser CoddyKit PRO upp alla lektioner, plus praktisk övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Den ingår i lärvägen för Bootcamp i backendutveckling med FastAPI, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Bootcamp i backendutveckling med FastAPI innehåller totalt 4 lektioner.

Varför tenant-kontext är viktig

I en multi-tenant-SaaS betjänar en enda FastAPI-process många kunder. Varje förfrågan måste avgränsas till exakt en tenant, och om detta blir fel läcker data mellan organisationer.

Kärnproblemet är att koden, när en fråga körs långt inne i en tjänst eller ett repository, måste veta vilken tenant den arbetar för. Vi löser detta med upplösning av tenant-kontext:

  • Identifiera tenanten från den inkommande förfrågan (subdomän eller token).
  • Validera att tenanten finns och är aktiv.
  • Vidarebefordra identiteten så att alla efterföljande dependencies kan läsa den.

Den här lektionen bygger upp den processen steg för steg.

Två strategier för att hitta tenanten

Det finns två vanliga sätt att hitta tenanten för en förfrågan:

  • Subdomänsbaserat: acme.app.com mappas till tenant acme. Läs från Host-headern. Passar bra för webbläsarsessioner och varumärkesanpassning per tenant.
  • Tokenbaserat: en JWT innehåller en tenant_id (ofta som en claim). Idealiskt för API-klienter och mobilappar där det inte finns någon subdomän.

Mogna system stöder båda, med en tydlig prioritetsregel. Ett vanligt val är att lita på token-claimen först (den är signerad) och sedan falla tillbaka på subdomänen. Oavsett vad ni väljer ska ni dokumentera det och tillämpa det konsekvent.

Tolka tenant från en subdomän

Subdomänen kommer från Host-headern. Vi tar bort den kända basdomänen och använder etiketten längst till vänster. Reserverade etiketter som www och api måste avvisas så att de aldrig behandlas som tenants.

Den här hjälpfunktionen består enbart av stränglogik och är därför enkel att enhetstesta isolerat:

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))

Hämta tenant från en JWT-claim

För API-klienter finns tenant-identiteten i åtkomsttoken som ett signerat claim. När signaturen har verifierats läser du claimet tenant_id.

Eftersom token är signerad är värdet tillförlitligt och bör vanligtvis ha företräde framför subdomänen. Kodexemplet nedan visar strukturen för avkodning och läsning (verifieringen illustreras; i produktion använder du din riktiga hemlighet och algoritm):

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"))

Lagra kontext med contextvars

När tenant har identifierats måste den vara åtkomlig överallt i begäran utan att skickas genom varje funktionsargument. Pythons contextvars är rätt verktyg: varje begäran får ett eget isolerat värde och det fungerar korrekt vid samtidighet med asyncio.

Vi kapslar in den råa ContextVar i en liten accessor som kastar ett fel om ingen tenant har angetts. På så sätt blir en saknad kontext ett tydligt fel som upptäcks tidigt:

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 för identifiering

Middleware är rätt plats för att identifiera tenant: den körs före alla route-hanterare och ser den råa begäran. Här kombinerar vi token och subdomän med en prioritetsregel, anger contextvar och avvisar tidigt trafik som saknar autentisering men kräver en tenant.

Observera try/finally som återställer contextvar-token, så att kontexten aldrig läcker mellan begäranden på samma 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)

Verifiera att tenant finns

Att identifiera en identitet är inte samma sak som att lita på den. En begäran kan innehålla ghost.app.com för en tenant som har tagits bort eller stängts av. Innan du utför det egentliga arbetet måste du validera följande:

  • Tenant finns i ditt register.
  • Den är aktiv (inte avstängd och inte förfallen med betalningen om du använder fakturering som villkor).

Identifiera identiteten i middleware, men gör databassökningen i ett beroende så att resultatet kan cachelagras per begäran och återanvändas. Returnera 404 för okända tenants och 403 för avstängda, så att du inte avslöjar vilka tenants som finns.

Exponera tenant via ett beroende

Route-hanterare bör inte läsa contextvar direkt. Exponera i stället ett FastAPI-beroende som returnerar den validerade tenant-posten. Då får du en enda plats där förekomst och aktiv status säkerställs, och kravet dokumenteras i routens signatur.

Beroendet läser id:t som middleware angav, läser in tenant och kastar annars tydliga HTTP-fel:

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)

Vidarebefordra kontext till databasen

Den största vinsten med en enda källa till tenant är automatisk databegränsning. Två vanliga mönster är:

  • Filtrering på applikationsnivå: varje repository-anrop lägger till WHERE tenant_id = :tid och läser id:t från get_current_tenant().
  • Postgres Row-Level Security (RLS): ange en sessionsvariabel per begäran så att policies upprätthåller isoleringen direkt i databasen.

Med RLS kör du SET app.tenant_id i början av varje anslutnings användning, så att inte ens ett glömt filter kan korsa 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 session

Bakgrundsjobb förlorar kontext

En subtil fallgrop är att contextvars är knutna till den aktuella exekveringskontexten. Kod som körs efter svaret (en Celery-uppgift, en asyncio.create_task som skapats utan kontext eller ett jobb i en trådpool) ser inte tenant om du inte skickar den explicit.

En bra tumregel är att läsa av tenant-id:t medan du fortfarande befinner dig i begäran och sedan skicka det till bakgrundsarbetet som ett explicit argument. Förlita dig aldrig på att contextvar överlever gränsen för begäran.

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)

Middlewareordning och testning

Ordningen spelar roll. Tenant-middleware måste köras före auktorisering och loggning, så att dessa lager redan kan se tenant. I Starlette/FastAPI körs middleware som läggs till sist först (den kapslar in det yttersta lagret), så lägg till tenant-identifieringen efter CORS men säkerställ att den körs före auth.

I testerna ska du skicka begäranden genom den riktiga middleware-stacken och kontrollera isoleringen:

  • Två begäranden med olika subdomäner får aldrig se varandras rader.
  • En okänd subdomän returnerar 404; en avstängd tenant returnerar 403.
  • En begäran utan Host och utan token returnerar 400.
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 == 404

Snabbkontroll

Testa din förståelse av hur kontext vidarebefordras genom begärans livscykel.

Sammanfattning

Du har byggt en komplett pipeline för tenant-kontext i FastAPI med flera tenants:

  • Identifiera tenant från ett JWT-claim (föredras och är signerat) eller subdomänen i Host-headern, med en tydlig prioritetsregel.
  • Lagra den i en contextvar som anges av middleware och återställs i ett finally-block, så att den aldrig läcker mellan begäranden.
  • Validera förekomst och aktiv status i ett beroende och returnera exakt 404 respektive 403.
  • Vidarebefordra till databasen via filter per anrop eller Postgres RLS-sessionsvariabler för försvar i flera lager.
  • Var uppmärksam på bakgrundsjobb och trådar: skicka tenant-id:t explicit och återskapa kontexten, eftersom contextvars inte överlever gränsen för begäran.

En enda källa till tenant, som upprätthålls i varje lager, är det som hindrar en SaaS-tjänst från att läcka data.

Gratis att börja

Lär dig Bootcamp i backendutveckling med FastAPI med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
21
Lektioner
84

Vanliga frågor

Är lektionen ”Identifiering av klientkontext och middleware” gratis?

Ja – du kan läsa vilka 3 lektioner som helst i lärvägen Bootcamp i backendutveckling med FastAPI, inklusive ”Identifiering av klientkontext och middleware”, kostnadsfritt i sin helhet här på webben. Därefter låser CoddyKit PRO upp alla lektioner, plus interaktiv övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Kursen i Bootcamp i backendutveckling med FastAPI innehåller totalt 4 lektioner.

Vad lär jag mig i ”Identifiering av klientkontext och middleware”?

Identifiera den aktuella klienten från subdomäner eller tokens och vidarebefordra kontexten genom varje beroende. Ni övar på Bootcamp i backendutveckling med FastAPI med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Bootcamp i backendutveckling med FastAPI?

Du behöver inga förkunskaper. Utbildningen i Bootcamp i backendutveckling med FastAPI på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 2 av 4.

Hur lång tid tar lektionen ”Identifiering av klientkontext och middleware”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Bootcamp i backendutveckling med FastAPI-lektionen?

Ja. Varje Bootcamp i backendutveckling med FastAPI-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Strategier och avvägningar för klientisolering
  2. Identifiering av klientkontext och middleware
  3. Säkerhet på radnivå och datapartitionering
  4. Användningsmätning, kvoter och billing-hooks
← Tillbaka till Bootcamp i backendutveckling med FastAPI