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}
)
四、功能测试与验证
- 启动项目:执行 uvicorn main:app --reload,此时项目根目录会自动生成 fastapi_jinja.db SQLite 数据库文件(首次启动时)。
- 访问注册页面:打开浏览器访问 http://127.0.0.1:8000/register,测试以下场景:
-
- 输入不符合长度的用户名(如 “ab” 或 “abcdefghijkl”),点击提交,后端会返回 “用户名长度需在 3-10 个字符之间” 的错误提示。
-
- 输入不一致的密码(如 “123456” 和 “1234567”),前端会实时显示 “两次密码输入不一致”,且阻止表单提交。
-
- 输入已存在的用户名 / 邮箱(如先注册 “testuser”,再次用该用户名注册),后端会返回重复提示。
-
- 输入符合要求的信息(如用户名 “testuser”、邮箱 “test@example.com”、密码 “123456”),注册成功后会自动跳转到登录页,并显示 “注册成功!请登录”。
- 测试登录功能:在登录页输入刚注册的用户名和密码:
-
- 若用户名不存在(如 “nonexist”),返回 “用户名不存在” 错误。
-
- 若密码错误(如 “1234567”),返回 “密码错误,请重新输入” 错误。
-
- 登录成功后,会跳转到用户列表页,页面会显示包括刚注册用户在内的所有数据库中的用户数据。
- 测试用户列表与详情:
-
- 在用户列表页,能看到每个用户的 ID、用户名、VIP 状态,点击 “查看详情” 可进入该用户的详情页。
-
- 在详情页,默认不显示邮箱,点击 “点击显示邮箱” 后,URL 会附加 ?show_email=true 参数,页面会渲染出用户邮箱。
-
- 若访问不存在的用户详情(如 http://127.0.0.1:8000/user/999),页面会显示 “用户不存在” 提示。
五、项目优化:密码加密与会话管理
前文登录注册功能中,密码以明文存储在数据库,存在严重安全隐患,此处补充密码加密方案;同时新增会话管理,实现登录状态保持(替代 “登录后直接跳转” 的简化逻辑)。
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 # 样式文件