
Python全棧實戰 Day 3用戶注冊與JWT登錄認證前言這是“10天Python全棧項目實戰”系列的第3篇。前兩天已經完成FastAPI與Vue3項目搭建第一個前后端接口調用MySQL數據庫連接SQLAlchemy用戶模型創建。今天將在現有項目中實現完整的后端用戶認證流程用戶注冊密碼哈希用戶登錄生成JWT Token根據Token獲取當前用戶。注意數據庫中不能直接保存用戶明文密碼只保存經過密碼哈希算法處理后的結果。一、安裝認證依賴進入后端項目目錄并激活虛擬環境cd backend .venv\Scripts\activate安裝依賴pip install pyjwt pwdlib[argon2] python-dotenv email-validator各依賴的作用依賴作用PyJWT生成和解析JWTpwdlib密碼哈希與校驗argon2實際使用的密碼哈希算法python-dotenv讀取環境變量email-validator校驗郵箱格式FastAPI當前官方JWT教程使用PyJWT處理Token并使用pwdlib處理密碼。PasswordHash.recommended()目前默認選擇Argon2。更新依賴文件pip freeze requirements.txt二、調整項目結構本文按照以下后端結構編寫backend ├── app │ ├── api │ │ ├── __init__.py │ │ └── auth.py │ ├── core │ │ ├── __init__.py │ │ └── security.py │ ├── models │ │ ├── __init__.py │ │ └── user.py │ ├── schemas │ │ ├── __init__.py │ │ └── user.py │ ├── __init__.py │ ├── database.py │ └── main.py ├── .env ├── .gitignore └── requirements.txtdatabase.py和models/user.py應該已經在Day 2中創建。三、檢查用戶數據模型打開app/models/user.py確認用戶表至少包含以下字段from datetime import datetime from sqlalchemy import Boolean, DateTime, String, func from sqlalchemy.orm import Mapped, mapped_column from app.database import Base class User(Base): __tablename__ users id: Mapped[int] mapped_column( primary_keyTrue, indexTrue, ) username: Mapped[str] mapped_column( String(30), uniqueTrue, indexTrue, nullableFalse, ) email: Mapped[str] mapped_column( String(100), uniqueTrue, indexTrue, nullableFalse, ) hashed_password: Mapped[str] mapped_column( String(255), nullableFalse, ) is_active: Mapped[bool] mapped_column( Boolean, defaultTrue, nullableFalse, ) created_at: Mapped[datetime] mapped_column( DateTime, server_defaultfunc.now(), nullableFalse, )這里保存的是hashed_password而不是password如果Day 2創建的表沒有hashed_password字段需要修改數據模型和數據庫表。開發階段沒有重要數據時可以刪除舊表后重新創建已有數據時應使用數據庫遷移或執行對應的ALTER TABLE語句不能直接刪表。四、創建請求和響應模型創建app/schemas/user.py寫入from pydantic import BaseModel, ConfigDict, EmailStr, Field class UserRegister(BaseModel): username: str Field( min_length3, max_length30, patternr^[a-zA-Z0-9_]$, ) email: EmailStr password: str Field( min_length8, max_length128, ) class UserLogin(BaseModel): username: str password: str class TokenResponse(BaseModel): access_token: str token_type: str bearer class UserResponse(BaseModel): id: int username: str email: EmailStr is_active: bool model_config ConfigDict(from_attributesTrue)這幾個模型分別負責UserRegister校驗注冊參數UserLogin校驗登錄參數TokenResponse定義登錄成功的返回內容UserResponse定義返回給前端的用戶信息。不要把hashed_password放進UserResponse否則可能將密碼哈希結果返回給前端。五、配置JWT密鑰在項目根目錄創建.env先生成隨機密鑰python -c import secrets; print(secrets.token_hex(32))終端會輸出一段隨機字符串將它復制進.envSECRET_KEY替換成剛才生成的隨機字符串 ACCESS_TOKEN_EXPIRE_MINUTES30創建或檢查.gitignore.venv/ __pycache__/ *.pyc .env.env中包含JWT簽名密鑰不應該提交到公開代碼倉庫。六、實現密碼加密和JWT生成創建app/core/security.py寫入import os from datetime import datetime, timedelta, timezone import jwt from dotenv import load_dotenv from pwdlib import PasswordHash load_dotenv() SECRET_KEY os.getenv(SECRET_KEY) ALGORITHM HS256 ACCESS_TOKEN_EXPIRE_MINUTES int( os.getenv(ACCESS_TOKEN_EXPIRE_MINUTES, 30) ) if not SECRET_KEY: raise RuntimeError(沒有配置SECRET_KEY請檢查.env文件) password_hash PasswordHash.recommended() def hash_password(password: str) - str: 將明文密碼轉換為密碼哈希。 return password_hash.hash(password) def verify_password( plain_password: str, hashed_password: str, ) - bool: 驗證明文密碼是否與密碼哈希匹配。 return password_hash.verify( plain_password, hashed_password, ) def create_access_token( subject: str, expires_delta: timedelta | None None, ) - str: 生成JWT訪問令牌。 expire datetime.now(timezone.utc) ( expires_delta if expires_delta is not None else timedelta(minutesACCESS_TOKEN_EXPIRE_MINUTES) ) payload { sub: subject, exp: expire, } return jwt.encode( payload, SECRET_KEY, algorithmALGORITHM, ) def decode_access_token(token: str) - dict: 校驗并解析JWT訪問令牌。 return jwt.decode( token, SECRET_KEY, algorithms[ALGORITHM], )JWT中保存{ sub: 用戶ID, exp: 過期時間, }其中sub表示Token對應的主體exp表示Token過期時間。PyJWT在執行jwt.decode()時會校驗expToken過期后會拋出相應異常解析時還應顯式限制允許使用的簽名算法。七、實現注冊接口創建app/api/auth.py先寫入注冊功能import jwt from fastapi import ( APIRouter, Depends, HTTPException, status, ) from fastapi.security import ( HTTPAuthorizationCredentials, HTTPBearer, ) from sqlalchemy import select from sqlalchemy.orm import Session from app.core.security import ( create_access_token, decode_access_token, hash_password, verify_password, ) from app.database import get_db from app.models.user import User from app.schemas.user import ( TokenResponse, UserLogin, UserRegister, UserResponse, ) router APIRouter( prefix/api/auth, tags[用戶認證], ) bearer_scheme HTTPBearer(auto_errorFalse) router.post( /register, response_modelUserResponse, status_codestatus.HTTP_201_CREATED, ) def register( data: UserRegister, db: Session Depends(get_db), ) - User: username_exists db.scalar( select(User).where( User.username data.username ) ) if username_exists: raise HTTPException( status_codestatus.HTTP_409_CONFLICT, detail用戶名已經存在, ) email_exists db.scalar( select(User).where( User.email data.email ) ) if email_exists: raise HTTPException( status_codestatus.HTTP_409_CONFLICT, detail郵箱已經被注冊, ) user User( usernamedata.username, emaildata.email, hashed_passwordhash_password(data.password), ) try: db.add(user) db.commit() db.refresh(user) except Exception: db.rollback() raise HTTPException( status_codestatus.HTTP_500_INTERNAL_SERVER_ERROR, detail用戶注冊失敗, ) return user注冊接口的主要流程接收注冊參數 ↓ 檢查用戶名是否存在 ↓ 檢查郵箱是否存在 ↓ 對密碼執行哈希 ↓ 寫入MySQL ↓ 返回用戶基本信息八、實現登錄接口繼續在auth.py中加入router.post( /login, response_modelTokenResponse, ) def login( data: UserLogin, db: Session Depends(get_db), ) - TokenResponse: user db.scalar( select(User).where( User.username data.username ) ) if user is None: raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail用戶名或密碼錯誤, ) if not verify_password( data.password, user.hashed_password, ): raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail用戶名或密碼錯誤, ) if not user.is_active: raise HTTPException( status_codestatus.HTTP_403_FORBIDDEN, detail當前賬號已被禁用, ) access_token create_access_token( subjectstr(user.id) ) return TokenResponse( access_tokenaccess_token, )無論用戶名不存在還是密碼錯誤都統一返回用戶名或密碼錯誤這樣可以減少接口向外部暴露賬號是否存在的信息。登錄成功后返回{ access_token: 生成的JWT字符串, token_type: bearer }九、獲取當前登錄用戶繼續加入Token驗證依賴def get_current_user( credentials: HTTPAuthorizationCredentials | None Depends( bearer_scheme ), db: Session Depends(get_db), ) - User: if credentials is None: raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail請先登錄, headers{WWW-Authenticate: Bearer}, ) try: payload decode_access_token( credentials.credentials ) user_id payload.get(sub) if user_id is None: raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail無效的登錄憑證, ) user_id_int int(user_id) except ( jwt.InvalidTokenError, ValueError, TypeError, ): raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail登錄憑證無效或已經過期, headers{WWW-Authenticate: Bearer}, ) user db.get(User, user_id_int) if user is None: raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail用戶不存在, ) if not user.is_active: raise HTTPException( status_codestatus.HTTP_403_FORBIDDEN, detail當前賬號已被禁用, ) return user然后創建受保護接口router.get( /me, response_modelUserResponse, ) def get_me( current_user: User Depends(get_current_user), ) - User: return current_user訪問/api/auth/me時必須攜帶Authorization: Bearer JWT_TOKEN否則接口會返回401。十、注冊認證路由打開app/main.py加入認證路由from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.api.auth import router as auth_router app FastAPI( title10天Python全棧項目實戰, version1.0.0, ) app.add_middleware( CORSMiddleware, allow_origins[ http://localhost:5173, http://127.0.0.1:5173, ], allow_credentialsTrue, allow_methods[*], allow_headers[*], ) app.include_router(auth_router) app.get(/) def read_root() - dict[str, str]: return { message: FastAPI服務運行成功, }啟動項目uvicorn app.main:app --reload打開接口文檔http://127.0.0.1:8000/docs十一、測試注冊接口找到POST /api/auth/register測試參數{ username: owen, email: owenexample.com, password: Python123456 }成功后返回{ id: 1, username: owen, email: owenexample.com, is_active: true }建議在這里放兩張截圖Swagger注冊成功MySQL用戶表新增數據。查看數據庫時hashed_password應該是一段密碼哈希而不是Python123456十二、測試登錄接口找到POST /api/auth/login提交{ username: owen, password: Python123456 }成功后獲得{ access_token: 一段JWT字符串, token_type: bearer }復制access_token備用。十三、測試登錄用戶接口找到GET /api/auth/me點擊Swagger頁面右上角的“Authorize”輸入Bearer 你的JWT_TOKEN部分Swagger頁面只需要填寫Token本身不需要手動寫Bearer以頁面提示為準。認證成功后執行GET /api/auth/me應該返回{ id: 1, username: owen, email: owenexample.com, is_active: true }再測試以下情況不攜帶Token隨便修改Token等Token過期后再次請求。這些情況都應該返回401而不是正常獲取用戶信息。十四、常見報錯1. 提示找不到jwt執行pip install pyjwt注意安裝包名稱是PyJWT代碼導入方式是import jwt2. 提示需要安裝email-validator執行pip install email-validator因為Pydantic的EmailStr依賴該組件驗證郵箱。3. 提示沒有配置SECRET_KEY檢查.env是否位于后端項目根目錄backend/.env并確認內容為SECRET_KEY真實生成的隨機密鑰修改.env后重新啟動FastAPI。4. 用戶表沒有hashed_password字段說明數據庫結構仍然是Day 2的舊版本。需要更新表結構或在沒有重要數據的開發環境中刪除舊表后重新創建。5. 正確密碼也登錄失敗重點檢查注冊時是否調用了hash_password()數據庫保存的是否為哈希結果登錄時是否使用verify_password()是否手動修改過數據庫中的密碼字段。6. Token剛生成就提示無效重點檢查生成和解析Token是否使用同一個SECRET_KEY使用的算法是否都是HS256是否重啟項目后更換了隨機密鑰.env是否正確加載。十五、Day 3驗收清單完成以下檢查說明Day 3已經通過注冊接口能夠創建用戶用戶名不能重復郵箱不能重復密碼少于8位時校驗失敗數據庫沒有保存明文密碼正確賬號密碼可以登錄錯誤密碼無法登錄登錄成功可以獲得JWT Token攜帶Token可以訪問/api/auth/me不攜帶Token時返回401偽造或過期Token無法通過認證十六、下一篇內容Day 4將完成Vue3登錄頁面、Axios請求封裝、Token保存和路由守衛。下一篇主要包括創建登錄頁面調用FastAPI登錄接口保存JWT TokenAxios自動攜帶Token未登錄用戶自動跳轉登錄頁登錄后獲取當前用戶信息。