Appearance
02_用户注册登录业务链实现指南
本章节完成 用户注册 (POST /auth/register) 与 登录获取 JWT (POST /auth/login) 的完整闭环:模型→加密→JWT→API→测试。
先决条件:脚手架已就绪,数据库表
users存在;依赖已包含python-jose。
0️⃣ 依赖补充
密码加密推荐 passlib:
bash
pip install passlib[bcrypt]==1.7.4 # 如果用 Docker, 修改 requirements.txt 后重建添加后执行:
bash
docker compose build backendrequirements.txt 需追加一行:
passlib[bcrypt]==1.7.41️⃣ 密码加密 & JWT 工具
文件:app/core/security.py
python
from datetime import datetime, timedelta
from typing import Any
from jose import jwt
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
ALGORITHM = "HS256"
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(raw: str, hashed: str) -> bool:
return pwd_context.verify(raw, hashed)
def create_access_token(data: dict[str, Any], expires_minutes: int | None = None) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=expires_minutes or settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)2️⃣ Pydantic Schemas
文件:app/schemas/auth.py
python
from pydantic import BaseModel, Field
class UserRegister(BaseModel):
username: str = Field(min_length=3, max_length=64)
password: str = Field(min_length=6, max_length=64)
class UserLogin(BaseModel):
username: str
password: str
class Token(BaseModel):
access_token: str
token_type: str = "bearer"3️⃣ Service 层
文件:app/services/auth_service.py
python
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import HTTPException, status
from app.core.security import hash_password, verify_password, create_access_token
from app.models.user import User
class AuthService:
def __init__(self, db: AsyncSession):
self.db = db
async def register(self, username: str, password: str):
# 检重
res = await self.db.execute(select(User).where(User.username == username))
if res.scalar_one_or_none():
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Username exists")
user = User(username=username, password_hash=hash_password(password))
self.db.add(user)
await self.db.commit()
await self.db.refresh(user)
return user
async def login(self, username: str, password: str) -> str:
res = await self.db.execute(select(User).where(User.username == username))
user = res.scalar_one_or_none()
if not user or not verify_password(password, user.password_hash):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
token = create_access_token({"sub": str(user.id)})
return token4️⃣ API 路由
文件:app/api/auth.py
python
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.schemas.auth import UserRegister, UserLogin, Token
from app.services.auth_service import AuthService
from app.db.session import get_db
router = APIRouter()
@router.post("/register", status_code=201, response_model=Token)
async def register(payload: UserRegister, db: AsyncSession = Depends(get_db)):
user = await AuthService(db).register(payload.username, payload.password)
token = await AuthService(db).login(payload.username, payload.password)
return {"access_token": token}
@router.post("/login", response_model=Token)
async def login(payload: UserLogin, db: AsyncSession = Depends(get_db)):
token = await AuthService(db).login(payload.username, payload.password)
return {"access_token": token}主入口 app/main.py 中已包含:
python
app.include_router(auth.router, prefix="/auth", tags=["Auth"])如未添加请补充。
5️⃣ 测试用例
文件:tests/test_auth.py
python
import pytest
from httpx import AsyncClient
from app.main import app
from app.db.session import AsyncSessionLocal, engine
from app.models.base import Base
@pytest.fixture(scope="module", autouse=True)
async def prepare_db():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
@pytest.mark.asyncio
async def test_register_and_login():
async with AsyncClient(app=app, base_url="http://test") as ac:
# 注册
r = await ac.post("/auth/register", json={"username": "u1", "password": "pass123"})
assert r.status_code == 201
token = r.json()["access_token"]
assert token
# 登录
r2 = await ac.post("/auth/login", json={"username": "u1", "password": "pass123"})
assert r2.status_code == 200
assert r2.json()["access_token"] == token or r2.json()["access_token"]执行:
bash
docker compose exec backend pytest -q应看到 1 passed。
6️⃣ Swagger 调试
- 浏览器打开
/docs - 先调用 POST /auth/register 创建账号
- 再调用 POST /auth/login 获取
access_token - 后续接口可在 Authorize 里填写
Bearer <token>进行认证调用
✅ 注册登录业务链完成!
接下去可:
- 使用依赖注入编写
get_current_user,给需要鉴权的接口加Depends。 - 在模型中添加
role字段实现多角色权限。 - 给刷新 Token/忘记密码等场景扩展新的接口。
注册 API
POST /auth/register
请求示例:
json
{
"username": "student01",
"password": "123456",
"role": "student", // student | teacher
"school_id": 1,
"class_id": 2 // 学生必填,教师可省略
}成功返回:
json
{
"access_token": "...",
"token_type": "bearer"
}登录 API
POST /auth/login
json
{
"username": "student01",
"password": "123456"
}元数据接口
- GET
/meta/schools→[ {"id":1,"name":"测试学校"}, ... ] - GET
/meta/classes?school_id=1→[ {"id":2,"name":"五年五班"}, ... ]