当前位置: 首页 > news >正文

FastAPI+Sqlite+HTML的登录注册与文件上传系统:完整实现指南

作为一名开发者,你是否曾想过亲手搭建一个包含用户注册、登录认证和文件上传功能的完整 Web 系统?今天,我将带大家一步步拆解一个基于FastAPI(后端)和原生 JavaScript(前端)的前后端分离项目,从核心功能实现到关键技术点解析,让你快速掌握前后端协作的精髓。

最后附超详细带解析的源码哦!

一、项目整体介绍:我们要做什么?

这个项目是一个极简但完整的 Web 应用,核心功能包括:

  • 用户注册:支持用户名和密码注册,包含前端表单验证和后端数据校验
  • 用户登录:基于 JWT(JSON Web Token)的身份认证,登录后返回令牌
  • 权限控制:仅登录用户可访问文件上传功能
  • 文件上传:支持二进制文件上传,保存到服务器本地

整个系统采用前后端分离架构:

  • 前端:HTML+CSS + 原生 JavaScript,用 Axios 发送 HTTP 请求
  • 后端:FastAPI 框架,处理业务逻辑、数据库交互和身份验证
  • 数据库:SQLite(轻量免配置,适合演示)
  • 通信方式:JSON 格式数据交互,文件上传采用 multipart/form-data 格式

二、技术栈解析:为什么选这些工具?

在开始实现前,先了解下项目使用的核心技术栈及其优势:

技术作用核心优势
FastAPI后端框架高性能、自动生成 API 文档、类型提示友好、支持异步
原生 JavaScript前端逻辑零依赖、兼容性好、适合理解 HTTP 请求本质
Axios前端 HTTP 库支持 Promise、拦截器、请求 / 响应转换,处理异步请求更优雅
SQLAlchemyORM 工具简化数据库操作,支持多种数据库,避免手写 SQL
JWT身份认证无状态、适合分布式系统、减少数据库查询
bcrypt密码加密单向哈希、抗暴力破解,比 MD5 等加密更安全

三、先看效果再看代码 

1、注册页面:

要注意的是,我们前端设置了密码校验,要求账号的密码的最少长度都是6个长度,并且注册成功自动跳转登录界面。

注册后数据库的密码存储使用哈希加密,避免了明文存储,增加了用户安全性。 

 

2、登录界面

所有页面都有错误提示框和成功的提示框,登录成功自动跳转主页上传文件。

 

3、文件上传界面

如果没有登录成功,由于该项目加入了jwt校验,直接访问url会跳转到登录界面,极大的保护了API的安全性,阻止没有权限的人上传文件。

上传失败:

上传成功: 

 成功文件的存放:

 

四、前端实现:用户交互与请求处理

前端部分主要包含 3 个页面:注册页(register.html)、登录页(login.html)和首页(welcome.html)。我们重点解析核心逻辑:

1. 表单验证:用户输入第一道防线

无论是注册还是登录,前端表单验证都能减少无效请求,提升用户体验。以注册页为例:

// 注册表单提交逻辑
document.querySelector('.register-form').onsubmit = function(e) {e.preventDefault(); // 阻止表单默认提交// 获取用户输入const username = document.querySelector('#username').value.trim();const password = document.querySelector('#password').value.trim();const confirmPassword = document.querySelector('#password_isok').value.trim();// 前端校验if (username.length < 6) {showError('用户名至少6个字符');return;}if (password.length < 6) {showError('密码至少6个字符');return;}if (password !== confirmPassword) {showError('两次密码不一致');return;}// 校验通过,发送请求...
};

为什么要做前端校验?

  • 即时反馈用户输入错误,无需等待后端响应
  • 减少无效的后端请求,降低服务器压力
  • 提升用户体验,明确告知错误原因

2. Axios 请求:前后端数据桥梁

前端通过 Axios 与后端通信,核心是处理请求参数、请求头和响应结果。以登录请求为例:

