Python FastAPI 教程:数据库集成(SQLAlchemy)+ 接口权限校验
前面我们掌握了参数传递与响应校验,这一节将聚焦生产级 API 核心需求 ------ 数据库持久化(基于 SQLAlchemy ORM)和接口权限控制(基于 OAuth2 + JWT),让 API 具备数据存储、身份认证和权限隔离能力。
一、前置依赖安装
需额外安装数据库相关和权限相关依赖:
css
pip install sqlalchemy pymysql python-jose[cryptography] passlib[bcrypt] python-multipart
- 依赖说明:
-
- sqlalchemy:Python 主流 ORM 框架,实现数据库操作抽象。
-
- pymysql:MySQL 数据库驱动(若用 PostgreSQL 可替换为 psycopg2-binary)。
-
- python-jose:生成和验证 JWT 令牌。
-
- passlib:密码哈希处理(避免明文存储)。
二、数据库集成(SQLAlchemy)
以 MySQL 为例,实现数据库连接、模型定义、CRUD 操作封装。
2.1 项目结构调整
为便于维护,调整项目结构如下:
bash
fastwebprojects/
├── main.py # 主程序入口(路由、APP 初始化)
├── database.py # 数据库连接配置
├── models.py # SQLAlchemy 数据模型(对应数据库表)
├── schemas.py # Pydantic 模型(请求/响应校验)
└── crud.py # 数据库 CRUD 操作封装
2.2 数据库连接配置(database.py)
ini
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# MySQL 连接地址:mysql+pymysql://用户名:密码@主机:端口/数据库名
SQLALCHEMY_DATABASE_URL = "mysql+pymysql://root:123456@localhost:3306/fastapi_db"
# 创建数据库引擎
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
pool_pre_ping=True # 连接前校验,避免失效连接
)
# 创建会话本地类(每次请求对应一个会话,独立隔离)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# 基础模型类(所有数据模型继承此类)
Base = declarative_base()
2.3 定义数据模型(models.py)
数据模型对应数据库表,SQLAlchemy 会自动生成表结构(需手动执行迁移)。
ini
from sqlalchemy import Column, Integer, String, Boolean
from database import Base
# 用户表模型
class User(Base):
__tablename__ = "users" # 数据库表名
id = Column(Integer, primary_key=True, index=True) # 主键 ID,索引
username = Column(String(50), unique=True, index=True, nullable=False) # 用户名,唯一
email = Column(String(100), unique=True, index=True, nullable=False) # 邮箱,唯一
hashed_password = Column(String(100), nullable=False) # 哈希后的密码
is_active = Column(Boolean, default=True) # 是否激活
is_admin = Column(Boolean, default=False) # 是否为管理员(用于权限控制)
2.4 初始化数据库表
在 main.py 中添加表创建逻辑,启动时自动创建不存在的表:
python
from fastapi import FastAPI
from database import engine, SessionLocal
import models
# 创建所有数据模型对应的数据库表
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
# 依赖函数:获取数据库会话(每个请求独立会话)
def get_db():
db = SessionLocal()
try:
yield db # 提供会话给路由函数
finally:
db.close() # 请求结束后关闭会话
2.5 封装 CRUD 操作(crud.py)
分离数据库操作与路由逻辑,降低耦合:
python
from sqlalchemy.orm import Session
import models
import schemas
from passlib.context import CryptContext
# 密码哈希上下文(使用 bcrypt 算法)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# 密码哈希处理
def get_password_hash(password: str):
return pwd_context.hash(password)
# 验证密码
def verify_password(plain_password: str, hashed_password: str):
return pwd_context.verify(plain_password, hashed_password)
# 创建用户
def create_user(db: Session, user: schemas.UserCreate):
hashed_password = get_password_hash(user.password)
db_user = models.User(
username=user.username,
email=user.email,
hashed_password=hashed_password
)
db.add(db_user) # 添加到会话
db.commit() # 提交事务
db.refresh(db_user) # 刷新数据(获取自动生成的 ID 等)
return db_user
# 根据用户名查询用户
def get_user_by_username(db: Session, username: str):
return db.query(models.User).filter(models.User.username == username).first()
2.6 定义 Pydantic 模型(schemas.py)
区分请求体(创建用户时传入密码)和响应体(返回时隐藏密码):
python
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
# 创建用户的请求体模型(需传入密码)
class UserCreate(BaseModel):
username: str = Field(..., min_length=3, max_length=50, description="用户名")
email: EmailStr = Field(..., description="邮箱") # EmailStr 自动校验邮箱格式
password: str = Field(..., min_length=6, description="密码,至少 6 位")
# 返回用户的响应体模型(隐藏密码)
class UserResponse(BaseModel):
id: int
username: str
email: EmailStr
is_active: bool
is_admin: bool
class Config:
orm_mode = True # 支持从 SQLAlchemy 模型直接转换
三、接口权限校验(OAuth2 + JWT)
实现「用户名密码登录获取令牌」→「携带令牌访问受保护接口」的完整流程。
3.1 配置 JWT 密钥与过期时间
在 main.py 中添加配置(实际项目建议用环境变量存储密钥):
python
from datetime import datetime, timedelta
from jose import JWTError, jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
# JWT 配置
SECRET_KEY = "your-secret-key-32bytes-long-keep-it-safe" # 密钥(生产环境需随机生成)
ALGORITHM = "HS256" # 加密算法
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # 令牌过期时间(30 分钟)
# OAuth2 依赖:从请求头的 Authorization 字段获取令牌(格式:Bearer <token>)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
3.2 实现令牌生成与验证逻辑
在 main.py 中添加核心函数:
ini
# 生成访问令牌
def create_access_token(data: dict):
to_encode = data.copy()
# 设置过期时间
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
# 生成 JWT 令牌
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# 依赖函数:验证令牌并返回当前用户
def get_current_user(
db: Session = Depends(get_db),
token: str = Depends(oauth2_scheme)
):
# 令牌验证失败异常
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="令牌无效或已过期",
headers={"WWW-Authenticate": "Bearer"},
)
try:
# 解码令牌
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub") # 令牌中存储的用户名(sub 为标准字段)
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
# 查询用户
user = crud.get_user_by_username(db, username=username)
if user is None:
raise credentials_exception
return user
# 依赖函数:验证当前用户是否为管理员(权限细化)
def get_current_admin_user(current_user: models.User = Depends(get_current_user)):
if not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权限访问,仅管理员可操作"
)
return current_user
3.3 实现登录接口(获取令牌)
ini
@app.post("/token", response_model=dict)
async def login_for_access_token(
db: Session = Depends(get_db),
form_data: OAuth2PasswordRequestForm = Depends() # 接收表单格式的用户名密码
):
# 验证用户名和密码
user = crud.get_user_by_username(db, username=form_data.username)
if not user or not crud.verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
# 生成令牌(sub 字段存储用户名)
access_token = create_access_token(data={"sub": user.username})
return {"access_token": access_token, "token_type": "bearer"}
3.4 实现受保护接口
- 普通接口:需登录(携带令牌)即可访问。
- 管理员接口:需登录且为管理员身份才能访问。
python
# 普通受保护接口:获取当前登录用户信息
@app.get("/users/me/", response_model=schemas.UserResponse)
async def read_users_me(current_user: models.User = Depends(get_current_user)):
return current_user
# 管理员接口:创建用户(仅管理员可操作)
@app.post("/users/", response_model=schemas.UserResponse)
async def create_new_user(
user: schemas.UserCreate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_admin_user) # 管理员权限依赖
):
# 检查用户名是否已存在
db_user = crud.get_user_by_username(db, username=user.username)
if db_user:
raise HTTPException(status_code=400, detail="用户名已存在")
return crud.create_user(db=db, user=user)
四、测试流程与验证
1. 初始化数据库
- 确保 MySQL 已启动,创建 fastapi_db 数据库(CREATE DATABASE fastapi_db;)。
- 启动 FastAPI 服务(uvicorn main:app --reload),自动创建 users 表。
2. 测试接口(通过 /docs 页面)
- 第一步:访问 http://127.0.0.1:8000/docs,找到 /token 接口,点击「Try it out」。
- 第二步:输入用户名(如 admin)、密码(如 123456),先调用 /users/ 接口(需管理员身份,可手动在数据库将 is_admin 设为 1)创建管理员用户。
- 第三步:用管理员用户名密码调用 /token 接口,获取 access_token。
- 第四步:调用 /users/me/ 接口,系统会自动携带令牌,返回当前用户信息。
- 第五步:非管理员用户调用 /users/ 接口,会返回 403 权限错误。
五、生产级优化建议
- 密钥与数据库配置:用 python-dotenv 读取环境变量,避免硬编码。
- 数据库迁移:使用 alembic 管理表结构变更(避免直接删除重建表)。
- 令牌安全:延长过期时间时,可结合刷新令牌机制。
- 日志与监控:添加接口访问日志,监控令牌过期、数据库异常等情况。