中国电子建设公司网站,调整网站模板大小,html5博客网站模板,wordpress转discuz作为一名开发者#xff0c;你是否曾想过亲手搭建一个包含用户注册、登录认证和文件上传功能的完整 Web 系统#xff1f;今天#xff0c;我将带大家一步步拆解一个基于FastAPI#xff08;后端#xff09;和原生 JavaScript#xff08;前端#xff09;的前后端分离项目你是否曾想过亲手搭建一个包含用户注册、登录认证和文件上传功能的完整 Web 系统今天我将带大家一步步拆解一个基于FastAPI后端和原生 JavaScript前端的前后端分离项目从核心功能实现到关键技术点解析让你快速掌握前后端协作的精髓。
最后附超详细带解析的源码哦
一、项目整体介绍我们要做什么
这个项目是一个极简但完整的 Web 应用核心功能包括
用户注册支持用户名和密码注册包含前端表单验证和后端数据校验用户登录基于 JWTJSON Web Token的身份认证登录后返回令牌权限控制仅登录用户可访问文件上传功能文件上传支持二进制文件上传保存到服务器本地
整个系统采用前后端分离架构
前端HTMLCSS 原生 JavaScript用 Axios 发送 HTTP 请求后端FastAPI 框架处理业务逻辑、数据库交互和身份验证数据库SQLite轻量免配置适合演示通信方式JSON 格式数据交互文件上传采用 multipart/form-data 格式
二、技术栈解析为什么选这些工具
在开始实现前先了解下项目使用的核心技术栈及其优势
技术作用核心优势FastAPI后端框架高性能、自动生成 API 文档、类型提示友好、支持异步原生 JavaScript前端逻辑零依赖、兼容性好、适合理解 HTTP 请求本质Axios前端 HTTP 库支持 Promise、拦截器、请求 / 响应转换处理异步请求更优雅SQLAlchemyORM 工具简化数据库操作支持多种数据库避免手写 SQLJWT身份认证无状态、适合分布式系统、减少数据库查询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 令牌持久化保存关闭浏览器不丢失统一响应格式codemessagedata便于前端统一处理用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(bindengine)
Base declarative_base()
为什么需要 CORS 前后端分离时前端页面和后端 API 通常不在同一域名下浏览器会限制跨域请求。通过配置 CORS后端明确允许前端域名的请求解决 跨域错误。
2. 数据模型数据库与请求响应格式
用 SQLAlchemy 定义用户表结构用 Pydantic 定义请求 / 响应格式
# 数据库模型用户表
class User(Base):__tablename__ usersid Column(Integer, primary_keyTrue, indexTrue)username Column(String(255), uniqueTrue, indexTrue, nullableFalse)password Column(String(255), nullableFalse) # 存储哈希后的密码# 响应模型统一格式
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_modelResponseModel)
async def register(user: UserRegister):db Session()try:# 检查用户名是否已存在existing_user db.query(User).filter(User.username user.username).first()if existing_user:return ResponseModel(code400, message用户名已存在)# 密码加密关键绝不能明文存储hashed_password pwd_context.hash(user.password)new_user User(usernameuser.username, passwordhashed_password)# 保存到数据库db.add(new_user)db.commit()return ResponseModel(code200, message注册成功)finally:db.close() 密码安全的关键
使用passlib库的bcrypt算法哈希密码单向加密无法解密哈希过程会自动添加随机盐值相同密码哈希结果不同防止彩虹表攻击
4. JWT 认证无状态登录验证
JWTJSON 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(minutes30))to_encode.update({exp: expire}) # 添加过期时间字段# 生成token密钥算法return jwt.encode(to_encode, SECURITY_KET, algorithmALGORITHMS)# 登录接口
app.post(/api/login, response_modelResponseModel)
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(code400, message用户名或密码错误)# 验证密码哈希比对if not pwd_context.verify(user.password, db_user.password):return ResponseModel(code400, message用户名或密码错误)# 生成tokenaccess_token create_access_token(data{sub: user.username})return ResponseModel(code200,message登录成功,data{access_token: access_token, token_type: bearer})finally:db.close()
JWT 的优势
无状态服务器不需要存储用户登录状态减轻服务器压力跨域支持适合分布式系统多个服务可共用同一套认证机制携带信息token 中可包含用户基本信息如用户名减少数据库查询
5. 文件上传权限验证与文件存储
文件上传接口需要先验证用户 token再处理文件存储
app.post(/api/upload_binary, response_modelResponseModel)
async def upload_binary_file(file: UploadFile File(...), # 接收文件token: str Header(None, aliasAuthorization) # 接收token
):try:# 1. 验证token简化版实际项目建议用依赖注入if not token or not token.startswith(Bearer ):return ResponseModel(code401, message未授权请先登录)token token.split( )[1]try:# 解析token验证有效性payload jwt.decode(token, SECURITY_KET, algorithms[ALGORITHMS])except:return ResponseModel(code401, messagetoken无效或已过期)# 2. 保存文件upload_dir uploads_binaryif 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(code200, messagef文件 {file.filename} 上传成功)except Exception as e:return ResponseModel(code500, message文件上传失败)
文件上传的注意事项
目录权限确保服务器对uploads_binary目录有写入权限文件大小限制实际项目中需限制文件大小防止恶意上传大文件文件名处理可能需要重命名文件如添加时间戳避免同名文件覆盖
六、项目源码和运行
有了这些直接无脑运行再无后顾之忧。
1、项目结构 注意uploads_binary不需己创建数据库不需要自己创建系统运行自己创建。
2、项目源码
①login.html
!DOCTYPE html
html langzh-CN
headmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1.0title用户登录/titlestylebody {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
bodydiv classform-containerh1用户登录/h1!-- 登录表单 --form idloginForminput typetext nameusername placeholder用户名 required /input typepassword namepassword placeholder密码 required /button typesubmit登录/button/form!-- 提示信息 --div classalert success idsuccessAlert styledisplay: none;/divdiv classalert error iderrorAlert styledisplay: none;/div!-- 注册链接 --div classregister-link没有账号a hrefregister.html去注册/a/div/div!-- 引入 axios --script srchttps://cdnjs.cloudflare.com/ajax/libs/axios/1.6.2/axios.min.js/script
scriptdocument.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 langzh-CN
headmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1.0title用户注册/titlestylebody {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
bodydiv classform-containerh1用户注册/h1form classregister-forminput typetext nameusername placeholder用户名 idusername requiredinput typepassword namepassword placeholder密码 idpassword requiredinput typepassword namepassword placeholder确认密码 idpassword_isok requiredbutton typesubmit classbtn-register idsubtn注册/button/formdiv classlogin-link已有账号a hreflogin.html去登录/a/div!-- 提示框容器 --div classalert success idsuccessAlert styledisplay: none;/divdiv classalert error iderrorAlert styledisplay: none;/div/divscript srchttps://cdnjs.cloudflare.com/ajax/libs/axios/1.6.2/axios.min.js/scriptscriptdocument.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 langzh-CN
headmeta charsetUTF-8title首页/titlestylebody {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[typefile] {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
bodyh1欢迎回来/h1p您已成功登录。/p!-- 文件上传表单 --form iduploadForm classform-container stylemargin-top: 40px;h2上传文件/h2input typefile idfileInput namefile required /button typesubmit上传/button/form!-- 提示信息 --div classalert success idsuccessAlert styledisplay: none;/divdiv classalert error iderrorAlert styledisplay: none;/div!-- 引入 axios --script srchttps://cdnjs.cloudflare.com/ajax/libs/axios/1.6.2/axios.min.js/scriptscript// 页面加载时检查是否有 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], deprecatedauto)# 创建访问令牌
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(minutes15)# 添加过期时间字段to_encode.update({exp: expire})# 使用 jwt 库生成 token ( 加密内容 加密秘钥 加密算法 )encoded_jwt jwt.encode(to_encode, SECURITY_KET, algorithmALGORITHMS)return encoded_jwt# 创建 FastAPI 实例对象这是整个应用的核心
app FastAPI()# 添加CORS中间件允许跨域传输
app.add_middleware(CORSMiddleware,allow_origins[*], # 允许所有源allow_credentialsTrue, # 是否允许发送 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(bindengine, autocommitFalse, autoflushFalse, expire_on_commitFalse)# 创建数据库表结构(可以创建数据库表结构)
class User(Base):__tablename__ usersid Column(Integer, primary_keyTrue, indexTrue)username Column(String(255), uniqueTrue, indexTrue, nullableFalse)password Column(String(255), nullableFalse)class Token(BaseModel):用于响应 token 的数据模型access_token: strtoken_type: str# 执行创建数据库表结构
Base.metadata.create_all(bindengine)# 定义注册接口的请求数据模型
class UserRegister(BaseModel):# 用户名字段# - 至少 3 个字符长# - 只能包含英文字母、数字和中文字符username: str Field(min_length6, pattern^[a-zA-Z0-9\u4e00-\u9fa5]$)# 密码字段# - 至少 6 个字符长password: constr(min_length6)# 定义统一的响应数据模型便于前端解析处理结果
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_modelResponseModel)
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(code400, message用户名或密码错误)# 验证密码是否匹配if not pwd_context.verify(user.password, db_user.password):return ResponseModel(code400, message用户名或密码错误)access_token_expires timedelta(minutesACCESS_TOKEN_EXPIRE_MINUTES)access_token create_access_token(data{sub: user.username},expires_deltaaccess_token_expires)return ResponseModel(code200,message登录成功,data{access_token: access_token, token_type: bearer})except Exception as e:print(服务器错误详情:, str(e))return ResponseModel(code500, message服务器错误)finally:db.close()# 定义上传文件的接口
app.post(/api/upload_binary, response_modelResponseModel)
async def upload_binary_file(#File(...) 表示该参数是一个文件类型的参数并且是必填项... 是 Python 的 Ellipsis表示必填# UploadFile 是 FastAPI 提供的一个类用于表示上传的文件。file: UploadFile File(...),# Optional[str] 表示这个参数可以不传默认为 Nonefilename: Optional[str] None
):try:# 创建存储文件的目录upload_dir uploads_binaryif 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(code200, messagef文件 {save_filename} 上传成功)except Exception as e:print(文件上传失败, str(e))return ResponseModel(code500, message文件上传失败)# 注册接口
app.post(/api/register, response_modelResponseModel) # response_modelResponseModel表示这个接口返回的数据结构必须符合 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_code400, detail用户名已存在)# 使用哈希加密存储密码hase_password pwd_context.hash(user.password)new_user User(usernameuser.username, passwordhase_password)# 将新用户插入到数据库中db.add(new_user)db.commit()db.refresh(new_user)return ResponseModel(code200, message注册成功)except HTTPException as e:# 如果用户名已存在抛出 HTTP 异常前端执行 catch 块显示错误信息return ResponseModel(codee.status_code, messagee.detail)except Exception as e:# 如果发生异常回滚事务并返回错误信息print(服务器错误详情:, str(e))db.rollback()return ResponseModel(code500, message服务器错误)finally:db.close()if __name__ __main__:import uvicornuvicorn.run(app, host127.0.0.1, port8080)3、项目运行
①安装后端依赖
pip install fastapi uvicorn sqlalchemy python-jose passlib[bcrypt] ②先运行后端再运行前端
运行后端 运行前端 七、总结前后端分离开发的核心思路
通过这个项目我们可以总结出前后端分离开发的关键原则
职责清晰前端负责用户交互和数据展示后端负责业务逻辑和数据存储接口先行前后端约定好接口文档FastAPI 自动生成并行开发数据安全敏感数据如密码必须在后端处理前端只做展示和基础验证状态管理前端负责维护客户端状态如登录状态后端通过 token 验证身份
如果你是前端开发者这个项目能帮你理解后端的认证逻辑如果你是后端开发者能让你更清晰前端的请求处理方式。关注我后续会带来更多前后端实战项目解析
你在开发中遇到过哪些前后端协作的坑欢迎在评论区分享你的解决方案有不懂的都可以来问小宁哦