// 登录请求
axios({url: 'http://127.0.0.1:8080/api/login',method: 'post',data: {username: username,password: password}
}).then(response => {if (response.data.code === 200) {// 登录成功,保存token到localStoragelocalStorage.setItem('token', response.data.data.access_token);// 跳转到首页setTimeout(() => window.location.href = 'welcome.html', 1000);}
}).catch(error => {// 处理错误(如用户名密码错误)showError(error.response.data.message);
});

这里的关键设计:

  • localStorage存储 JWT 令牌,持久化保存(关闭浏览器不丢失)
  • 统一响应格式(code+message+data),便于前端统一处理
  • setTimeout实现登录成功后的延迟跳转,给用户提示时间

3. 权限控制:保护敏感页面

首页(文件上传页)需要验证用户是否登录,否则强制跳转登录页:

// 页面加载时验证登录状态
window.addEventListener("DOMContentLoaded", function() {const token = localStorage.getItem('token');if (!token || token.trim() === "") {// 未登录,提示并跳转showError("您尚未登录,正在跳转至登录页...");setTimeout(() => window.location.href = 'login.html', 1500);}
});

权限控制的核心思路:

  • 前端:通过检查localStorage中的 token 判断登录状态(简单验证)
  • 后端:每次请求验证 token 有效性(安全验证,防止前端篡改)

4. 文件上传:二进制数据处理

文件上传是前端的一个特殊场景,需要用FormData构造请求体:

// 文件上传处理
const formData = new FormData();
formData.append("file", file); // 添加文件对象// 发送带token的上传请求
axios.post('http://127.0.0.1:8080/api/upload_binary', formData, {headers: {'Content-Type': 'multipart/form-data', // 文件上传专用格式'Authorization': `Bearer ${localStorage.getItem('token')}` // 携带token}
}).then(response => {showSuccess(`文件 ${file.name} 上传成功`);
});

文件上传的关键点:

  • Content-Type必须设为multipart/form-data,告诉服务器这是文件上传请求
  • 通过Authorization头携带 JWT 令牌,后端验证用户权限
  • FormData对象包装文件数据,无需手动处理二进制格式

五、后端实现:业务逻辑与安全校验

后端基于 FastAPI 实现,核心功能包括用户管理、JWT 认证和文件上传。我们逐一解析:

1. 项目初始化:配置与依赖

首先需要初始化 FastAPI 应用,配置数据库和跨域支持:

# 导入核心库
from fastapi import FastAPI, HTTPException, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base
import jwt
from passlib.context import CryptContext# 初始化FastAPI应用
app = FastAPI()# 配置CORS(跨域资源共享)
app.add_middleware(CORSMiddleware,allow_origins=["*"],  # 允许所有源(生产环境需指定具体域名)allow_methods=["*"],  # 允许所有HTTP方法allow_headers=["*"]   # 允许所有请求头
)# 配置数据库(SQLite)
DATABASE_URL = "sqlite:///users.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
Session = sessionmaker(bind=engine)
Base = declarative_base()

为什么需要 CORS?
前后端分离时,前端页面和后端 API 通常不在同一域名下,浏览器会限制跨域请求。通过配置 CORS,后端明确允许前端域名的请求,解决 "跨域错误"。

2. 数据模型:数据库与请求响应格式

用 SQLAlchemy 定义用户表结构,用 Pydantic 定义请求 / 响应格式:

# 数据库模型(用户表)
class User(Base):__tablename__ = "users"id = Column(Integer, primary_key=True, index=True)username = Column(String(255), unique=True, index=True, nullable=False)password = Column(String(255), nullable=False)  # 存储哈希后的密码# 响应模型(统一格式)
class ResponseModel(BaseModel):code: int  # 状态码:200成功,400客户端错误,500服务器错误message: str  # 提示信息data: Optional[dict] = None  # 可选数据

 统一响应格式的好处:
前端可以用同一套逻辑解析所有接口响应,无需为每个接口单独处理格式,例如:

// 前端统一处理响应
if (response.data.code === 200) {// 成功逻辑
} else {// 错误提示showError(response.data.message);
}

3. 用户注册:数据校验与密码安全

注册接口需要实现两个核心功能:用户名唯一性校验和密码加密存储:

@app.post("/api/register", response_model=ResponseModel)
async def register(user: UserRegister):db = Session()try:# 检查用户名是否已存在existing_user = db.query(User).filter(User.username == user.username).first()if existing_user:return ResponseModel(code=400, message="用户名已存在")# 密码加密(关键!绝不能明文存储)hashed_password = pwd_context.hash(user.password)new_user = User(username=user.username, password=hashed_password)# 保存到数据库db.add(new_user)db.commit()return ResponseModel(code=200, message="注册成功")finally:db.close()

密码安全的关键:

  • 使用passlib库的bcrypt算法哈希密码(单向加密,无法解密)
  • 哈希过程会自动添加随机盐值,相同密码哈希结果不同,防止彩虹表攻击

4. JWT 认证:无状态登录验证

JWT(JSON Web Token)是实现无状态认证的核心,登录成功后生成 token,后续请求携带 token 即可验证身份:

# 生成JWT令牌
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):to_encode = data.copy()# 设置过期时间(默认30分钟)expire = datetime.utcnow() + (expires_delta or timedelta(minutes=30))to_encode.update({"exp": expire})  # 添加过期时间字段# 生成token(密钥+算法)return jwt.encode(to_encode, SECURITY_KET, algorithm=ALGORITHMS)# 登录接口
@app.post("/api/login", response_model=ResponseModel)
async def login(user: UserLogin):db = Session()try:# 查找用户db_user = db.query(User).filter(User.username == user.username).first()if not db_user:return ResponseModel(code=400, message="用户名或密码错误")# 验证密码(哈希比对)if not pwd_context.verify(user.password, db_user.password):return ResponseModel(code=400, message="用户名或密码错误")# 生成tokenaccess_token = create_access_token(data={"sub": user.username})return ResponseModel(code=200,message="登录成功",data={"access_token": access_token, "token_type": "bearer"})finally:db.close()

JWT 的优势:

  • 无状态:服务器不需要存储用户登录状态,减轻服务器压力
  • 跨域支持:适合分布式系统,多个服务可共用同一套认证机制
  • 携带信息:token 中可包含用户基本信息(如用户名),减少数据库查询

5. 文件上传:权限验证与文件存储

文件上传接口需要先验证用户 token,再处理文件存储:

@app.post("/api/upload_binary", response_model=ResponseModel)
async def upload_binary_file(file: UploadFile = File(...),  # 接收文件token: str = Header(None, alias="Authorization")  # 接收token
):try:# 1. 验证token(简化版,实际项目建议用依赖注入)if not token or not token.startswith("Bearer "):return ResponseModel(code=401, message="未授权,请先登录")token = token.split(" ")[1]try:# 解析token,验证有效性payload = jwt.decode(token, SECURITY_KET, algorithms=[ALGORITHMS])except:return ResponseModel(code=401, message="token无效或已过期")# 2. 保存文件upload_dir = "uploads_binary"if not os.path.exists(upload_dir):os.makedirs(upload_dir)  # 创建目录file_path = os.path.join(upload_dir, file.filename)with open(file_path, "wb") as buffer:buffer.write(await file.read())  # 写入文件return ResponseModel(code=200, message=f"文件 {file.filename} 上传成功")except Exception as e:return ResponseModel(code=500, message="文件上传失败")

文件上传的注意事项:

  • 目录权限:确保服务器对uploads_binary目录有写入权限
  • 文件大小限制:实际项目中需限制文件大小,防止恶意上传大文件
  • 文件名处理:可能需要重命名文件(如添加时间戳),避免同名文件覆盖

六、项目源码和运行

有了这些直接无脑运行,再无后顾之忧。

1、项目结构

注意:uploads_binary不需己创建,数据库不需要自己创建,系统运行自己创建。

2、项目源码

①login.html

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>用户登录</title><style>body {font-family: Arial, sans-serif;max-width: 400px;margin: 0 auto;padding: 20px;}.form-container {margin-bottom: 20px;padding: 20px;border: 1px solid #ddd;border-radius: 5px;}h1 {text-align: center;margin-top: 0;}input {display: block;width: 100%;padding: 8px;margin-bottom: 10px;border: 1px solid #ddd;border-radius: 4px;box-sizing: border-box;}button {background-color: #28cccf;color: white;padding: 10px 15px;border: none;border-radius: 4px;cursor: pointer;width: 100%;}button:hover {background-color: #1afaff;}.alert {font-size: 20px;text-align: center;margin-top: 20px;border: 1px solid #ddd;border-radius: 4px;display: none;padding: 10px;}.alert.success {background-color: #d4edda;color: #155724;border-color: #c3e6cb;}.alert.error {background-color: #f8d7da;color: #721c24;border-color: #f5c6cb;}.register-link {text-align: center;margin-top: 15px;}.register-link a {color: #28cccf;text-decoration: none;}.register-link a:hover {text-decoration: underline;}</style>
</head>
<body><div class="form-container"><h1>用户登录</h1><!-- 登录表单 --><form id="loginForm"><input type="text" name="username" placeholder="用户名" required /><input type="password" name="password" placeholder="密码" required /><button type="submit">登录</button></form><!-- 提示信息 --><div class="alert success" id="successAlert" style="display: none;"></div><div class="alert error" id="errorAlert" style="display: none;"></div><!-- 注册链接 --><div class="register-link">没有账号?<a href="register.html">去注册</a></div></div><!-- 引入 axios --><script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.6.2/axios.min.js"></script>
<script>document.getElementById('loginForm').addEventListener('submit', function (e) {e.preventDefault();const form = e.target;const username = form.username.value.trim();const password = form.password.value.trim();const successDiv = document.getElementById('successAlert');const errorDiv = document.getElementById('errorAlert');// 清空上次提示successDiv.style.display = 'none';errorDiv.style.display = 'none';successDiv.textContent = '';errorDiv.textContent = '';// 发送登录请求axios({url:'http://127.0.0.1:8080/api/login',method: "post",data:{username: username,password: password}}).then(response => {if (response.data.code === 200) {successDiv.style.display = 'block';successDiv.textContent = response.data.message;// 如果返回了 token,将其保存到 localStorage 中if (response.data.data && response.data.data.access_token) {// 将登录成功后服务器返回的 token 保存到浏览器的本地存储中,以便后续请求时使用localStorage.setItem('token', response.data.data.access_token);  //localStorage.setItem(key, value) 是浏览器提供的一个用于持久化存储数据的方法}// 跳转页面setTimeout(() => {window.location.href = 'welcome.html';}, 1000);} else {errorDiv.style.display = 'block';errorDiv.textContent = response.data.message;}}).catch(error => {errorDiv.style.display = 'block';errorDiv.textContent = "登录失败:" +(error.response?.data?.message || error.message);});});
</script></body>
</html>

②register.html 

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>用户注册</title><style>body {font-family: Arial, sans-serif;max-width: 400px;margin: 0 auto;padding: 20px;}.form-container {margin-bottom: 20px;padding: 20px;border: 1px solid #ddd;border-radius: 5px;}h1 {text-align: center;margin-top: 0;}input {display: block;width: 100%;padding: 8px;margin-bottom: 10px;border: 1px solid #ddd;border-radius: 4px;box-sizing: border-box;}button {background-color: #28cccf;color: white;padding: 10px 15px;border: none;border-radius: 4px;cursor: pointer;width: 100%;}button:hover {background-color: #1afaff;}.login-link {text-align: center;margin-top: 15px;}.login-link a {color: #1afaff;text-decoration: none;}.login-link a:hover {text-decoration: underline;}.alert {font-size: 20px;text-align: center;margin-top: 20px;border: 1px solid #ddd;border-radius: 4px;display: none;padding: 10px;}.alert.success {background-color: #d4edda;color: #155724;border-color: #c3e6cb;}.alert.error {background-color: #f8d7da;color: #721c24;border-color: #f5c6cb;}</style>
</head>
<body><div class="form-container"><h1>用户注册</h1><form class="register-form"><input type="text" name="username" placeholder="用户名" id="username" required><input type="password" name="password" placeholder="密码" id="password" required><input type="password" name="password" placeholder="确认密码" id="password_isok" required><button type="submit" class="btn-register" id="subtn">注册</button></form><div class="login-link">已有账号?<a href="login.html">去登录</a></div><!-- 提示框容器 --><div class="alert success" id="successAlert" style="display: none;"></div><div class="alert error" id="errorAlert" style="display: none;"></div></div><script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.6.2/axios.min.js"></script><script>document.querySelector('.register-form').onsubmit = function (e) {e.preventDefault();const username = document.querySelector('#username').value.trim();const password = document.querySelector('#password').value.trim();const confirmPassword = document.querySelector('#password_isok').value.trim();const successDiv = document.getElementById('successAlert');const errorDiv = document.getElementById('errorAlert');// 清空上次提示并隐藏successDiv.style.display = 'none';errorDiv.style.display = 'none';successDiv.textContent = '';errorDiv.textContent = '';// 前端校验if (username.length < 6) {errorDiv.style.display = 'block';errorDiv.textContent = '用户名至少6个字符';return;}if (password.length < 6) {errorDiv.style.display = 'block';errorDiv.textContent = '密码至少6个字符';return;}if (password !== confirmPassword) {errorDiv.style.display = 'block';errorDiv.textContent = '两次密码不一致';return;}// 发送请求axios({url: 'http://127.0.0.1:8080/api/register',method: 'post',data: {username: username,password: password}}).then(result => {if (result.data.code === 200) {successDiv.style.display = 'block';successDiv.textContent = result.data.message;setTimeout(function () {window.location.href = 'login.html';}, 1000)// 注册成功后清空表单document.querySelector('#username').value = "";document.querySelector('#password').value = "";document.querySelector('#password_isok').value = "";} else {errorDiv.style.display = 'block';errorDiv.textContent = result.data.message;}}).catch(error => {errorDiv.style.display = 'block';errorDiv.textContent = "注册失败:" +(error.response?.data?.detail || error.response?.data?.message || error.message);});};</script>
</body>
</html>

③welcome.html 

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>首页</title><style>body {font-family: Arial, sans-serif;max-width: 400px;margin: 0 auto;padding: 20px;}.form-container {margin-bottom: 20px;padding: 20px;border: 1px solid #ddd;border-radius: 5px;}p {text-align: center;font-size: 20px;font-weight: bold;}h1, h2 {text-align: center;margin-top: 0;}input[type="file"] {display: block;width: 100%;padding: 8px;margin-bottom: 10px;border: 1px solid #ddd;border-radius: 4px;box-sizing: border-box;}button {background-color: #28cccf;color: white;padding: 10px 15px;border: none;border-radius: 4px;cursor: pointer;width: 100%;}button:hover {background-color: #1afaff;}.alert {font-size: 20px;text-align: center;margin-top: 20px;border: 1px solid #ddd;border-radius: 4px;display: none;padding: 10px;}.alert.success {background-color: #d4edda;color: #155724;border-color: #c3e6cb;}.alert.error {background-color: #f8d7da;color: #721c24;border-color: #f5c6cb;}</style>
</head>
<body><h1>欢迎回来!</h1><p>您已成功登录。</p><!-- 文件上传表单 --><form id="uploadForm" class="form-container" style="margin-top: 40px;"><h2>上传文件</h2><input type="file" id="fileInput" name="file" required /><button type="submit">上传</button></form><!-- 提示信息 --><div class="alert success" id="successAlert" style="display: none;"></div><div class="alert error" id="errorAlert" style="display: none;"></div><!-- 引入 axios --><script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.6.2/axios.min.js"></script><script>// 页面加载时检查是否有 token,没有则跳转到登录页面并提示window.addEventListener("DOMContentLoaded", function () {const token = localStorage.getItem('token');const warningDiv = document.getElementById('errorAlert');if (!token || token.trim() === "") {warningDiv.style.display = 'block';warningDiv.innerText = "您尚未登录,正在跳转至登录页...";setTimeout(() => {window.location.href = 'login.html';}, 1500);} else {// token 存在,继续加载页面内容warningDiv.style.display = 'none';}});// 文件上传处理document.getElementById('uploadForm').addEventListener('submit', async function (e) {e.preventDefault();const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];const successDiv = document.getElementById('successAlert');const errorDiv = document.getElementById('errorAlert');// 清空上次提示successDiv.style.display = 'none';errorDiv.style.display = 'none';successDiv.textContent = '';errorDiv.textContent = '';if (!file) {errorDiv.style.display = 'block';errorDiv.textContent = '请选择一个文件';return;}// 创建一个空的 FormData 对象,用于构建 HTTP 请求中需要发送的数据体。const formData = new FormData();// 将用户选择的文件(变量 file)附加到 FormData 对象中,字段名为 "file"。这与后端接收文件的键名保持一致。formData.append("file", file);await axios.post('http://127.0.0.1:8080/api/upload_binary', formData, {headers: {// 显式声明请求内容类型为 multipart/form-data,这是上传文件的标准格式。'Content-Type': 'multipart/form-data',  // 支持将文本、二进制文件和其他类型的数据// 这行代码用于从浏览器的 localStorage 中获取名为 'token' 的 用户身份凭证(Token),// 并将其作为 Bearer Token 添加到 HTTP 请求头中,以完成对后端接口的身份认证。'Authorization': `Bearer ${localStorage.getItem('token')}`}}).then(response =>{if (response.data.code === 200) {successDiv.style.display = 'block';successDiv.textContent = response.data.message;fileInput.value = ''; // 清空文件选择框} else {errorDiv.style.display = 'block';errorDiv.textContent = response.data.message;}}).catch (error=>{errorDiv.style.display = 'block';errorDiv.textContent = "上传失败:" +(error.response?.data?.message || error.message);});});</script>
</body>
</html>

 ④Register_API.py

# 导入 FastAPI 框架核心模块,用于创建 Web API 应用
from fastapi import FastAPI, HTTPException
# 用于处理跨域请求(CORS),允许前端访问后端接口(解决跨域问题)
from fastapi.middleware.cors import CORSMiddleware
from jose.constants import ALGORITHMS
# pydantic 的 BaseModel 用于定义请求体的数据模型(数据校验)
# Field 用于为模型字段添加额外信息或约束
# constr 是一个字符串类型约束工具,例如可以限制字符串长度、正则匹配等
from pydantic import BaseModel, Field, constr
import sqlite3
# Optional 用于标注某个字段可以为 None,常用于定义可选字段的数据模型
from typing import Optional
# 用于创建数据库引擎,常用于同步数据库连接
from sqlalchemy import create_engine, Column, Integer, String
# 用于创建数据库会话,用于执行数据库操作
from sqlalchemy.orm import sessionmaker, declarative_base
# 用于处理文件读写
import os
from datetime import datetime, timedelta
from typing import Optional
import jwt  # 用于生成和解析 JWT token
from passlib.context import CryptContext   # 哈希加密
# UploadFile:表示一个上传的文件对象,包含文件名、类型、内容等信息
# File:是一个类,用于作为参数的默认值,配合 UploadFile 使用,表示该参数必须是一个上传的文件
from fastapi import UploadFile, FileSECURITY_KET = "asdfghjklzxcvbnm"  # 密钥
ALGORITHMS = "HS256"  #加密的算法
ACCESS_TOKEN_EXPIRE_MINUTES = 30  # token有效期为30分钟pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")# 创建访问令牌
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):"""创建访问令牌:param data: 要编码的数据(通常是用户信息):param expires_delta: 过期时间"""to_encode = data.copy()# 设置过期时间# 设置过期时间if expires_delta:expire = datetime.utcnow() + expires_deltaelse:expire = datetime.utcnow() + timedelta(minutes=15)# 添加过期时间字段to_encode.update({"exp": expire})# 使用 jwt 库生成 token    (  加密内容,    加密秘钥,         加密算法     )encoded_jwt = jwt.encode(to_encode, SECURITY_KET, algorithm=ALGORITHMS)return encoded_jwt# 创建 FastAPI 实例对象,这是整个应用的核心
app = FastAPI()# 添加CORS中间件,允许跨域传输
app.add_middleware(CORSMiddleware,allow_origins=["*"],  # 允许所有源allow_credentials=True,  # 是否允许发送 Cookieallow_methods=["*"],  # 允许所有HTTP方法allow_headers=["*"],  # 允许所有HTTP头部
) # 定义数据库连接URL
DATABASE_URL = "sqlite:///users.db"# 创建基类
Base = declarative_base()# 创建数据库引擎,设置连接参数以允许在多线程环境中使用(地址)
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})# 创建会话,绑定数据库引擎
Session = sessionmaker(bind=engine, autocommit=False, autoflush=False, expire_on_commit=False)# 创建数据库表结构(可以创建数据库表结构)
class User(Base):__tablename__ = "users"id = Column(Integer, primary_key=True, index=True)username = Column(String(255), unique=True, index=True, nullable=False)password = Column(String(255), nullable=False)class Token(BaseModel):"""用于响应 token 的数据模型"""access_token: strtoken_type: str# 执行创建数据库表结构
Base.metadata.create_all(bind=engine)# 定义注册接口的请求数据模型
class UserRegister(BaseModel):# 用户名字段:# - 至少 3 个字符长# - 只能包含英文字母、数字和中文字符username: str = Field(min_length=6, pattern='^[a-zA-Z0-9\u4e00-\u9fa5]+$')# 密码字段:# - 至少 6 个字符长password: constr(min_length=6)# 定义统一的响应数据模型,便于前端解析处理结果
class ResponseModel(BaseModel):code: int  # 状态码(200 表示成功,400 表示客户端错误,500 表示服务器错误)message: str  # 描述信息(如“注册成功”、“用户名已存在”)data: Optional[dict] = None  # 可选返回数据,默认为 None# 定义登录请求的数据模型
class UserLogin(BaseModel):username: strpassword: str# 定义文件上传请求数据模型
class UploadRequest(BaseModel):filename: strcontent: str# 登录接口
@app.post("/api/login", response_model=ResponseModel)
async def login(user: UserLogin):db = Session()try:db_user = db.query(User).filter(User.username == user.username).first()if not db_user:return ResponseModel(code=400, message="用户名或密码错误")# 验证密码是否匹配if not pwd_context.verify(user.password, db_user.password):return ResponseModel(code=400, message="用户名或密码错误")access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)access_token = create_access_token(data={"sub": user.username},expires_delta=access_token_expires)return ResponseModel(code=200,message="登录成功",data={"access_token": access_token, "token_type": "bearer"})except Exception as e:print("服务器错误详情:", str(e))return ResponseModel(code=500, message="服务器错误")finally:db.close()# 定义上传文件的接口
@app.post("/api/upload_binary", response_model=ResponseModel)
async def upload_binary_file(#File(...) 表示该参数是一个文件类型的参数,并且是必填项(... 是 Python 的 Ellipsis,表示必填)# UploadFile 是 FastAPI 提供的一个类,用于表示上传的文件。file: UploadFile = File(...),# Optional[str] 表示这个参数可以不传,默认为 Nonefilename: Optional[str] = None
):try:# 创建存储文件的目录upload_dir = "uploads_binary"if not os.path.exists(upload_dir):os.makedirs(upload_dir)# 使用自定义文件名或原始文件名save_filename = filename if filename else file.filenamefile_path = os.path.join(upload_dir, save_filename)# 写入文件(异步方式)with open(file_path, "wb") as buffer:buffer.write(await file.read())return ResponseModel(code=200, message=f"文件 {save_filename} 上传成功")except Exception as e:print("文件上传失败:", str(e))return ResponseModel(code=500, message="文件上传失败")# 注册接口
@app.post("/api/register", response_model=ResponseModel)  # response_model=ResponseModel:表示这个接口返回的数据结构必须符合 ResponseModel 的格式
async def register(user: UserRegister):  # user: UserRegister表示这个函数接收一个参数 user,它的数据结构由 UserRegister 定义try:db = Session()# 查询用户名是否已存在existing_user = db.query(User).filter(User.username == user.username).first()if existing_user:# 如果用户名已存在,抛出 HTTP 异常,提示“用户名已存在”,前端执行 catch 块,显示错误信息raise HTTPException(status_code=400, detail="用户名已存在")# 使用哈希加密存储密码hase_password = pwd_context.hash(user.password)new_user = User(username=user.username, password=hase_password)# 将新用户插入到数据库中db.add(new_user)db.commit()db.refresh(new_user)return ResponseModel(code=200, message="注册成功")except HTTPException as e:# 如果用户名已存在,抛出 HTTP 异常,前端执行 catch 块,显示错误信息return ResponseModel(code=e.status_code, message=e.detail)except Exception as e:# 如果发生异常,回滚事务,并返回错误信息print("服务器错误详情:", str(e))db.rollback()return ResponseModel(code=500, message="服务器错误")finally:db.close()if __name__ == "__main__":import uvicornuvicorn.run(app, host="127.0.0.1", port=8080)

3、项目运行

①安装后端依赖:

pip install fastapi uvicorn sqlalchemy python-jose passlib[bcrypt]

 ②先运行后端再运行前端

运行后端:

运行前端:

七、总结:前后端分离开发的核心思路

通过这个项目,我们可以总结出前后端分离开发的关键原则:

  • 职责清晰:前端负责用户交互和数据展示,后端负责业务逻辑和数据存储
  • 接口先行:前后端约定好接口文档(FastAPI 自动生成),并行开发
  • 数据安全:敏感数据(如密码)必须在后端处理,前端只做展示和基础验证
  • 状态管理:前端负责维护客户端状态(如登录状态),后端通过 token 验证身份

如果你是前端开发者,这个项目能帮你理解后端的认证逻辑;如果你是后端开发者,能让你更清晰前端的请求处理方式。关注我,后续会带来更多前后端实战项目解析!

你在开发中遇到过哪些前后端协作的坑?欢迎在评论区分享你的解决方案,有不懂的都可以来问小宁哦~

相关文章:

  • 命令模式 - Flutter中的操作封装大师,把“动作“变成可管理的对象!
  • 数据同步工具对比:Canal、DataX与Flink CDC
  • stm32hal模块驱动(2)bmi270气压计
  • 数据结构之单链表
  • 爬虫实战之图片及人物信息爬取
  • 华为云Flexus+DeepSeek征文 | 华为云 ModelArts Studio 赋能 AI 法务:合同审查与法律文件生成系统
  • 【硬核数学】4. AI的“寻路”艺术:优化理论如何找到模型的最优解《从零构建机器学习、深度学习到LLM的数学认知》
  • Leetcode 3598. Longest Common Prefix Between Adjacent Strings After Removals
  • 滑块验证码(1)
  • 【blender】使用bpy对一个obj的不同mesh进行不同的材质贴图(涉及对bmesh的操作)
  • ViTMatte:利用预训练的基础视觉Transformer提升图像抠图性能
  • 云计算在布莱克-斯科尔斯模型中的应用:解析解、蒙特卡洛模拟与可视化-AI云计算数值分析和代码验证
  • Node.js特训专栏-实战进阶:11. Redis缓存策略与应用场景
  • 【更新至2024年】1999-2024年各省城镇居民人均消费支出数据(无缺失)
  • 八股文——JAVA基础:String s1 = new String(“abc“);这句话创建了几个字符串对象?
  • window11 本地安装 MySQL8.0
  • SAP顾问职位汇总(第26周)
  • 数据分析标普500
  • 实现win系统控制局域网的linux主机桌面
  • 现代 JavaScript (ES6+) 入门到实战(三):字符串与对象的魔法升级—模板字符串/结构赋值/展开运算符