Bauen Sie Ihr eigenes Forum mit FastAPI: Schritt 4 – Benutzersystem
Min-jun Kim
Dev Intern · Leapcell

Im vorherigen Artikel haben wir die Jinja2-Template-Engine verwendet, um den Frontend-HTML-Code von der Backend-Python-Logik zu trennen und die Projektstruktur übersichtlicher zu gestalten.
Das aktuelle Forum erlaubt es jedem, anonym zu posten, was nicht der richtige Weg ist, eine Community zu betreiben. Ein Forum sollte um Benutzer aufgebaut sein: Jeder hat seine eigene Identität, seine eigenen Beiträge und Antworten.
Daher werden wir in diesem Artikel ein komplettes Benutzersystem zum Forum hinzufügen, einschließlich Registrierungs-, Anmelde- und Abmeldefunktionen.
Schritt 1: Abhängigkeiten installieren
Wir benötigen eine Bibliothek zur Handhabung der Passwortverschlüsselung. Benutzernamenpasswörter dürfen nicht im Klartext gespeichert werden, was extrem gefährlich ist. Wir werden passlib und den pbkdf2_sha256-Algorithmus verwenden.
Führen Sie den folgenden Befehl aus:
pip install "passlib[pbkdf2_sha256]"
Schritt 2: Das Datenbankmodell aktualisieren
Wir benötigen eine neue Tabelle zum Speichern von Benutzerinformationen, und wir müssen die posts-Tabelle mit der users-Tabelle verknüpfen, um den Autor jedes Beitrags zu speichern.
Öffnen Sie die Datei models.py und nehmen Sie die folgenden Änderungen vor:
models.py (Aktualisiert)
from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship from database import Base class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) username = Column(String, unique=True, index=True) hashed_password = Column(String) posts = relationship("Post", back_populates="owner") class Post(Base): __tablename__ = "posts" id = Column(Integer, primary_key=True, index=True) title = Column(String, index=True) content = Column(String) owner_id = Column(Integer, ForeignKey("users.id")) owner = relationship("User", back_populates="posts")
Hier wurden zwei Dinge getan:
User-Modell erstellen:- Definiert die
users-Tabelle, die die Felderid, eindeutigenusernameundhashed_passwordenthält.
- Definiert die
PostundUserverknüpfen:- Im
Post-Modell wurde einowner_id-Feld als Fremdschlüssel hinzugefügt, das auf dieidderusers-Tabelle verweist. - Unter Verwendung von
relationshipvon SQLAlchemy wurde eine bidirektionale Verknüpfung zwischenPostundUserhergestellt. Jetzt können wir überpost.ownerauf den Autor eines Beitrags zugreifen, und wir können auch überuser.postsauf alle Beiträge eines Benutzers zugreifen.
- Im
Bevor Sie diese Modelle anwenden, müssen Sie Ihre Datenbank manuell aktualisieren. Sie müssen die users-Tabelle erstellen und die posts-Tabelle ändern.
Die entsprechenden SQL-Anweisungen lauten wie folgt:
-- Erstellen Sie die Benutzertabelle CREATE TABLE users ( id SERIAL PRIMARY KEY, username VARCHAR UNIQUE, hashed_password VARCHAR ); -- Ändern Sie die Beiträge-Tabelle, fügen Sie die Spalte owner_id und eine Fremdschlüsselbeschränkung hinzu ALTER TABLE posts ADD COLUMN owner_id INTEGER; ALTER TABLE posts ADD CONSTRAINT fk_owner_id FOREIGN KEY (owner_id) REFERENCES users (id);
Wenn Ihre Datenbank mit Leapcell erstellt wurde,
können Sie diese SQL-Anweisungen direkt im webbasierten Bedienfeld ausführen.

Schritt 3: Passwörter verarbeiten
Wir erstellen eine neue Datei auth.py und schreiben Funktionen zum Hashing und Überprüfen von Passwörtern, um Passwörter sicher zu verarbeiten.
auth.py
from passlib.context import CryptContext # 1. Erstellen Sie eine CryptContext-Instanz und geben Sie den Verschlüsselungsalgorithmus an pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # 2. Funktion zur Überprüfung des Passworts def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) # 3. Funktion zum Generieren des Passwort-Hashs def get_password_hash(password): return pwd_context.hash(password)
verify_password: Vergleicht das vom Benutzer eingegebene Klartextpasswort mit dem im Datenbank gespeicherten Hash-Passwort, um festzustellen, ob sie übereinstimmen.get_password_hash: Wandelt das Klartextpasswort in einen Hash-Wert um, damit es in der Datenbank gespeichert werden kann.
Schritt 4: Benutzerregistrierungs- und Anmeldeseiten erstellen
Ähnlich wie posts.html erstellen wir zwei neue HTML-Dateien im templates-Ordner: register.html und login.html.
templates/register.html
<!DOCTYPE html> <html> <head> <title>Registrieren - Mein FastAPI Forum</title> <style> body { font-family: sans-serif; margin: 2em; } form { width: 300px; margin: 0 auto; } input { width: 100%; padding: 8px; margin-bottom: 10px; box-sizing: border-box; } button { padding: 10px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; width: 100%; } .error { color: red; } </style> </head> <body> <h1>Neuen Benutzer registrieren</h1> {% if error %} <p class="error">{{ error }}</p> {% endif %} <form method="post"> <input type="text" name="username" placeholder="Benutzername" required /><br /> <input type="password" name="password" placeholder="Passwort" required /><br /> <button type="submit">Registrieren</button> </form> </body> </html>
templates/login.html
<!DOCTYPE html> <html> <head> <title>Anmelden - Mein FastAPI Forum</title> <style> body { font-family: sans-serif; margin: 2em; } form { width: 300px; margin: 0 auto; } input { width: 100%; padding: 8px; margin-bottom: 10px; box-sizing: border-box; } button { padding: 10px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; width: 100%; } .error { color: red; } </style> </head> <body> <h1>Benutzeranmeldung</h1> {% if error %} <p class="error">{{ error }}</p> {% endif %} <form method="post"> <input type="text" name="username" placeholder="Benutzername" required /><br /> <input type="password" name="password" placeholder="Passwort" required /><br /> <button type="submit">Anmelden</button> </form> </body> </html>
Schritt 5: API-Routen für Authentifizierung implementieren
Nun werden wir main.py refaktorieren, um Funktionen für Registrierung, Anmeldung, Abmeldung und die Verwaltung des aktuellen Benutzerstatus hinzuzufügen. Dies ist eine relativ große Aktualisierung.
main.py (Endgültige vollständige Version)
from fastapi import FastAPI, Form, Depends, Request, Response, HTTPException, status from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc from sqlalchemy.orm import selectinload from typing import Optional import models from database import get_db from auth import get_password_hash, verify_password app = FastAPI() templates = Jinja2Templates(directory="templates") # --- Abhängigkeit für den Benutzerstatus --- async def get_current_user(request: Request, db: AsyncSession = Depends(get_db)) -> Optional[models.User]: username = request.cookies.get("forum_user") if not username: return None result = await db.execute(select(models.User).where(models.User.username == username)) return result.scalar_one_or_none() # --- Routen --- @app.get("/", response_class=RedirectResponse) def read_root(): return RedirectResponse(url="/posts", status_code=status.HTTP_302_FOUND) @app.get("/posts", response_class=HTMLResponse) async def view_posts(request: Request, db: AsyncSession = Depends(get_db), current_user: Optional[models.User] = Depends(get_current_user)): # Verwenden Sie selectinload, um die owner-Beziehung vorzuladen und das N+1-Abfrageproblem zu vermeiden result = await db.execute( select(models.Post).options(selectinload(models.Post.owner)).order_by(desc(models.Post.id)) ) posts = result.scalars().all() return templates.TemplateResponse("posts.html", {"request": request, "posts": posts, "current_user": current_user}) @app.post("/api/posts") async def create_post( title: str = Form(...), content: str = Form(...), db: AsyncSession = Depends(get_db), current_user: Optional[models.User] = Depends(get_current_user) ): if not current_user: return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) new_post = models.Post(title=title, content=content, owner_id=current_user.id) db.add(new_post) await db.commit() await db.refresh(new_post) return RedirectResponse(url="/posts", status_code=status.HTTP_303_SEE_OTHER) @app.get("/register", response_class=HTMLResponse) async def get_registration_form(request: Request): return templates.TemplateResponse("register.html", {"request": request}) @app.post("/register") async def register_user( request: Request, username: str = Form(...), password: str = Form(...), db: AsyncSession = Depends(get_db) ): result = await db.execute(select(models.User).where(models.User.username == username)) if result.scalar_one_or_none(): return templates.TemplateResponse("register.html", {"request": request, "error": "Benutzername existiert bereits"}) hashed_password = get_password_hash(password) new_user = models.User(username=username, hashed_password=hashed_password) db.add(new_user) await db.commit() return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER) @app.get("/login", response_class=HTMLResponse) async def get_login_form(request: Request): return templates.TemplateResponse("login.html", {"request": request}) @app.post("/login") async def login_user( response: Response, request: Request, username: str = Form(...), password: str = Form(...), db: AsyncSession = Depends(get_db) ): result = await db.execute(select(models.User).where(models.User.username == username)) user = result.scalar_one_or_none() if not user or not verify_password(password, user.hashed_password): return templates.TemplateResponse("login.html", {"request": request, "error": "Falscher Benutzername oder falsches Passwort"}) # Verwenden Sie einen Cookie zur Implementierung einer einfachen Sitzung response = RedirectResponse(url="/posts", status_code=status.HTTP_303_SEE_OTHER) response.set_cookie(key="forum_user", value=user.username, httponly=True) return response @app.get("/logout") def logout_user(response: Response): response = RedirectResponse(url="/posts", status_code=status.HTTP_303_SEE_OTHER) response.delete_cookie(key="forum_user") return response
Diese Datei hat hauptsächlich folgende Änderungen vorgenommen:
- Die Funktion
get_current_userhinzugefügt: Diese Funktion liest den Cookieforum_userin der Anfrage, um den aktuellen Benutzer zu identifizieren. In nachfolgenden Routen können wir überDepends(get_current_user)direkt auf die Informationen des angemeldeten Benutzers zugreifen. - Routen für die Benutzerregistrierung und -anmeldung hinzugefügt
- Registrierung (
/register): Die GET-Anfrage zeigt das Registrierungsformular an, und die POST-Anfrage verarbeitet die Formularübermittlung. Sie prüft, ob der Benutzername bereits existiert, hasht dann das Passwort und speichert es in der Datenbank. - Anmeldung (
/login): Die GET-Anfrage zeigt das Anmeldeformular an. Die POST-Anfrage überprüft Benutzername und Passwort. Bei Erfolg setzt sie einen Cookie namensforum_userin der Antwort und setzt den Benutzernamen als Wert. Dies ist eine einfache Sitzungsimplementierung. - Abmeldung (
/logout): Löscht den Cookieforum_userund leitet zur Homepage zurück.
- Registrierung (
- Routenschutz: Die Route
create_posthängt jetzt vonget_current_userab. Wenn der Benutzer nicht angemeldet ist, wird er zur Anmeldeseite weitergeleitet. Beim Posten wird dieowner_iddes Beitrags automatisch auf die ID des aktuell angemeldeten Benutzers gesetzt. - Ansichtsaktualisierung: Routen wie
/postsrufen nun die Informationen des aktuellen Benutzers ab und übergeben sie an die Vorlage, damit der Anmeldestatus auf der Seite angezeigt werden kann.
Schritt 6: Vorlage der Homepage aktualisieren, um den Benutzerstatus anzuzeigen
Schließlich müssen wir templates/posts.html ändern, damit es je nach Anmeldestatus des Benutzers unterschiedliche Inhalte anzeigen kann.
templates/posts.html (Aktualisiert)
<!DOCTYPE html> <html> <head> <title>Mein FastAPI Forum</title> <style> body { font-family: sans-serif; margin: 2em; } input, textarea { width: 100%; padding: 8px; margin-bottom: 10px; box-sizing: border-box; } button { padding: 10px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; } header { display: flex; justify-content: space-between; align-items: center; } </style> </head> <body> <header> <h1>Willkommen in meinem Forum</h1> <div class="auth-links"> {% if current_user %} <span>Willkommen, {{ current_user.username }}!</span> <a href="/logout">Abmelden</a> {% else %} <a href="/login">Anmelden</a> | <a href="/register">Registrieren</a> {% endif %} </div> </header> {% if current_user %} <h2>Neuen Beitrag erstellen</h2> <form action="/api/posts" method="post"> <input type="text" name="title" placeholder="Beitragstitel" required /><br /> <textarea name="content" rows="4" placeholder="Beitragsinhalt" required></textarea><br /> <button type="submit">Posten</button> </form> {% else %} <p><a href="/login">Anmelden</a>, um einen neuen Beitrag zu erstellen.</p> {% endif %} <hr /> <h2>Beitragsliste</h2> {% for post in posts %} <div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px;"> <h3>{{ post.title }}</h3> <p>{{ post.content }}</p> <small>Autor: {{ post.owner.username if post.owner else 'Unbekannt' }}</small> </div> {% endfor %} </body> </html>
Die Vorlage hat hauptsächlich folgende Änderungen vorgenommen:
- Die obere Navigation verwendet
{% if current_user %}, um den Anmeldestatus zu bestimmen. Wenn der Benutzer angemeldet ist, wird eine Willkommensnachricht und ein Link zum Abmelden angezeigt; andernfalls werden die Links Anmelden und Registrieren angezeigt. - Das Formular zum Erstellen eines neuen Beitrags ist eingeschränkt, sodass nur angemeldete Benutzer es sehen können.
- Am unteren Rand jedes Beitrags wird der Benutzername des Autors über
{{ post.owner.username }}angezeigt.
Ausführen und Überprüfen
Es ist an der Zeit, die Ergebnisse zu sehen! Starten Sie Ihren Uvicorn-Server neu:
uvicorn main:app --reload
Besuchen Sie http://127.0.0.1:8000. Sie sehen die Links "Anmelden" und "Registrieren" in der oberen rechten Ecke der Homepage, und es gibt keinen Bereich zum Erstellen von Beiträgen auf der Seite.

Versuchen Sie, einen neuen Benutzer zu registrieren und sich dann anzumelden. Nach der Anmeldung sehen Sie das Beitragsformular und Ihr Benutzername wird oben auf der Seite angezeigt.

Posten Sie einen Beitrag, und sein Autor wird korrekt als Ihr Benutzername angezeigt.

Zusammenfassung
Durch diesen Artikel haben wir ein Benutzersystem für das Forum erstellt. Jeder kann sich jetzt registrieren, anmelden und eigene Beiträge veröffentlichen.
Nachdem die Beiträge eigene Benutzer haben, können Sie über das nächste nachdenken: Was, wenn der Benutzer den Inhalt der Beiträge, die er veröffentlicht hat, ändern möchte?
Im nächsten Artikel werden wir eine neue Funktion auf der Grundlage des aktuellen Benutzersystems implementieren: Benutzern erlauben, ihre bereits erstellten Beiträge zu bearbeiten.


