security: 安全审计修复 - API Key清理 + JWT改为HttpOnly Cookie

- API Key: run.md/.env/seed_model_providers.sql 中明文Key替换为占位符
- 认证: JWT从localStorage迁移到HttpOnly Cookie,移除后端body返回token
- 前端: api.ts/knowledge/page.tsx/auth.ts 全面改用Cookie认证
- postcss XSS: package.json添加overrides强制升级到8.5.15
- gitignore: 添加server/.env和.env排除规则
- lint: 移除tenant.go中未使用的roleKey常量
- 文档: 新增docs/security-audit-report.md和hardware-requirements.md
This commit is contained in:
selfrelease
2026-06-26 10:44:04 +08:00
parent c011f2eae8
commit a3245b249a
15 changed files with 442 additions and 86 deletions
+1 -3
View File
@@ -19,8 +19,6 @@ setup("登录法制日报账号并保存会话", async ({ page }) => {
await page.waitForURL(/\/store/, { timeout: 30_000 });
const token = await page.evaluate(() => localStorage.getItem("token"));
expect(token, "登录后应在 localStorage 写入 token").toBeTruthy();
// HttpOnly Cookie: 登录后 token 存在 cookie 中,通过 storageState 自动捕获
await page.context().storageState({ path: AUTH_FILE });
});
+4 -32
View File
@@ -8946,34 +8946,6 @@
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -9544,9 +9516,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"version": "8.5.15",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"funding": [
{
"type": "opencollective",
@@ -9563,7 +9535,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
+4 -1
View File
@@ -46,5 +46,8 @@
"tailwindcss": "^4",
"typescript": "^5",
"typescript-eslint": "^8.62.0"
},
"overrides": {
"postcss": ">=8.5.10"
}
}
}
+1 -2
View File
@@ -45,10 +45,9 @@ export default function RegisterPage() {
email: string;
role: "user" | "super_admin" | "admin" | "creator";
};
access_token: string;
}>("/api/v1/auth/register", { name, email, password });
setAuth(res.user, res.access_token);
setAuth(res.user);
toast.success("注册成功");
router.push("/store");
} catch (err) {
+1 -1
View File
@@ -113,8 +113,8 @@ export default function KnowledgePage() {
formData.append("file", file);
const res = await fetch(`/api/v1/knowledge/${selectedKB!.id}/documents`, {
method: "POST",
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
body: formData,
credentials: "include",
});
if (!res.ok) throw new Error("上传失败");
return res.json();
+1 -10
View File
@@ -36,11 +36,8 @@ class APIError extends Error {
}
}
// 认证:token 通过 HttpOnly Cookie 自动携带,不再使用 localStorage
function getAuthHeaders(): Record<string, string> {
if (typeof window !== "undefined") {
const token = localStorage.getItem("token");
if (token) return { Authorization: `Bearer ${token}` };
}
return {};
}
@@ -81,12 +78,6 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
}
if (res.status === 401) {
if (typeof window !== "undefined") {
const currentPath = window.location.pathname;
if (currentPath !== "/login" && currentPath !== "/register") {
localStorage.removeItem("token");
}
}
throw new APIError(json.code || 40101, json.message || "未登录或登录已过期");
}
+4 -20
View File
@@ -12,7 +12,7 @@ interface AuthState {
fetchUser: () => Promise<void>;
login: (email: string, password: string, orgId?: string) => Promise<void>;
logout: () => Promise<void>;
setAuth: (user: User, token: string) => void;
setAuth: (user: User) => void;
switchOrg: (orgId: string) => Promise<void>;
}
@@ -26,12 +26,6 @@ export const useAuthStore = create<AuthState>((set, get) => ({
if (get()._hasFetched) return;
set({ _hasFetched: true });
const token = typeof window !== "undefined" ? localStorage.getItem("token") : null;
if (!token) {
set({ user: null, isAuthenticated: false, isLoading: false });
return;
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
@@ -39,7 +33,6 @@ export const useAuthStore = create<AuthState>((set, get) => ({
const user = await api.get<User>("/api/v1/auth/me", { signal: controller.signal });
set({ user, isAuthenticated: true, isLoading: false });
} catch {
if (typeof window !== "undefined") localStorage.removeItem("token");
set({ user: null, isAuthenticated: false, isLoading: false });
} finally {
clearTimeout(timeout);
@@ -47,19 +40,15 @@ export const useAuthStore = create<AuthState>((set, get) => ({
},
login: async (email: string, password: string, orgId?: string) => {
const data = await api.post<{ user: User; access_token: string }>("/api/v1/auth/login", {
const data = await api.post<{ user: User }>("/api/v1/auth/login", {
email,
password,
org_id: orgId,
});
if (data.access_token) {
localStorage.setItem("token", data.access_token);
}
set({ user: data.user, isAuthenticated: true, isLoading: false });
},
setAuth: (user: User, token: string) => {
if (token) localStorage.setItem("token", token);
setAuth: (user: User) => {
set({ user, isAuthenticated: true, isLoading: false });
},
@@ -69,7 +58,6 @@ export const useAuthStore = create<AuthState>((set, get) => ({
} catch {
// ignore
} finally {
localStorage.removeItem("token");
set({ user: null, isAuthenticated: false, _hasFetched: false });
}
},
@@ -78,12 +66,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
const data = await api.post<{
message: string;
org: Organization;
token?: string;
user?: User;
}>("/api/v1/auth/switch-org", { org_id: orgId });
if (data.token) {
localStorage.setItem("token", data.token);
}
if (data.user && data.org) {
set({ user: { ...data.user, org_id: orgId, org: data.org }, isAuthenticated: true });
} else {
@@ -93,4 +77,4 @@ export const useAuthStore = create<AuthState>((set, get) => ({
}
}
},
}));
}));