Commit 72e22009 authored by 李文光's avatar 李文光

fix(extension): 修复浏览器扩展登录与登录态恢复

- main.py: CORS 放行 chrome-extension 源并开启 Private Network Access,解决扩展跨域预检 400
- security.py: 鉴权中间件放行 OPTIONS 预检,避免需登录接口(如 /api/auth/me)预检被 401 拦截
- popup.js: 令牌仅在 401/403 时清除,瞬态校验失败保留登录信息;测试连接按钮结束后复位
- test_static_security.py: 新增认证接口预检回归测试
parent 755cfd9d
...@@ -80,6 +80,10 @@ def create_app() -> FastAPI: ...@@ -80,6 +80,10 @@ def create_app() -> FastAPI:
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=settings.cors_origins, allow_origins=settings.cors_origins,
# 浏览器扩展(popup/后台 Service Worker)的源是 chrome-extension://<随机ID>,无法静态枚举,
# 用正则放行扩展源;同时放开 Private Network Access,便于经局域网 IP 访问后端。
allow_origin_regex=r"chrome-extension://.*",
allow_private_network=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "X-Ingest-Token", "Authorization"], allow_headers=["Content-Type", "X-Ingest-Token", "Authorization"],
allow_credentials=False, allow_credentials=False,
......
...@@ -22,6 +22,10 @@ class DeploymentAuthMiddleware(BaseHTTPMiddleware): ...@@ -22,6 +22,10 @@ class DeploymentAuthMiddleware(BaseHTTPMiddleware):
# 静态资源与登录页本身不设防,否则未登录时页面无法加载 # 静态资源与登录页本身不设防,否则未登录时页面无法加载
if not path.startswith("/api"): if not path.startswith("/api"):
return await call_next(request) return await call_next(request)
# CORS 预检(OPTIONS)不携带 Authorization,若不先放行,需登录接口的预检会被当成未登录拦下(401),
# 浏览器拿不到 CORS 头从而中断真实请求。预检本身只做跨域校验,交给 CORSMiddleware 决定允许/拒绝。
if request.method == "OPTIONS":
return await call_next(request)
settings = get_settings() settings = get_settings()
if path in _EXEMPT_API_PATHS: if path in _EXEMPT_API_PATHS:
return await call_next(request) return await call_next(request)
......
...@@ -32,3 +32,19 @@ def test_frontend_origin_is_allowed_by_cors(client): ...@@ -32,3 +32,19 @@ def test_frontend_origin_is_allowed_by_cors(client):
assert response.status_code == 200 assert response.status_code == 200
assert response.headers["access-control-allow-origin"] == "*" assert response.headers["access-control-allow-origin"] == "*"
def test_authenticated_endpoint_preflight_bypasses_auth_middleware(client):
# CORS 预检不携带 Authorization;鉴权中间件必须放行 OPTIONS,否则 /api/auth/me 这类受保护接口的
# 预检会被当成未登录以 401 拦下,浏览器拿不到 CORS 头导致真实请求中断(扩展登录态校验正依赖此接口)。
response = client.options(
"/api/auth/me",
headers={
"Origin": "chrome-extension://abcdefghijklmnop",
"Access-Control-Request-Method": "GET",
"Access-Control-Request-Headers": "authorization",
"Authorization": "",
},
)
assert response.status_code == 200
assert response.headers["access-control-allow-origin"] == "*"
...@@ -70,14 +70,21 @@ function validateToken(token) { ...@@ -70,14 +70,21 @@ function validateToken(token) {
const base = baseOf(apiUrlInput.value); const base = baseOf(apiUrlInput.value);
if (!base) return setAuthUI(false); if (!base) return setAuthUI(false);
fetch(base + "/api/auth/me", { headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(5000) }) fetch(base + "/api/auth/me", { headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(5000) })
.then((r) => (r.ok ? r.json() : Promise.reject(new Error("status " + r.status)))) .then((r) => (r.ok ? r.json() : Promise.reject({ status: r.status })))
.then((data) => { .then((data) => {
const name = data.user?.name || data.user?.username || ""; const name = data.user?.name || data.user?.username || "";
chrome.storage.local.set({ token, username: data.user?.username || "" }); chrome.storage.local.set({ token, username: data.user?.username || "" });
setAuthUI(true, name); setAuthUI(true, name);
}) })
.catch(() => { .catch((err) => {
chrome.storage.local.remove(["token", "username"]); // 仅当后端明确拒绝令牌(401/403)才清除登录态;网络/CORS/超时等瞬态失败保留已存信息,
// 避免「重开一次就静默登出、用户名也被清空」。
const status = err && err.status;
if (status === 401 || status === 403) {
chrome.storage.local.remove(["token", "username"]);
} else {
addLog("warn", "⚠️ 未能重新校验登录态,请稍后重试或重新登录");
}
setAuthUI(false); setAuthUI(false);
}); });
} }
...@@ -146,6 +153,8 @@ async function runTest(apiUrl) { ...@@ -146,6 +153,8 @@ async function runTest(apiUrl) {
} catch { } catch {
addLog("err", "❌ 无法连接本地系统,请先启动招聘系统"); addLog("err", "❌ 无法连接本地系统,请先启动招聘系统");
} }
} finally {
resetBtn();
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment