作為一名開發者,你是否曾想過親手搭建一個包含用戶注冊、登錄認證和文件上傳功能的完整 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到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 驗證身份
如果你是前端開發者,這個項目能幫你理解后端的認證邏輯;如果你是后端開發者,能讓你更清晰前端的請求處理方式。關注我,后續會帶來更多前后端實戰項目解析!
你在開發中遇到過哪些前后端協作的坑?歡迎在評論區分享你的解決方案,有不懂的都可以來問小寧哦~