Python FastAPI + Jinja 扩展功能:表单提交与数据库连接

3 阅读13分钟

Python FastAPI + Jinja 扩展功能:表单提交与数据库连接

基于原有项目,新增用户注册表单(含前端验证 + 后端校验)和SQLite 数据库连接(替代模拟数据),实现数据持久化存储,以下是完整扩展代码及配置。

一、扩展前的依赖准备

新增表单处理和数据库操作所需依赖,执行命令安装:

# 表单处理:python-multipart;数据库:sqlalchemy(ORM框架)
pip install python-multipart sqlalchemy

二、数据库连接配置(ORM 模式)

使用 SQLAlchemy 构建 ORM 模型,实现与 SQLite 数据库的交互,无需手动编写 SQL 语句。

1. 新增数据库配置文件:database.py

在项目根目录创建 database.py,定义数据库连接和会话:

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# SQLite 数据库文件路径(项目根目录下的 fastapi_jinja.db)
SQLALCHEMY_DATABASE_URL = "sqlite:///./fastapi_jinja.db"
# 创建数据库引擎(SQLite 需添加 check_same_thread=False,避免线程问题)
engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
# 创建数据库会话工厂(每次请求创建一个会话,请求结束关闭)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# 声明基类(所有 ORM 模型需继承此类)
Base = declarative_base()

2. 定义用户 ORM 模型:models.py

在项目根目录创建 models.py,映射数据库表结构:

from sqlalchemy import Column, Integer, String, Boolean
from database import Base
from datetime import datetime
class DBUser(Base):
    """用户数据库模型(对应 users 表)"""
    __tablename__ = "users"  # 数据库表名
    # 字段定义
    id = Column(Integer, primary_key=True, index=True)  # 主键,自增
    username = Column(String(50), unique=True, index=True, nullable=False)  # 用户名(唯一)
    email = Column(String(100), unique=True, index=True, nullable=False)  # 邮箱(唯一)
    password = Column(String(100), nullable=False)  # 密码(实际项目需加密,此处简化)
    is_vip = Column(Boolean, default=False)  # VIP 状态(默认普通用户)
    create_time = Column(String(20), default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))  # 创建时间

3. 创建数据库表

main.py 中添加表创建逻辑,启动项目时自动生成数据库文件和表:

# 在 main.py 顶部导入数据库相关模块
from database import engine, SessionLocal
from models import Base, DBUser
from sqlalchemy.orm import Session
from fastapi import Depends, Form, HTTPException  # 新增 Form(处理表单)、Depends(依赖注入)、HTTPException(异常处理)
# 创建数据库表(若表不存在则创建)
Base.metadata.create_all(bind=engine)
# 依赖函数:获取数据库会话(每次请求自动获取,结束后关闭)
def get_db():
    db = SessionLocal()
    try:
        yield db  # 提供会话给请求使用
    finally:
        db.close()  # 请求结束后关闭会话

三、用户注册表单功能(前端 + 后端)

实现用户注册页面,包含前端表单渲染、数据验证和后端表单接收、数据库存储。

1. 新增注册模板:templates/auth/register.html

在 templates 目录下创建 auth 子目录,新增注册模板:

{% extends "base.html" %}
{% block title %}用户注册{% endblock %}
{% block content %}
<div class="register-container">
    <h2>用户注册</h2>
    <!-- 显示错误信息(若有) -->
    {% if error %}
        <div class="error-message">{{ error }}</div>
    {% endif %}
    <!-- 注册表单:提交到 /register 路由,方法为 POST -->
    <form action="/register" method="post" class="register-form">
        <div class="form-group">
            <label for="username">用户名(3-10个字符):</label>
            <input type="text" id="username" name="username" required 
                   minlength="3" maxlength="10" placeholder="请输入用户名">
        </div>
        <div class="form-group">
            <label for="email">邮箱:</label>
            <input type="email" id="email" name="email" required 
                   placeholder="请输入邮箱(如:user@example.com)">
        </div>
        <div class="form-group">
            <label for="password">密码(6-16个字符):</label>
            <input type="password" id="password" name="password" required 
                   minlength="6" maxlength="16" placeholder="请输入密码">
        </div>
        <div class="form-group">
            <label for="confirm_password">确认密码:</label>
            <input type="password" id="confirm_password" name="confirm_password" required 
                   minlength="6" maxlength="16" placeholder="请再次输入密码">
            <!-- 前端密码一致性校验提示 -->
            <span id="password_tip" class="tip"></span>
        </div>
        <div class="form-group">
            <button type="submit" class="submit-btn">注册</button>
        </div>
        <div class="login-link">
            已有账号?<a href="/login">前往登录</a>
        </div>
    </form>
</div>
<!-- 前端表单验证:密码一致性校验 -->
<script>
    // 获取密码和确认密码输入框
    const password = document.getElementById("password");
    const confirmPassword = document.getElementById("confirm_password");
    const tip = document.getElementById("password_tip");
    // 监听输入事件,实时校验
    confirmPassword.addEventListener("input", function() {
        if (password.value !== confirmPassword.value) {
            tip.textContent = "两次密码输入不一致!";
            tip.style.color = "#e74c3c";
        } else {
            tip.textContent = "两次密码输入一致!";
            tip.style.color = "#2ecc71";
        }
    });
    // 表单提交前再次校验(防止绕过前端验证)
    const form = document.querySelector(".register-form");
    form.addEventListener("submit", function(e) {
        if (password.value !== confirmPassword.value) {
            alert("两次密码输入不一致,请重新输入!");
            e.preventDefault(); // 阻止表单提交
        }
    });
</script>
{% endblock %}

2. 新增登录模板(可选,用于跳转):templates/auth/login.html

{% extends "base.html" %}
{% block title %}用户登录{% endblock %}
{% block content %}
<div class="login-container">
    <h2>用户登录</h2>
    {% if error %}
        <div class="error-message">{{ error }}</div>
    {% endif %}
    <form action="/login" method="post" class="login-form">
        <div class="form-group">
            <label for="username">用户名:</label>
            <input type="text" id="username" name="username" required placeholder="请输入用户名">
        </div>
        <div class="form-group">
            <label for="password">密码:</label>
            <input type="password" id="password" name="password" required placeholder="请输入密码">
        </div>
        <div class="form-group">
            <button type="submit" class="submit-btn">登录</button>
        </div>
        <div class="register-link">
            没有账号?<a href="/register">前往注册</a>
        </div>
    </form>
</div>
{% endblock %}

3. 补充注册 / 登录相关 CSS 样式:static/css/styles.css

在原有 styles.css 末尾添加以下样式:

