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

发布于:2025-07-04 ⋅ 阅读:(25) ⋅ 点赞:(0)

作为一名开发者,你是否曾想过亲手搭建一个包含用户注册、登录认证和文件上传功能的完整 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、拦截器、请求 / 响应转换,处理异步请求更优雅
SQLAlchemy ORM 工具 简化数据库操作,支持多种数据库,避免手写 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到localStorage
    localStorage.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="用户名或密码错误")
    
    # 生成token
    access_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, File


SECURITY_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_delta
    else:
        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,  # 是否允许发送 Cookie
    allow_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: str
    token_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: str
    password: str

# 定义文件上传请求数据模型
class UploadRequest(BaseModel):
    filename: str
    content: 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] 表示这个参数可以不传,默认为 None
        filename: 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.filename
        file_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 uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8080)

3、项目运行

①安装后端依赖:

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

 ②先运行后端再运行前端

运行后端:

运行前端:

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

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

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

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

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