feat(vNext.4): implement trusted-subject security chain for portal user key self-service
- Add portal_auth.go: Portal user session auth with HMAC-signed cookies
- Add /api/portal/session/{login,logout,state} endpoints
- Update nginx config template: cookie-to-header trusted proxy pattern
- Update frontend: sync CRM session on login/logout
- Add TRUSTED_SUBJECT_DEPLOY_GUIDE.md with remote43 deployment steps
- Update EXECUTION_BOARD.md: mark trusted-subject blocking issue as resolved
This implements the secure chain:
Browser → Portal → nginx (cookie→header) → CRM (verify proxy secret)
Required remote43 actions:
1. Generate 64-char hex secret
2. Update .env.crm with TRUSTED_* config
3. Update nginx with cookie map and header injection
4. Restart services
Fixes EXECUTION_BOARD.md 2026-06-08 blocking issue
This commit is contained in:
@@ -508,7 +508,14 @@
|
||||
$("access-token").value = state.accessToken;
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
async function clearSession() {
|
||||
// 同时登出 CRM session(清除 httpOnly cookie)
|
||||
try {
|
||||
await requestControlPlane("/portal/session/logout", { method: "POST" });
|
||||
} catch (e) {
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
state.accessToken = "";
|
||||
state.user = null;
|
||||
state.groups = [];
|
||||
@@ -574,11 +581,6 @@
|
||||
|
||||
async function requestControlPlane(path, options = {}) {
|
||||
const headers = Object.assign({ Accept: "application/json" }, options.headers || {});
|
||||
const subjectID = portalSubjectID();
|
||||
if (!subjectID) {
|
||||
throw new Error("当前缺少 portal subject,请先登录");
|
||||
}
|
||||
headers["X-Portal-Subject"] = subjectID;
|
||||
const res = await fetch("/portal-admin-api/api" + path, {
|
||||
method: options.method || "GET",
|
||||
headers,
|
||||
@@ -1220,7 +1222,7 @@
|
||||
renderCurlExample($("api-key").value.trim(), selectedLogicalGroupRow());
|
||||
renderAll();
|
||||
} catch (err) {
|
||||
clearSession();
|
||||
await clearSession();
|
||||
statusPill("bad", "登录失效");
|
||||
setStatus("auth-status", "bad", "会话已失效,请重新登录:" + err.message);
|
||||
}
|
||||
@@ -1242,11 +1244,24 @@
|
||||
setBusy("auth-btn", true);
|
||||
setStatus("auth-status", "", "正在验证账号…");
|
||||
try {
|
||||
// 先尝试登录
|
||||
// 先尝试登录宿主
|
||||
const data = await requestJSON("/auth/login", "POST", {
|
||||
email, password, turnstile_token: ""
|
||||
}, false);
|
||||
rememberAuth(data);
|
||||
|
||||
// 同时登录 CRM session(设置 httpOnly cookie 供 user-key API 使用)
|
||||
try {
|
||||
await requestControlPlane("/portal/session/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
} catch (crmErr) {
|
||||
console.warn("CRM session login failed:", crmErr.message);
|
||||
// 不影响宿主导航,继续
|
||||
}
|
||||
|
||||
setStatus("auth-status", "ok", "登录成功,正在同步你的账号状态与申请资格。");
|
||||
await refreshUserState();
|
||||
} catch (loginErr) {
|
||||
@@ -1341,8 +1356,8 @@
|
||||
|
||||
$("create-key-btn").addEventListener("click", handleCreateKey);
|
||||
$("refresh-session-btn").addEventListener("click", refreshUserState);
|
||||
$("logout-btn").addEventListener("click", () => {
|
||||
clearSession();
|
||||
$("logout-btn").addEventListener("click", async () => {
|
||||
await clearSession();
|
||||
statusPill("warn", "已退出");
|
||||
});
|
||||
$("auth-btn").addEventListener("click", handleAuth);
|
||||
|
||||
@@ -6,6 +6,17 @@
|
||||
# - /portal-proxy/ 是页面调用宿主用户态 API 的同域代理
|
||||
# - /portal-admin-api/ 是页面调用 CRM 管理 API 的同域代理
|
||||
# - /kimi/ 与 /kimi-v1/ 继续保留,兼容旧的 Kimi 专用客户端配置
|
||||
#
|
||||
# 安全注意事项:
|
||||
# - portal-subject 从 cookie 提取,由后端 /api/portal/session/login 设置 httpOnly cookie
|
||||
# - CRM 验证 X-CRM-Trusted-Proxy header 确保请求来自受信 nginx
|
||||
# - 两者必须同时配置才能启用 user-key self-service
|
||||
|
||||
# 从 httpOnly cookie 提取 portal subject
|
||||
map $http_cookie $portal_subject {
|
||||
default "";
|
||||
~*crm_session=([^;]+) $1;
|
||||
}
|
||||
|
||||
location = /portal {
|
||||
return 302 /portal/;
|
||||
@@ -36,11 +47,20 @@ location /portal-proxy/ {
|
||||
}
|
||||
|
||||
location /portal-admin-api/ {
|
||||
# 必须由受信登录/鉴权层把用户 subject 放进 $portal_subject,不能信任浏览器自带 header。
|
||||
# 同时 CRM 需配置:
|
||||
# SUB2API_CRM_TRUSTED_SUBJECT_HEADER=X-CRM-Authenticated-Subject
|
||||
# SUB2API_CRM_TRUSTED_PROXY_SECRET_HEADER=X-CRM-Trusted-Proxy
|
||||
# SUB2API_CRM_TRUSTED_PROXY_SECRET=<same-secret-as-nginx>
|
||||
proxy_pass http://127.0.0.1:18190/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# 关键:从验证过的 cookie 提取并注入 subject
|
||||
proxy_set_header X-CRM-Authenticated-Subject $portal_subject;
|
||||
# 受信代理密钥(必须与 CRM 配置一致)
|
||||
proxy_set_header X-CRM-Trusted-Proxy "REPLACE_WITH_64_CHAR_HEX_SECRET";
|
||||
proxy_http_version 1.1;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user