作为一名开发者,你是否曾想过亲手搭建一个包含用户注册、登录认证和文件上传功能的完整 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 验证身份

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/912900.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/912900.shtml
英文地址,请注明出处:http://en.pswp.cn/news/912900.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【动态规划】P11188 「KDOI-10」商店砍价|普及+

本文涉及知识点 C动态规划 P11188 「KDOI-10」商店砍价 题目背景 English Statement. You must submit your code at the Chinese version of the statement. 您可以点击 这里 下载本场比赛的选手文件。 You can click here to download all tasks and examples of the c…

国产LHR3040芯片是REF5040的代替品

LHR3040是一款噪声低、漂移低、精度高的电压基准产品系列。这些基准同时支持灌电流和拉电流&#xff0c;并且具有出色的线性和负载调节性能。采用专有的设计技术实现了出色的温漂(3ppm/℃)和高精度(0.05%)。这些特性与极低噪声相结合&#xff0c;使LHR30XX系列成为高精度数据采…

专题:2025AI营销市场发展研究报告|附400+份报告PDF汇总下载

原文链接&#xff1a;https://tecdat.cn/?p42800 在数字化浪潮席卷全球的当下&#xff0c;AI营销正成为驱动企业增长的核心动力。 从市场规模来看&#xff0c;AI营销正经历着爆发式增长&#xff0c;生成式AI的出现更是为其注入了强大活力。在应用层面&#xff0c;AI已渗透到营…

深入对比 Python 中的 `__repr__` 与 `__str__`:选择正确的对象表示方法

文章目录 核心概念对比1. 根本目的差异2. 调用场景对比深入解析:何时使用哪种方法场景 1:开发者调试 vs 用户展示场景 2:技术表示 vs 简化视图高级对比:特殊场景处理1. 容器中的对象表示2. 日志记录的最佳实践3. 异常信息展示最佳实践指南1. 何时实现哪个方法?2. 实现原则…

万能公式基分析重构补丁复分析和欧拉公式原理推导

基分析&#xff0c; x11 x2-1 x3i 存在加法法则 x1x20 所以x1-x2 存在链式基乘法法则 x1x1*x1x2*x2 x2x3*x3 x3x1*x3 -x1x2x3 将链式基乘法操作 二次&#xff0c;三次&#xff0c;直至n次化简得 一次 x1 -x1 x3 矩阵 x1 x1 x2 x2 x3 …

OpenCV 4.10.0 移植

OpenCV 4.10.0 移植使用 概述移植编译下载解压编译环境编译 编译完成OpenCV 库文件及其作用 使用实例参考代码 参考 概述 OpenCV&#xff08;Open Source Computer Vision Library&#xff09;是计算机视觉领域最广泛使用的开源库之一&#xff0c;提供了丰富的功能模块&#xf…

Tomcat10.0以上版本编译成功但报错HTTP状态 404

Tomcat正常启动且项目已成功部署&#xff0c;但出现404错误。 HTTP状态 404 - 未找到package org.example;import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpSer…

在Flask项目中用Git LFS管理大文件(PDF)的完整实践

在Flask项目中用Git LFS高效管理大文件(以农机说明书PDF为例) 背景与需求 在农机管理系统等实际项目中,经常需要上传和管理大量超大文件(如200MB以上的PDF说明书、图片等)。如果直接用Git管理这些大文件,不仅会导致仓库膨胀、clone/pull速度变慢,还可能遇到推送失败等…

朴素贝叶斯算法案例演示及Python实现

目录 一、基本原理二、案例演示2.1 未平滑处理2.2 Laplace平滑处理 三、Python实现 一、基本原理 朴素贝叶斯思想&#xff1a;依靠特征概率去预测分类&#xff0c;针对于代分类的样本&#xff0c;会求解在该样本出现的条件下&#xff0c;各个类别出现的概率&#xff0c;哪个类…

RAG从入门到高阶(二):Retrieve-and-Rerank

