Skip to content

Commit 1a485d6

Browse files
committed
feat(auth): 登录成功同步 satoken 到 .involutionhell.com cookie
配合后端 /api/admin/pgadmin-check 和 Caddy forward_auth 的整条链:用户直连 api.involutionhell.com/admin/pgadmin/* 时浏览器不会主动发 satoken header, 必须靠 cookie 自动携带。 - 新加 syncTokenCookie(token):登录 / 刷新有效 session / 登出全部打点 localhost 域不写 Domain(浏览器默认绑当前 host); 生产写 Domain=.involutionhell.com 让主域 + 所有子域共享 SameSite=Lax 刚好够——顶层导航 / 子资源 GET 都会带;跨站 POST 不带但我们 也不需要(pgAdmin 的 CSRF 有自己的 cookie) Max-Age=2592000 与 sa-token.timeout 保持一致 - token 无效 / 登出时清掉 cookie,避免 stale 身份残留 服务端配套:InvolutionHell/involutionhell-backend#12
1 parent f913232 commit 1a485d6

1 file changed

Lines changed: 38 additions & 2 deletions

File tree

lib/use-auth.tsx

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,37 @@ function getStoredToken(): string | null {
3636
return localStorage.getItem("satoken");
3737
}
3838

39+
/**
40+
* 把 satoken 同步写一份到 `.involutionhell.com` 域名的 cookie。
41+
*
42+
* 为什么需要:直接访问 api.involutionhell.com/admin/pgadmin/*(新标签页打开 pgAdmin)
43+
* 时浏览器不会主动发 `satoken` header——业务 API 是走 Next.js rewrite 同源的,所以
44+
* 前端能手动附 header;但新标签直连 api 子域就只能靠 cookie 自动带。
45+
*
46+
* Caddy 在 api 子域前放了 forward_auth 钩子调后端 /api/admin/pgadmin-check,
47+
* sa-token 默认从 cookie 读 token(sa-token.is-read-cookie=true 默认开),
48+
* 只要 cookie 存在且对应 satoken 在后端会话库里且拥有 admin 角色就放行。
49+
*
50+
* 本地开发(localhost)的 Domain 属性要留空,浏览器会默认绑当前 host;
51+
* 生产(involutionhell.com + api.involutionhell.com)要显式写 `.involutionhell.com`
52+
* 否则两个子域 cookie 各存各的。
53+
*/
54+
function syncTokenCookie(token: string | null) {
55+
if (typeof document === "undefined") return;
56+
const isLocalhost =
57+
window.location.hostname === "localhost" ||
58+
window.location.hostname === "127.0.0.1";
59+
const domainAttr = isLocalhost ? "" : "; Domain=.involutionhell.com";
60+
const secureAttr = window.location.protocol === "https:" ? "; Secure" : "";
61+
if (token) {
62+
// 30 天 TTL 和 sa-token 服务端配置保持一致(application.properties 里 2592000)
63+
document.cookie = `satoken=${encodeURIComponent(token)}; Path=/${domainAttr}; Max-Age=2592000; SameSite=Lax${secureAttr}`;
64+
} else {
65+
// 清除:设空值并 Max-Age=0;Domain / Path 必须与写入时一致浏览器才认这是"同一条"
66+
document.cookie = `satoken=; Path=/${domainAttr}; Max-Age=0; SameSite=Lax${secureAttr}`;
67+
}
68+
}
69+
3970
// 调用后端 /auth/me 验证 token 并获取用户信息
4071
// 走 Next.js rewrite(/auth/* → 后端),浏览器无跨域问题
4172
async function fetchCurrentUser(token: string): Promise<UserView | null> {
@@ -63,8 +94,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
6394
const urlToken = hashParams.get("token");
6495

6596
if (urlToken) {
66-
// 存入 localStorage
97+
// 存入 localStorage + 同步写 cookie(供 api 子域直连场景使用,比如 pgAdmin)
6798
localStorage.setItem("satoken", urlToken);
99+
syncTokenCookie(urlToken);
68100
// 用 replaceState 清除 URL 中的 fragment,避免刷新或分享时 token 泄露
69101
hashParams.delete("token");
70102
const newHash = hashParams.toString();
@@ -87,9 +119,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
87119
if (u) {
88120
setUser(u);
89121
setStatus("authenticated");
122+
// 已登录用户每次刷新也重写 cookie,覆盖掉可能过期 / 丢失的副本
123+
syncTokenCookie(token);
90124
} else {
91-
// token 无效或已过期,清除
125+
// token 无效或已过期,localStorage + cookie 都清
92126
localStorage.removeItem("satoken");
127+
syncTokenCookie(null);
93128
setStatus("unauthenticated");
94129
}
95130
});
@@ -110,6 +145,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
110145
}
111146
localStorage.removeItem("satoken");
112147
}
148+
syncTokenCookie(null);
113149
setUser(null);
114150
setStatus("unauthenticated");
115151
};

0 commit comments

Comments
 (0)