fix: 修复混合认证方案 - Cookie + Authorization Header
- 修改 login/register API 使用 JSONResponse 正确设置 Cookie - 添加 path="/" 确保 Cookie 在所有路径可用 - 前端同时使用 localStorage token 进行 API 认证 - 修复登录后闪屏返回登录页的问题 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -528,7 +528,7 @@ def get_current_user_id(authorization: Optional[str] = Header(None)) -> int:
|
||||
# ===== 认证 API =====
|
||||
|
||||
@app.post("/api/auth/login")
|
||||
async def login(data: LoginInput, response: Response):
|
||||
async def login(data: LoginInput):
|
||||
"""用户登录"""
|
||||
user = db.get_user_by_name(data.username)
|
||||
if not user:
|
||||
@@ -543,6 +543,17 @@ async def login(data: LoginInput, response: Response):
|
||||
# 创建 token(根据 remember_me 设置不同的过期时间)
|
||||
token = create_token(user.id, user.name, user.is_admin, data.remember_me)
|
||||
|
||||
# 创建响应并设置 Cookie
|
||||
response = JSONResponse(content={
|
||||
"token": token,
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"name": user.name,
|
||||
"is_admin": user.is_admin,
|
||||
"is_disabled": user.is_disabled,
|
||||
}
|
||||
})
|
||||
|
||||
# 设置 HTTPOnly Cookie
|
||||
max_age = get_token_expire_seconds(data.remember_me) if data.remember_me else None
|
||||
response.set_cookie(
|
||||
@@ -552,21 +563,14 @@ async def login(data: LoginInput, response: Response):
|
||||
secure=False, # HTTP 环境,生产环境应设为 True
|
||||
samesite="lax",
|
||||
max_age=max_age, # None 表示会话 Cookie
|
||||
path="/", # 确保所有路径都能使用此 Cookie
|
||||
)
|
||||
|
||||
return {
|
||||
"token": token,
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"name": user.name,
|
||||
"is_admin": user.is_admin,
|
||||
"is_disabled": user.is_disabled,
|
||||
}
|
||||
}
|
||||
return response
|
||||
|
||||
|
||||
@app.post("/api/auth/register")
|
||||
async def register(data: RegisterInput, response: Response):
|
||||
async def register(data: RegisterInput):
|
||||
"""用户注册(需要邀请码)"""
|
||||
# 验证邀请码
|
||||
invite = db.get_invite_by_code(data.invite_code)
|
||||
@@ -599,18 +603,9 @@ async def register(data: RegisterInput, response: Response):
|
||||
# 获取创建的用户
|
||||
user = db.get_user(user_id)
|
||||
|
||||
# 生成 token 并设置 Cookie
|
||||
# 生成 token 并创建响应
|
||||
token = create_token(user.id, user.name, user.is_admin)
|
||||
response.set_cookie(
|
||||
key="auth_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
secure=False,
|
||||
samesite="lax",
|
||||
max_age=get_token_expire_seconds(False), # 注册默认 1 天
|
||||
)
|
||||
|
||||
return {
|
||||
response = JSONResponse(content={
|
||||
"token": token,
|
||||
"user": {
|
||||
"id": user.id,
|
||||
@@ -618,7 +613,20 @@ async def register(data: RegisterInput, response: Response):
|
||||
"is_admin": user.is_admin,
|
||||
"is_disabled": user.is_disabled,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
# 设置 Cookie
|
||||
response.set_cookie(
|
||||
key="auth_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
secure=False,
|
||||
samesite="lax",
|
||||
max_age=get_token_expire_seconds(False), # 注册默认 1 天
|
||||
path="/",
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@app.post("/api/auth/logout")
|
||||
@@ -2228,17 +2236,24 @@ def get_login_page_html() -> str:
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// 保存用户信息(非敏感)
|
||||
// 保存 token 和用户信息
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
|
||||
// 显示成功状态
|
||||
btn.textContent = '✓ 登录成功';
|
||||
btn.classList.add('success');
|
||||
|
||||
// 跳转
|
||||
// 跳转(管理员跳转到管理页面)
|
||||
setTimeout(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const redirect = params.get('redirect') || '/';
|
||||
let redirect = params.get('redirect');
|
||||
|
||||
// 如果没有指定重定向,管理员默认跳转到 /admin
|
||||
if (!redirect) {
|
||||
redirect = data.user.is_admin ? '/admin' : '/';
|
||||
}
|
||||
|
||||
window.location.href = redirect;
|
||||
}, 500);
|
||||
} else {
|
||||
@@ -2566,7 +2581,8 @@ def get_register_page_html() -> str:
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// 保存用户信息
|
||||
// 保存 token 和用户信息
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
|
||||
// 显示成功
|
||||
@@ -2959,18 +2975,28 @@ def get_admin_page_html() -> str:
|
||||
} catch (e) {
|
||||
// 忽略错误
|
||||
}
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('token');
|
||||
}
|
||||
|
||||
async function apiRequest(url, options = {}) {
|
||||
const token = getToken();
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
}
|
||||
headers
|
||||
});
|
||||
if (response.status === 401) {
|
||||
logout();
|
||||
@@ -7133,6 +7159,26 @@ def get_settings_page_html() -> str:
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 账户管理 -->
|
||||
<div class="section">
|
||||
<h2>账户</h2>
|
||||
<p style="color: #64748B; margin-bottom: 20px;">当前登录账户管理</p>
|
||||
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; padding: 20px; background: #F8FAFC; border-radius: 8px; margin-bottom: 20px;">
|
||||
<div>
|
||||
<div style="font-weight: 600; color: #1E293B; font-size: 1.1rem;" id="account-name">--</div>
|
||||
<div style="color: #64748B; font-size: 0.9rem; margin-top: 4px;" id="account-role">普通用户</div>
|
||||
</div>
|
||||
<div id="account-badge" style="display: none; padding: 4px 12px; background: #3B82F6; color: white; border-radius: 12px; font-size: 0.75rem; font-weight: 700;">
|
||||
管理员
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-danger" onclick="logoutAccount()" style="width: 100%;">
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 创建用户模态框 -->
|
||||
<div id="create-user-modal" class="modal">
|
||||
<div class="modal-content" style="max-width: 500px;">
|
||||
@@ -7575,10 +7621,50 @@ def get_settings_page_html() -> str:
|
||||
});
|
||||
});
|
||||
|
||||
// 加载账户信息
|
||||
async function loadAccountInfo() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/me', { credentials: 'same-origin' });
|
||||
if (response.ok) {
|
||||
const user = await response.json();
|
||||
document.getElementById('account-name').textContent = user.name;
|
||||
document.getElementById('account-role').textContent = user.is_admin ? '管理员账户' : '普通用户';
|
||||
if (user.is_admin) {
|
||||
document.getElementById('account-badge').style.display = 'block';
|
||||
}
|
||||
// 保存到 localStorage
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载账户信息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
async function logoutAccount() {
|
||||
if (!confirm('确定要退出登录吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('登出失败:', error);
|
||||
}
|
||||
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
// 页面加载
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadUsers();
|
||||
loadProfile();
|
||||
loadAccountInfo();
|
||||
|
||||
// BMI 实时计算
|
||||
document.getElementById('profile-height').addEventListener('input', calculateBMI);
|
||||
|
||||
Reference in New Issue
Block a user