在上一篇教程中&#xff0c;我们了解了 Naive RAG 的基本原理和实现。它就像一个刚刚学会查找资料的新手&#xff0c;虽然能找到一些信息&#xff0c;但有时候找到的并不够精准&#xff0c;甚至会有一些无关的干扰。 今天&#xff0c;我们将介绍 Retrieve-and-Rerank RAG&…

【脚本】Linux磁盘目录挂载脚本(不分区)

以下是一个不带分区&#xff0c;直接挂载整个磁盘到指定目录的脚本。该脚本会检查磁盘是否已挂载&#xff0c;自动创建文件系统&#xff08;可选&#xff09;&#xff0c;并配置开机自动挂载&#xff1a; #!/bin/bash# 磁盘直接挂载脚本&#xff08;不分区&#xff09; # 使用…

壁纸网站分享

壁纸网站链接&#xff1a; 1.Microsoft Design - Wallpapers&#xff1a;https://wallpapers.microsoft.design/?refwww.8kmm.com 2.哲风壁纸&#xff1a;https://haowallpaper.com/wallpaperForum 3.壁纸湖&#xff1a;https://bizihu.com/ 4.极简壁纸&#xff1a;https://bz…

XILINX FPGA如何做时序分析和时序优化?

时序分析和时序优化是FPGA开发流程中关键步骤&#xff0c;确保设计在目标时钟频率下正确运行&#xff0c;避免时序违例&#xff08;如建立时间或保持时间不足&#xff09;。以下以Xilinx Kintex-7系列FPGA为例&#xff0c;详细介绍时序分析和时序优化的方法、工具、流程及实用技…

linux screen轻松管理长时间运行的任务

以下是针对 Alpine Linux 环境下 screen 的安装与使用指南&#xff0c;结合迁移数据场景的具体操作步骤&#xff1a; 1. 安装 screen‌ 在 Alpine Linux 中需通过 apk 安装&#xff08;非默认预装&#xff09;&#xff1a; apk add screen 验证安装&#xff1a; screen --…

VR制作公司业务范围

VR制作公司概念、能力与服务范围 虚拟现实&#xff08;Virtual Reality, VR&#xff09;技术&#xff0c;作为当代科技的前沿领域&#xff0c;通过计算机技术模拟出真实或虚构的世界环境&#xff0c;使用户能够沉浸其中并进行交互体验。VR制作公司&#xff0c;是这一领域的专业…

STM32之28BYJ-48步进电机驱动

目录 一、引言 二、28BYJ-48步进电机简介 2.1 基本特性 2.2 内部结构 2.3 工作模式 2.4 驱动原理 2.5 性能特点 2.6 驱动方案 2.7 使用注意事项 三、ULN2003驱动板简介 3.1 基本概述 3.2 电路结构 3.3 驱动原理 3.4 接口定义 3.5 使用注意事项 四、…

TDSQL如何查出某一列中的逗号数量

在 TDSQL 中&#xff0c;要统计某一列里逗号的数量&#xff0c;可借助字符串函数来实现。下面为你介绍具体的实现方法&#xff1a; sql SELECT your_column,LENGTH(your_column) - LENGTH(REPLACE(your_column, ,, )) AS comma_count FROM your_table;下面对这段 SQL 进行详细…

如何避免服务器出现故障情况?

服务器作为存储数据信息的重要网络设备&#xff0c;能够保护企业重要数据的安全性&#xff0c;但是随着网络攻击的不断拓展&#xff0c;各个行业中的服务器也会遭受到不同类型的网络攻击&#xff0c;严重的会导致服务器业务中断出现故障&#xff0c;给企业带来巨大的经济损失。…

C++ 优先级队列

一、引言 队列的特性是先进先出。优先级队列的本质是一个有序队列&#xff0c;根据成员的优先级&#xff0c;对队列中的成员进行排序。优先级队列默认是大顶堆&#xff0c;即堆顶元素最大 二、常用函数 empty()size()top()push()emplace()pop()swap() 三、代码示例 class …