回滚bug修复
This commit is contained in:
@@ -166,3 +166,28 @@
|
|||||||
- 第 148 行 JS 渲染处:`<td>${student.parent_phone || '-'}</td>` 改为根据 `userRole` 判断渲染内容:班主任显示真实手机号,其他角色显示 `***`
|
- 第 148 行 JS 渲染处:`<td>${student.parent_phone || '-'}</td>` 改为根据 `userRole` 判断渲染内容:班主任显示真实手机号,其他角色显示 `***`
|
||||||
- 第 156 行空数据提示处:`colspan="6"` 需根据角色动态调整——班主任 6 列,非班主任 5 列(可用 PHP 输出:`colspan="<?php echo $role === '班主任' ? '6' : '5'; ?>"`)
|
- 第 156 行空数据提示处:`colspan="6"` 需根据角色动态调整——班主任 6 列,非班主任 5 列(可用 PHP 输出:`colspan="<?php echo $role === '班主任' ? '6' : '5'; ?>"`)
|
||||||
- 新增学生表单(第 101-129 行)和导入学生功能中的手机号字段保持不变,任何有学生管理权限的角色都能录入(与 proposal 一致)
|
- 新增学生表单(第 101-129 行)和导入学生功能中的手机号字段保持不变,任何有学生管理权限的角色都能录入(与 proposal 一致)
|
||||||
|
|
||||||
|
### 阶段 7:修复 AuthMiddleware 401 无限循环
|
||||||
|
|
||||||
|
- [x] 7.1 AuthMiddleware 添加调试日志和修复潜在问题
|
||||||
|
【目标对象】`backend/middleware/auth_middleware.py`
|
||||||
|
【修改目的】修复 401 无限循环问题。当前 AuthMiddleware 注册为全局中间件后,所有非公开路径的请求都需要 JWT 认证,但缺少详细日志无法定位 Token 验证失败的具体原因。需要添加详细日志并修复 OPEN_PATHS 未被检查的 Bug。
|
||||||
|
【修改方式】在 dispatch 方法中添加每个认证步骤的调试日志,修复 OPEN_PATHS 逻辑,添加更多公开路径
|
||||||
|
【修改内容】
|
||||||
|
- 在 dispatch 方法中每个关键检查点添加 logger.debug 日志:
|
||||||
|
1. 请求路径和方法
|
||||||
|
2. Authorization header 是否存在
|
||||||
|
3. JWT 验证结果(成功时记录 user_id,失败时记录原因)
|
||||||
|
4. Redis Token 查询结果(stored_token 是否存在、是否匹配)
|
||||||
|
- 修复 OPEN_PATHS 未被检查的 Bug:在 is_public_path 检查后增加 OPEN_PATHS 检查,OPEN_PATHS 中的路径跳过 Token 验证但仍继续到路由层
|
||||||
|
- 将 `/api/auth/me` 和 `/api/auth/change-password` 添加到 PUBLIC_PATHS(目前 OPEN_PATHS 逻辑未被 dispatch 使用,导致这些路径被拦截)
|
||||||
|
- `_cors_response` 方法中的 allowed_origins 改为从 config.settings 读取,而非硬编码
|
||||||
|
|
||||||
|
- [x] 7.2 前端 common.js 添加 401 防循环机制
|
||||||
|
【目标对象】`frontend/assets/js/common.js`
|
||||||
|
【修改目的】防止 401 响应导致的无限刷新循环。当前 apiRequest 在收到 401 时直接 clearAuth + redirect,如果用户重新登录后 Token 仍然无效(如 Redis 重启),会形成 401 → 登录 → 401 → 登录的无限循环。
|
||||||
|
【修改方式】在 401 处理逻辑中添加防循环机制
|
||||||
|
【修改内容】
|
||||||
|
- 在 401 处理中添加路径判断:如果当前已在 `/index.php`(登录页),不执行重定向
|
||||||
|
- 添加重定向频率限制:使用 sessionStorage 记录最近一次 401 重定向时间,如果 5 秒内重复 401,停止重定向并在控制台输出警告
|
||||||
|
- 清除认证信息后,在跳转前添加延迟或在 URL 中添加标记参数,防止浏览器缓存导致的循环
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from fastapi.responses import JSONResponse
|
|||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from config import settings
|
||||||
from utils.jwt_handler import jwt_handler
|
from utils.jwt_handler import jwt_handler
|
||||||
from utils.redis_client import RedisClient
|
from utils.redis_client import RedisClient
|
||||||
from utils.response import unauthorized_response
|
from utils.response import unauthorized_response
|
||||||
@@ -32,12 +33,6 @@ PUBLIC_PATHS = [
|
|||||||
r'^/debug/.*$', # 调试入口
|
r'^/debug/.*$', # 调试入口
|
||||||
]
|
]
|
||||||
|
|
||||||
# 不需要Token验证但需要记录访问的路由
|
|
||||||
OPEN_PATHS = [
|
|
||||||
r'^/api/auth/change-password$',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def is_public_path(path: str) -> bool:
|
def is_public_path(path: str) -> bool:
|
||||||
"""检查是否为公开路径"""
|
"""检查是否为公开路径"""
|
||||||
for pattern in PUBLIC_PATHS:
|
for pattern in PUBLIC_PATHS:
|
||||||
@@ -50,34 +45,42 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|||||||
"""JWT认证中间件"""
|
"""JWT认证中间件"""
|
||||||
|
|
||||||
async def dispatch(self, request: Request, call_next):
|
async def dispatch(self, request: Request, call_next):
|
||||||
|
path = request.url.path
|
||||||
|
|
||||||
# OPTIONS 预检请求跳过认证
|
# OPTIONS 预检请求跳过认证
|
||||||
if request.method == "OPTIONS":
|
if request.method == "OPTIONS":
|
||||||
|
logger.debug(f"[Auth] OPTIONS {path} - 跳过认证")
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
|
|
||||||
path = request.url.path
|
|
||||||
|
|
||||||
# 公开路径跳过认证
|
# 公开路径跳过认证
|
||||||
if is_public_path(path):
|
if is_public_path(path):
|
||||||
|
logger.debug(f"[Auth] {request.method} {path} - 公开路径,跳过认证")
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
|
|
||||||
|
logger.info(f"[Auth] {request.method} {path} - 开始认证")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 获取Authorization头
|
# 获取Authorization头
|
||||||
auth_header = request.headers.get("Authorization")
|
auth_header = request.headers.get("Authorization")
|
||||||
|
|
||||||
if not auth_header:
|
if not auth_header:
|
||||||
|
logger.warning(f"[Auth] {path} - 缺少Authorization header")
|
||||||
return self._cors_response(request, 401, "缺少认证令牌")
|
return self._cors_response(request, 401, "缺少认证令牌")
|
||||||
|
|
||||||
# 解析Bearer Token
|
# 解析Bearer Token
|
||||||
try:
|
try:
|
||||||
scheme, token = auth_header.split()
|
scheme, token = auth_header.split()
|
||||||
if scheme.lower() != "bearer":
|
if scheme.lower() != "bearer":
|
||||||
|
logger.warning(f"[Auth] {path} - Authorization header格式错误")
|
||||||
return self._cors_response(request, 401, "认证格式错误")
|
return self._cors_response(request, 401, "认证格式错误")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
logger.warning(f"[Auth] {path} - Authorization header格式错误")
|
||||||
return self._cors_response(request, 401, "认证格式错误")
|
return self._cors_response(request, 401, "认证格式错误")
|
||||||
|
|
||||||
# 验证Token
|
# 验证Token
|
||||||
payload = jwt_handler.verify_token(token)
|
payload = jwt_handler.verify_token(token)
|
||||||
if not payload:
|
if not payload:
|
||||||
|
logger.warning(f"[Auth] {path} - JWT验证失败")
|
||||||
return self._cors_response(request, 401, "令牌无效或已过期")
|
return self._cors_response(request, 401, "令牌无效或已过期")
|
||||||
|
|
||||||
# 验证Redis中的Token
|
# 验证Redis中的Token
|
||||||
@@ -85,6 +88,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|||||||
stored_token = await RedisClient.get_user_token(user_id)
|
stored_token = await RedisClient.get_user_token(user_id)
|
||||||
|
|
||||||
if not stored_token or stored_token != token:
|
if not stored_token or stored_token != token:
|
||||||
|
logger.warning(f"[Auth] {path} - Redis Token不匹配, user_id={user_id}, stored={'有' if stored_token else '无'}")
|
||||||
return self._cors_response(request, 401, "令牌已失效,请重新登录")
|
return self._cors_response(request, 401, "令牌已失效,请重新登录")
|
||||||
|
|
||||||
# 将用户信息存储到request.state
|
# 将用户信息存储到request.state
|
||||||
@@ -95,9 +99,10 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|||||||
request.state.role = payload.get("role")
|
request.state.role = payload.get("role")
|
||||||
|
|
||||||
# 刷新Token过期时间
|
# 刷新Token过期时间
|
||||||
from config import settings
|
|
||||||
await RedisClient.expire(f"user_token:{user_id}", settings.JWT_EXPIRE_MINUTES * 60)
|
await RedisClient.expire(f"user_token:{user_id}", settings.JWT_EXPIRE_MINUTES * 60)
|
||||||
|
|
||||||
|
logger.debug(f"[Auth] {path} - 认证成功, user_id={user_id}, username={payload.get('username')}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"认证中间件异常: {e}", exc_info=True)
|
logger.error(f"认证中间件异常: {e}", exc_info=True)
|
||||||
return self._cors_response(request, 401, "认证服务异常,请稍后重试")
|
return self._cors_response(request, 401, "认证服务异常,请稍后重试")
|
||||||
@@ -107,7 +112,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|||||||
def _cors_response(self, request: Request, status_code: int, message: str) -> JSONResponse:
|
def _cors_response(self, request: Request, status_code: int, message: str) -> JSONResponse:
|
||||||
"""创建带CORS头的响应"""
|
"""创建带CORS头的响应"""
|
||||||
origin = request.headers.get("origin", "")
|
origin = request.headers.get("origin", "")
|
||||||
allowed_origins = ["https://class.sea-studio.top", "https://classbackendapi.sea-studio.top"]
|
allowed_origins = settings.CORS_ORIGINS or []
|
||||||
|
|
||||||
headers = {}
|
headers = {}
|
||||||
if origin in allowed_origins:
|
if origin in allowed_origins:
|
||||||
|
|||||||
@@ -51,6 +51,22 @@ async function apiRequest(url, options = {}) {
|
|||||||
|
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
clearAuth();
|
clearAuth();
|
||||||
|
|
||||||
|
// 防循环机制:检查是否已在登录页
|
||||||
|
if (window.location.pathname === '/index.php' || window.location.pathname === '/') {
|
||||||
|
console.warn('[Auth] 已在登录页收到401,停止重定向');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防循环机制:5秒内重复401则停止重定向
|
||||||
|
const now = Date.now();
|
||||||
|
const lastRedirect = parseInt(sessionStorage.getItem('_last_401_redirect') || '0');
|
||||||
|
if (now - lastRedirect < 5000) {
|
||||||
|
console.warn('[Auth] 5秒内重复401,停止重定向。请检查Token是否有效。');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
sessionStorage.setItem('_last_401_redirect', now.toString());
|
||||||
|
|
||||||
window.location.href = '/index.php';
|
window.location.href = '/index.php';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user