您的位置:首页 >FastAPI与React JWT会话管理实践
发布于2025-09-07 阅读(0)
扫一扫,手机访问

在Web应用中,有时我们需要在用户未登录的情况下也能追踪其行为或提供个性化服务。例如,电商网站的访客购物车、内容推荐系统等。传统上,这通常通过Cookie来实现。然而,当后端(FastAPI)和前端(React)部署在不同域时,Cookie的跨域限制(CORS、SameSite策略)以及withCredentials配置可能导致复杂的问题,如HTTP 400 Bad Request错误。
JSON Web Token (JWT) 提供了一种更灵活、无状态的会话管理方案。通过将用户标识信息编码到Token中,并由客户端在每次请求时通过Authorization头发送,可以有效规避Cookie的跨域限制,同时实现对匿名用户的识别和跟踪。
核心思想是复用FastAPI的JWT认证机制,但将其应用于“匿名用户”的概念:
我们将基于FastAPI的security模块,构建匿名用户会话管理。
首先,定义JWT相关的配置,包括密钥、算法和过期时间。
# auth_utils.py
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
# 密钥和算法
SECRET_KEY = "your-secret-key" # 生产环境请使用强随机密钥
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 例如,匿名会话保留7天
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") # tokenUrl可以是任何你定义的登录或获取token的路由
class TokenData(BaseModel):
username: Optional[str] = None
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def decode_token(token: str):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return TokenData(username=username)
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
为了跟踪匿名用户的行为,我们需要在数据库中存储他们的信息。
# database.py (示例,实际可能使用SQLAlchemy/ORM)
from typing import Dict, Any
import uuid
# 模拟数据库
anonymous_users_db: Dict[str, Dict[str, Any]] = {}
class AnonymousUser:
def __init__(self, user_id: str, created_at: datetime = None, last_active_at: datetime = None, data: Dict = None):
self.user_id = user_id
self.created_at = created_at if created_at else datetime.utcnow()
self.last_active_at = last_active_at if last_active_at else datetime.utcnow()
self.data = data if data is not None else {} # 用于存储用户特定数据,如购物车、偏好等
def to_dict(self):
return {
"user_id": self.user_id,
"created_at": self.created_at.isoformat(),
"last_active_at": self.last_active_at.isoformat(),
"data": self.data
}
def get_anonymous_user_from_db(user_id: str) -> Optional[AnonymousUser]:
user_data = anonymous_users_db.get(user_id)
if user_data:
return AnonymousUser(
user_id=user_data["user_id"],
created_at=datetime.fromisoformat(user_data["created_at"]),
last_active_at=datetime.fromisoformat(user_data["last_active_at"]),
data=user_data["data"]
)
return None
def save_anonymous_user_to_db(user: AnonymousUser):
anonymous_users_db[user.user_id] = user.to_dict()
def update_anonymous_user_activity(user_id: str):
user = get_anonymous_user_from_db(user_id)
if user:
user.last_active_at = datetime.utcnow()
save_anonymous_user_to_db(user)当客户端首次访问时,调用此路由以获取匿名会话Token。
# main.py
from fastapi import FastAPI, Response, status, Depends
from fastapi.responses import JSONResponse
from auth_utils import create_access_token, decode_token, oauth2_scheme, TokenData
from database import get_anonymous_user_from_db, save_anonymous_user_to_db, update_anonymous_user_activity, AnonymousUser
import uuid
from datetime import datetime
app = FastAPI()
# 跨域设置,根据你的前端域名调整
from fastapi.middleware.cors import CORSMiddleware
origins = [
"http://localhost:3000", # React 开发服务器地址
# "https://your-frontend-domain.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 依赖注入:获取当前匿名用户
async def get_current_anonymous_user(token: str = Depends(oauth2_scheme)) -> AnonymousUser:
token_data = decode_token(token)
username = token_data.username
if not username or not username.startswith("anonymous_"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not an anonymous user token",
)
user = get_anonymous_user_from_db(username)
if not user:
# 如果数据库中没有,可能是token过期后首次访问,或数据库被清空
# 这里可以选择重新生成匿名用户或抛出错误
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Anonymous user not found in database. Please re-initialize session.",
)
# 更新用户活跃时间
update_anonymous_user_activity(username)
return user
@app.get("/api/v1/anonymous-session")
async def get_anonymous_session():
# 生成新的匿名用户ID
anonymous_id = f"anonymous_{uuid.uuid4().hex}"
# 创建匿名用户记录并保存到数据库
new_anonymous_user = AnonymousUser(user_id=anonymous_id)
save_anonymous_user_to_db(new_anonymous_user)
# 生成访问令牌
access_token = create_access_token(data={"sub": anonymous_id})
return {"access_token": access_token, "token_type": "bearer", "user_id": anonymous_id}
@app.get("/api/v1/my-anonymous-data")
async def get_my_anonymous_data(current_user: AnonymousUser = Depends(get_current_anonymous_user)):
"""
一个示例API,用于获取当前匿名用户的数据。
"""
return {
"user_id": current_user.user_id,
"created_at": current_user.created_at.isoformat(),
"last_active_at": current_user.last_active_at.isoformat(),
"data": current_user.data,
"message": f"Hello, anonymous user {current_user.user_id}! Here is your session data."
}
@app.post("/api/v1/update-anonymous-data")
async def update_anonymous_data(
data: dict,
current_user: AnonymousUser = Depends(get_current_anonymous_user)
):
"""
一个示例API,用于更新当前匿名用户的数据。
"""
current_user.data.update(data)
save_anonymous_user_to_db(current_user) # 确保更新后的数据被保存
return {"message": "Anonymous data updated successfully", "new_data": current_user.data}
React前端需要负责在首次访问时请求匿名会话Token,并将其存储起来,然后在后续请求中附加到Authorization头。
// api.js (或一个服务文件)
import axios from 'axios';
const API_BASE_URL = 'http://localhost:8000/api/v1'; // 你的FastAPI后端地址
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器:在每次请求前添加Authorization头
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('anonymous_access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
export const getAnonymousSession = async () => {
try {
const response = await api.get('/anonymous-session');
const { access_token, user_id } = response.data;
localStorage.setItem('anonymous_access_token', access_token);
localStorage.setItem('anonymous_user_id', user_id); // 可选:存储匿名ID方便调试
console.log('Anonymous session established:', user_id);
return { access_token, user_id };
} catch (error) {
console.error('Error getting anonymous session:', error);
throw error;
}
};
export const getMyAnonymousData = async () => {
try {
const response = await api.get('/my-anonymous-data');
return response.data;
} catch (error) {
console.error('Error getting anonymous data:', error);
throw error;
}
};
export const updateAnonymousData = async (data) => {
try {
const response = await api.post('/update-anonymous-data', data);
return response.data;
} catch (error) {
console.error('Error updating anonymous data:', error);
throw error;
}
};
// App.js (React组件示例)
import React, { useEffect, useState } from 'react';
import { getAnonymousSession, getMyAnonymousData, updateAnonymousData } from './api';
function App() {
const [anonymousData, setAnonymousData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const initializeSession = async () => {
try {
// 检查是否存在匿名会话Token
let token = localStorage.getItem('anonymous_access_token');
if (!token) {
// 如果没有,则获取新的匿名会话
await getAnonymousSession();
}
// 获取匿名用户数据
const data = await getMyAnonymousData();
setAnonymousData(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
initializeSession();
}, []);
const handleUpdateData = async () => {
try {
const newData = { favoriteColor: 'blue', lastVisited: new Date().toISOString() };
const updated = await updateAnonymousData(newData);
setAnonymousData(prev => ({ ...prev, data: updated.new_data }));
alert('Data updated!');
} catch (err) {
console.error('Failed to update data:', err);
alert('Failed to update data.');
}
};
if (loading) return <div>Loading anonymous session...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>FastAPI & React Anonymous Session</h1>
{anonymousData && (
<div>
<p><strong>Anonymous User ID:</strong> {anonymousData.user_id}</p>
<p><strong>Created At:</strong> {new Date(anonymousData.created_at).toLocaleString()}</p>
<p><strong>Last Active At:</strong> {new Date(anonymousData.last_active_at).toLocaleString()}</p>
<p><strong>Stored Data:</strong> {JSON.stringify(anonymousData.data)}</p>
<button onClick={handleUpdateData}>Update My Data</button>
</div>
)}
</div>
);
}
export default App;通过将FastAPI的JWT认证机制巧妙地应用于匿名用户,我们可以为未登录用户提供稳定且可追踪的会话体验。这种方法避免了传统Cookie在跨域场景下可能遇到的复杂问题,并通过数据库持久化实现了匿名用户行为的长期跟踪。结合前端的Token管理,可以构建出功能强大且用户体验良好的现代Web应用。
上一篇:Soul虚拟伴侣语音是真人吗?
下一篇:Java构造器重载与可变参数应用
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
9