verify-dms-service-token.mjs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. /**
  2. * 验证「未登录访客由前端自己换 DMS token」这条链(用户 2026-09-29 要求)。
  3. *
  4. * 验证什么(都打**真实服务**,经过 dev server 的同源代理):
  5. * ① 前端模块 `getServiceDmsToken()` 能经 `/oauth/...` 用服务账号换到 JWT;
  6. * ② 拿这个 token **自己带 `token` 头**请求 `/dms/...` 能取到数据;
  7. * ③ ⚠️ 关键:**代理不覆盖**前端带的 token —— 故意带一个假 token,DMS 必须拒绝
  8. * (若代理仍无条件注入自己的 token,这里会错误地返回 200)。
  9. *
  10. * 前置:先起 dev server(脚本会自己起,端口 8101)。自签证书 → 用
  11. * `NODE_TLS_REJECT_UNAUTHORIZED=0` 跑(脚本内部已设置)。
  12. *
  13. * 怎么跑:
  14. * npx esbuild harness/tools/_entry-dms-service-token.ts --bundle --format=esm \
  15. * --outfile=harness/tools/_dms-service-token.mjs --alias:@=./src --define:import.meta.env='{}'
  16. * node harness/tools/verify-dms-service-token.mjs
  17. */
  18. import { spawn } from 'node:child_process';
  19. import { setTimeout as sleep } from 'node:timers/promises';
  20. process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
  21. const PORT = 8101;
  22. const ORIGIN = `https://localhost:${PORT}`;
  23. let pass = 0;
  24. let fail = 0;
  25. const check = (label, ok, extra = '') => {
  26. if (ok) {
  27. pass += 1;
  28. console.log(` ok ${label}${extra ? ` —— ${extra}` : ''}`);
  29. } else {
  30. fail += 1;
  31. console.log(` FAIL ${label}${extra ? ` —— ${extra}` : ''}`);
  32. }
  33. };
  34. const dev = spawn('npx', ['vite', '--port', String(PORT), '--mode', 'development'], {
  35. cwd: process.cwd(),
  36. shell: true,
  37. stdio: 'ignore',
  38. });
  39. const stopDev = () => {
  40. try {
  41. if (process.platform === 'win32') spawn('taskkill', ['/PID', String(dev.pid), '/T', '/F'], { shell: true });
  42. else dev.kill('SIGTERM');
  43. } catch {
  44. /* 尽力而为 */
  45. }
  46. };
  47. try {
  48. // 等 dev server 起来
  49. for (let i = 0; i < 40; i += 1) {
  50. await sleep(1000);
  51. try {
  52. const res = await fetch(`${ORIGIN}/`);
  53. if (res.ok) break;
  54. } catch {
  55. /* 还没起来 */
  56. }
  57. }
  58. // 模块在 node 里跑:相对路径没法直接 fetch,所以用 globalThis 覆盖成绝对地址
  59. globalThis.VITE_OAUTH_LOGIN_URL = `${ORIGIN}/oauth/api/user/login`;
  60. const { getServiceDmsToken, clearServiceDmsToken } = await import('./_dms-service-token.mjs');
  61. console.log('\n【1】前端模块换 token(经 /oauth 同源代理)');
  62. let token = '';
  63. try {
  64. token = await getServiceDmsToken();
  65. } catch (error) {
  66. check('换 token 成功', false, error instanceof Error ? error.message : String(error));
  67. }
  68. check('换 token 成功且是 JWT 形状', /^ey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\./.test(token), `长度 ${token.length}`);
  69. check('再次调用命中缓存(同一 token)', (await getServiceDmsToken()) === token);
  70. console.log('\n【2】带着这个 token 走 /dms 代理读写');
  71. const dmsCall = async (tokenHeader) => {
  72. const res = await fetch(`${ORIGIN}/dms/content/selectContentList`, {
  73. method: 'POST',
  74. headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...(tokenHeader ? { token: tokenHeader } : {}) },
  75. body: new URLSearchParams({ columnId: '1889', page: '1', size: '1' }).toString(),
  76. });
  77. return res.json();
  78. };
  79. const okBody = await dmsCall(token);
  80. check('带真 token → code 200 且有数据', okBody?.code === 200 && (okBody?.content?.data?.length ?? 0) > 0, `code=${okBody?.code}`);
  81. console.log('\n【3】代理不覆盖前端带的 token(关键断言)');
  82. const bogus = await dmsCall('this-is-not-a-valid-token');
  83. check(
  84. '带假 token → DMS 拒绝(201/212),说明代理没有偷偷注入自己的 token',
  85. bogus?.code === 201 || bogus?.code === 212,
  86. `code=${bogus?.code} message=${String(bogus?.message ?? '').slice(0, 40)}`
  87. );
  88. console.log('\n【4】不带 token → 仍由代理兜底注入(命令行走代理的排查方式)');
  89. const viaProxy = await dmsCall('');
  90. check('不带 token → 代理注入 `.dms-token` 后 200', viaProxy?.code === 200, `code=${viaProxy?.code}`);
  91. clearServiceDmsToken();
  92. } finally {
  93. stopDev();
  94. }
  95. console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
  96. process.exit(fail ? 1 : 0);