/** * 验证「未登录访客由前端自己换 DMS token」这条链(用户 2026-09-29 要求)。 * * 验证什么(都打**真实服务**,经过 dev server 的同源代理): * ① 前端模块 `getServiceDmsToken()` 能经 `/oauth/...` 用服务账号换到 JWT; * ② 拿这个 token **自己带 `token` 头**请求 `/dms/...` 能取到数据; * ③ ⚠️ 关键:**代理不覆盖**前端带的 token —— 故意带一个假 token,DMS 必须拒绝 * (若代理仍无条件注入自己的 token,这里会错误地返回 200)。 * * 前置:先起 dev server(脚本会自己起,端口 8101)。自签证书 → 用 * `NODE_TLS_REJECT_UNAUTHORIZED=0` 跑(脚本内部已设置)。 * * 怎么跑: * npx esbuild harness/tools/_entry-dms-service-token.ts --bundle --format=esm \ * --outfile=harness/tools/_dms-service-token.mjs --alias:@=./src --define:import.meta.env='{}' * node harness/tools/verify-dms-service-token.mjs */ import { spawn } from 'node:child_process'; import { setTimeout as sleep } from 'node:timers/promises'; process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; const PORT = 8101; const ORIGIN = `https://localhost:${PORT}`; let pass = 0; let fail = 0; const check = (label, ok, extra = '') => { if (ok) { pass += 1; console.log(` ok ${label}${extra ? ` —— ${extra}` : ''}`); } else { fail += 1; console.log(` FAIL ${label}${extra ? ` —— ${extra}` : ''}`); } }; const dev = spawn('npx', ['vite', '--port', String(PORT), '--mode', 'development'], { cwd: process.cwd(), shell: true, stdio: 'ignore', }); const stopDev = () => { try { if (process.platform === 'win32') spawn('taskkill', ['/PID', String(dev.pid), '/T', '/F'], { shell: true }); else dev.kill('SIGTERM'); } catch { /* 尽力而为 */ } }; try { // 等 dev server 起来 for (let i = 0; i < 40; i += 1) { await sleep(1000); try { const res = await fetch(`${ORIGIN}/`); if (res.ok) break; } catch { /* 还没起来 */ } } // 模块在 node 里跑:相对路径没法直接 fetch,所以用 globalThis 覆盖成绝对地址 globalThis.VITE_OAUTH_LOGIN_URL = `${ORIGIN}/oauth`; const { getServiceDmsToken, clearServiceDmsToken } = await import('./_dms-service-token.mjs'); console.log('\n【1】前端模块换 token(经 /oauth 同源代理)'); let token = ''; try { token = await getServiceDmsToken(); } catch (error) { check('换 token 成功', false, error instanceof Error ? error.message : String(error)); } check('换 token 成功且是 JWT 形状', /^ey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\./.test(token), `长度 ${token.length}`); check('再次调用命中缓存(同一 token)', (await getServiceDmsToken()) === token); console.log('\n【2】带着这个 token 走 /dms 代理读写'); const dmsCall = async (tokenHeader) => { const res = await fetch(`${ORIGIN}/dms/content/selectContentList`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...(tokenHeader ? { token: tokenHeader } : {}) }, body: new URLSearchParams({ columnId: '1889', page: '1', size: '1' }).toString(), }); return res.json(); }; const okBody = await dmsCall(token); check('带真 token → code 200 且有数据', okBody?.code === 200 && (okBody?.content?.data?.length ?? 0) > 0, `code=${okBody?.code}`); console.log('\n【3】代理不覆盖前端带的 token(关键断言)'); const bogus = await dmsCall('this-is-not-a-valid-token'); check( '带假 token → DMS 拒绝(201/212),说明代理没有偷偷注入自己的 token', bogus?.code === 201 || bogus?.code === 212, `code=${bogus?.code} message=${String(bogus?.message ?? '').slice(0, 40)}` ); console.log('\n【4】不带 token → 仍由代理兜底注入(命令行走代理的排查方式)'); const viaProxy = await dmsCall(''); check('不带 token → 代理注入 `.dms-token` 后 200', viaProxy?.code === 200, `code=${viaProxy?.code}`); clearServiceDmsToken(); } finally { stopDev(); } console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`); process.exit(fail ? 1 : 0);