/* 注册/登录页面样式 */
.register-container, .login-container {
    background-color: white;
    padding: 2.5rem;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    max-width: 600px;
    margin: 2rem auto;
}
.register-container h2, .login-container h2 {
    color: #2c3e50;
    margin-bottom: 1.5rem;
    text-align: center;
    border-bottom: 1px solid #eee;
    padding-bottom: 0.5rem;
}
.form-group {
    margin-bottom: 1.2rem;
}
.form-group label {
    display: block;
    margin-bottom: 0.5rem;
    color: #2c3e50;
    font-weight: 500;
}
.form-group input {
    width: 100%;
    padding: 0.8rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 1rem;
}
.form-group input:focus {
    outline: none;
    border-color: #3498db;
    box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
.tip {
    display: block;
    margin-top: 0.3rem;
    font-size: 0.9rem;
}
.error-message {
    background-color: rgba(231, 76, 60, 0.1);
    color: #e74c3c;
    padding: 1rem;
    border-radius: 4px;
    margin-bottom: 1.5rem;
    text-align: center;
}
.submit-btn {
    width: 100%;
    padding: 0.8rem;
    background-color: #3498db;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 1.1rem;
    cursor: pointer;
    transition: background-color 0.3s;
}
.submit-btn:hover {
    background-color: #2980b9;
}
.login-link, .register-link {
    text-align: center;
    margin-top: 1.5rem;
    font-size: 1rem;
}
.login-link a, .register-link a {
    color: #3498db;
    text-decoration: none;
}
.login-link a:hover, .register-link a:hover {
    text-decoration: underline;
}

4. 实现注册 / 登录路由:main.py 补充代码

main.py 中添加表单处理路由,包含数据校验、数据库存储和异常处理:

# 1. 注册页面路由(GET:显示表单)
@app.get("/register", response_class=HTMLResponse)
async def show_register(request: Request):
    return templates.TemplateResponse(
        "auth/register.html",
        {"request": request}  # 初始无错误信息
    )
# 2. 注册表单提交路由(POST:处理数据)
@app.post("/register", response_class=HTMLResponse)
async def handle_register(
    request: Request,
    username: str = Form(...),  # Form(...) 表示接收表单字段,必填
    email: str = Form(...),
    password: str = Form(...),
    confirm_password: str = Form(...),
    db: Session = Depends(get_db)  # 依赖注入获取数据库会话
):
    # 步骤1:后端二次校验(防止绕过前端验证)
    # 校验密码一致性
    if password != confirm_password:
        return templates.TemplateResponse(
            "auth/register.html",
            {"request": request, "error": "两次密码输入不一致,请重新输入!"}
        )
    # 校验用户名长度
    if len(username) < 3 or len(username) > 10:
        return templates.TemplateResponse(
            "auth/register.html",
            {"request": request, "error": "用户名长度需在 3-10 个字符之间!"}
        )
    # 校验密码长度
    if len(password) < 6 or len(password) > 16:
        return templates.TemplateResponse(
            "auth/register.html",
            {"request": request, "error": "密码长度需在 6-16 个字符之间!"}
        )
    # 步骤2:校验用户名/邮箱是否已存在
    existing_user = db.query(DBUser).filter(
        (DBUser.username == username) | (DBUser.email == email)  # 用户名或邮箱重复
    ).first()
    if existing_user:
        error_msg = "用户名已存在!" if existing_user.username == username else "邮箱已被注册!"
        return templates.TemplateResponse(
            "auth/register.html",
            {"request": request, "error": error_msg}
        )
    # 步骤3:数据存入数据库(实际项目需加密密码,此处简化)
    new_user = DBUser(
        username=username,
        email=email,
        password=password  # 注意:生产环境需用 bcrypt 等工具加密,如 passlib.hash.bcrypt.hash(password)
    )
    db.add(new_user)  # 添加到会话
    db.commit()  # 提交事务(写入数据库)
    db.refresh(new_user)  # 刷新会话,获取最新数据(如自增 ID)
    # 步骤4:注册成功,跳转到登录页(带成功提示)
    return templates.TemplateResponse(
        "auth/login.html",
        {"request": request, "success": "注册成功!请登录"}
    )
# 3. 登录页面路由(GET:显示表单)
@app.get("/login", response_class=HTMLResponse)
async def show_login(request: Request, success: str = None):
    return templates.TemplateResponse(
        "auth/login.html",
        {"request": request, "success": success}  # 接收注册成功的提示
    )
# 4. 登录表单提交路由(POST:处理登录,简化版)
@app.post("/login", response_class=HTMLResponse)
async def handle_login(
    request: Request,
    username: str = Form(...),
    password: str = Form(...),
    db: Session = Depends(get_db)
):
    # 步骤1:查询用户是否存在
    user = db.query(DBUser).filter(DBUser.username == username).first()
    if not user:
        return templates.TemplateResponse(
            "auth/login.html",
            {"request": request, "error": "用户名不存在!"}
        )
    # 步骤2:校验密码(实际项目需用加密后的密码比对,如 passlib.hash.bcrypt.verify(password, user.password))
    if user.password != password:
        return templates.TemplateResponse(
            "auth/login.html",
            {"request": request, "error": "密码错误,请重新输入!"}
        )
    # 步骤3:登录成功,跳转到用户列表页(实际项目需添加会话管理,如用 FastAPI-Users 或 Session)
    # 此处简化:直接获取数据库中所有用户,渲染列表页
    all_users = db.query(DBUser).all()
    return templates.TemplateResponse(
        "users/list.html",
        {"request": request, "users": all_users, "login_user": username}  # 传递当前登录用户名
    )
# 5. 优化原有用户列表路由:从数据库获取数据(替代模拟数据)
@app.get("/users", response_class=HTMLResponse)
async def user_list(request: Request, db: Session = Depends(get_db)):
    all_users = db.query(DBUser).all()  # 从数据库查询所有用户
    return templates.TemplateResponse(
        "users/list.html",
        {"request": request, "users": all_users}
    )
# 6. 优化原有用户详情路由:从数据库获取数据
@app.get("/user/{user_id}", response_class=HTMLResponse)
async def user_detail(
    request: Request,
    user_id: int,
    show_email: bool = False,
    db: Session = Depends(get_db)
):
    # 从数据库查询指定 ID 的用户
    user = db.query(DBUser).filter(DBUser.id == user_id).first()
    return templates.TemplateResponse(
        "users/detail.html",
        {"request": request, "user": user, "show_email": show_email}
    )

四、功能测试与验证

  1. 启动项目:执行 uvicorn main:app --reload,此时项目根目录会自动生成 fastapi_jinja.db SQLite 数据库文件(首次启动时)。
  1. 访问注册页面:打开浏览器访问 http://127.0.0.1:8000/register,测试以下场景:
    • 输入不符合长度的用户名(如 “ab” 或 “abcdefghijkl”),点击提交,后端会返回 “用户名长度需在 3-10 个字符之间” 的错误提示。
    • 输入不一致的密码(如 “123456” 和 “1234567”),前端会实时显示 “两次密码输入不一致”,且阻止表单提交。
    • 输入已存在的用户名 / 邮箱(如先注册 “testuser”,再次用该用户名注册),后端会返回重复提示。
    • 输入符合要求的信息(如用户名 “testuser”、邮箱 “test@example.com”、密码 “123456”),注册成功后会自动跳转到登录页,并显示 “注册成功!请登录”。
  1. 测试登录功能:在登录页输入刚注册的用户名和密码:
    • 若用户名不存在(如 “nonexist”),返回 “用户名不存在” 错误。
    • 若密码错误(如 “1234567”),返回 “密码错误,请重新输入” 错误。
    • 登录成功后,会跳转到用户列表页,页面会显示包括刚注册用户在内的所有数据库中的用户数据。
  1. 测试用户列表与详情
    • 在用户列表页,能看到每个用户的 ID、用户名、VIP 状态,点击 “查看详情” 可进入该用户的详情页。
    • 在详情页,默认不显示邮箱,点击 “点击显示邮箱” 后,URL 会附加 ?show_email=true 参数,页面会渲染出用户邮箱。

五、项目优化:密码加密与会话管理

前文登录注册功能中,密码以明文存储在数据库,存在严重安全隐患,此处补充密码加密方案;同时新增会话管理,实现登录状态保持(替代 “登录后直接跳转” 的简化逻辑)。

1. 密码加密:使用 passlib

(1)安装依赖
pip install passlib[bcrypt]  # bcrypt 是安全的密码哈希算法
(2)新增密码工具类:utils.py

在项目根目录创建 utils.py,封装密码加密与验证函数:

from passlib.context import CryptContext
# 初始化密码上下文(指定使用 bcrypt 算法)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
    """加密密码:返回哈希后的密码字符串"""
    return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
    """验证密码:对比明文密码与哈希密码,返回是否匹配"""
    return pwd_context.verify(plain_password, hashed_password)
(3)修改注册与登录逻辑(main.py
# 在 main.py 顶部导入密码工具函数
from utils import hash_password, verify_password
# 1. 修改注册路由:存储加密后的密码
@app.post("/register", response_class=HTMLResponse)
async def handle_register(
    request: Request,
    username: str = Form(...),
    email: str = Form(...),
    password: str = Form(...),
    confirm_password: str = Form(...),
    db: Session = Depends(get_db)
):
    # (原有校验逻辑不变,此处省略)
    
    # 步骤3:加密密码后存入数据库(替换原明文存储逻辑)
    hashed_pwd = hash_password(password)  # 新增:加密密码
    new_user = DBUser(
        username=username,
        email=email,
        password=hashed_pwd  # 存储哈希密码,而非明文
    )
    db.add(new_user)
    db.commit()
    db.refresh(new_user)
    
    # (后续跳转逻辑不变)
# 2. 修改登录路由:验证加密密码
@app.post("/login", response_class=HTMLResponse)
async def handle_login(
    request: Request,
    username: str = Form(...),
    password: str = Form(...),
    db: Session = Depends(get_db)
):
    # 步骤1:查询用户是否存在(原有逻辑不变)
    user = db.query(DBUser).filter(DBUser.username == username).first()
    if not user:
        return templates.TemplateResponse(
            "auth/login.html",
            {"request": request, "error": "用户名不存在!"}
        )
    # 步骤2:验证加密密码(替换原明文对比逻辑)
    if not verify_password(password, user.password):
        return templates.TemplateResponse(
            "auth/login.html",
            {"request": request, "error": "密码错误,请重新输入!"}
        )
    
    # (后续跳转逻辑不变)

2. 会话管理:使用 Starlette Sessions

(1)安装依赖
pip install python-multipart sqlalchemy starlette-sessions
(2)配置会话(main.py
# 在 main.py 顶部导入会话相关模块
from starlette.middleware.sessions import SessionMiddleware
from secrets import token_hex
# 初始化 FastAPI 应用(原有代码不变)
app = FastAPI(title="Jinja 模板演示")
# 新增:添加会话中间件(secret_key 需保密,生产环境建议从环境变量获取)
app.add_middleware(
    SessionMiddleware,
    secret_key=token_hex(32),  # 生成32位随机密钥(每次启动会变化,生产环境需固定)
    session_cookie="fastapi_jinja_session",  # 会话 Cookie 名称
    max_age=3600  # 会话有效期(秒),1小时后自动失效
)
# (原有数据库挂载、模板初始化等逻辑不变)
# 3. 修改登录路由:登录成功后设置会话
@app.post("/login", response_class=HTMLResponse)
async def handle_login(
    request: Request,  # Request 对象包含 session 属性
    username: str = Form(...),
    password: str = Form(...),
    db: Session = Depends(get_db)
):
    # (原有用户查询、密码验证逻辑不变)
    
    # 步骤3:登录成功,设置会话(存储当前登录用户名)
    request.session["username"] = username  # 将用户名存入会话
    
    # 跳转到用户列表页
    all_users = db.query(DBUser).all()
    return templates.TemplateResponse(
        "users/list.html",
        {"request": request, "users": all_users, "login_user": username}
    )
# 4. 新增登出路由:清除会话
@app.get("/logout", response_class=HTMLResponse)
async def logout(request: Request):
    # 清除会话中的用户名(登出)
    request.session.pop("username", None)
    # 跳转到登录页
    return templates.TemplateResponse(
        "auth/login.html",
        {"request": request, "success": "已成功登出!"}
    )
# 5. 优化模板:显示登录状态(修改 base.html)
{% extends "base.html" %}
{% block title %}默认标题{% endblock %}
{% block content %}
<header class="header">
    <div class="header-content">
        <h1>FastAPI + Jinja 演示平台</h1>
        <nav>
            <a href="/">首页</a>
            <a href="/users">用户列表</a>
            <!-- 新增:根据会话显示登录/登出按钮 -->
            {% if request.session.username %}
                <span>欢迎,{{ request.session.username }}!</span>
                <a href="/logout">登出</a>
            {% else %}
                <a href="/login">登录</a>
                <a href="/register">注册</a>
            {% endif %}
        </nav>
    </div>
</header>
<!-- (原有内容区域、底部代码不变) -->
{% endblock %}

六、生产环境配置

开发环境(--reload 模式)仅用于开发,生产环境需关闭自动重载、配置日志、使用更稳定的服务器(如 Gunicorn)。

1. 安装生产环境依赖

pip install gunicorn  # 生产级 WSGI 服务器

2. 新增生产环境启动脚本:start.sh

在项目根目录创建 start.sh(Linux/Mac),内容如下:

#!/bin/bash
# 生产环境启动脚本:使用 Gunicorn 作为服务器,绑定 0.0.0.0:80(默认端口)
# workers 数量建议设置为(CPU核心数 * 2 + 1),此处设为 4
gunicorn -w 4 -b 0.0.0.0:80 "main:app" --access-logfile ./logs/access.log --error-logfile ./logs/error.log
(1)创建日志目录
mkdir logs  # 存储访问日志和错误日志
(2)赋予脚本执行权限
chmod +x start.sh
(3)启动生产环境
./start.sh

3. Windows 生产环境启动脚本:start.bat

@echo off
:: Windows 生产环境启动脚本
gunicorn -w 4 -b 0.0.0.0:80 "main:app" --access-logfile ./logs/access.log --error-logfile ./logs/error.log

4. 关键生产环境配置说明

  • 密钥安全:会话中间件的 secret_key 不能硬编码在代码中,建议从环境变量获取(如使用 os.getenv("SECRET_KEY"))。
  • 数据库配置:若使用 MySQL/PostgreSQL 替代 SQLite(SQLite 不适合高并发),需修改 database.py 中的 SQLALCHEMY_DATABASE_URL:
# MySQL 示例(需安装依赖:pip install pymysql)
SQLALCHEMY_DATABASE_URL = "mysql+pymysql://user:password@host:port/db_name"
# PostgreSQL 示例(需安装依赖:pip install psycopg2-binary)
SQLALCHEMY_DATABASE_URL = "postgresql://user:password@host:port/db_name"
  • HTTPS 配置:生产环境需启用 HTTPS,可通过 Nginx 反向代理实现(前端用 Nginx 处理 HTTPS,后端 Gunicorn 处理业务逻辑)。

七、项目完整目录结构

fastapi-jinja-demo/
├── main.py                # 项目入口(路由、核心逻辑)
├── database.py            # 数据库配置(连接、会话)
├── models.py              # ORM 模型(用户表)
├── utils.py               # 工具函数(密码加密)
├── start.sh               # Linux/Mac 生产启动脚本
├── start.bat              # Windows 生产启动脚本
├── logs/                  # 日志目录
│   ├── access.log         # 访问日志
│   └── error.log          # 错误日志
├── templates/             # 模板目录
│   ├── base.html          # 父模板(含登录状态显示)
│   ├── index.html         # 首页
│   ├── auth/              # 登录注册模板
│   │   ├── login.html     # 登录页
│   │   └── register.html  # 注册页
│   └── users/             # 用户相关模板
│       ├── list.html      # 用户列表页
│       └── detail.html    # 用户详情页
└── static/                # 静态资源目录
    └── css/
        └── styles.css     # 样式文件