多租户定制化HTML输出必须禁用静态缓存并按X-Tenant-ID做缓存分片,且响应头强制设置Vary: X-Tenant-ID,否则会导致租户越权可见;API路径须由网关注入响应头,data-*属性仅限展示层使用;模板变量需作用域隔离,禁止内联脚本,HTMLHint规则须按租户分级配置。

多租户定制化 HTML 输出必须禁用静态缓存,并按 X-Tenant-ID 做缓存分片,否则租户 A 的页面被 CDN 缓存后,租户 B 刷新就看到 A 的菜单、logo 和数据入口——这不是样式错乱,是越权可见。
为什么 Vary: X-Tenant-ID 是硬性要求
CDN 或反向代理缓存默认只认 URL,/dashboard 对所有租户都一样。不加 Vary: X-Tenant-ID,缓存层根本不知道该为不同租户存多份副本。
- 必须在 SSR 响应头中显式设置:
Vary: X-Tenant-ID(推荐)或Vary: Tenant-Slug - 若用 Edge/CDN,需配置缓存 key 包含
X-Tenant-ID值,不能只靠路径 -
Cache-Control: public与data-tenant-属性共存会直接导致缓存污染,此时应设为private, max-age=0, no-cache
data-* 属性只能用于展示层,不能参与任何运行时逻辑
把 data-api-base="/api/v1/acme" 写进 <body>,再让 JS 拼接请求地址,等于把租户上下文控制权交到前端——用户改 DOM 就能调其他租户接口。
- API 基础路径必须由网关注入响应头(如
X-Api-Base),JS 通过document.querySelector('meta[name="api-base"]')安全读取 -
data-tenant-slug只允许用于 CSS 主题切换、埋点打标或调试日志,禁止出现在fetch()、location.href、router.push()中 - SSR 渲染时可输出
data-theme="acme",但 JS 不据此构造 URL 或切换功能开关
EJS 模板中租户变量必须作用域隔离
locals 是共享对象,<% tenant = locals.tenant %> 在 layout.ejs 里声明后,后续 partial 若再定义同名变量会覆盖,导致权限判断失效或主题类名错乱。
立即学习“前端免费学习笔记(深入)”;
- 模板内禁止直接赋值同名变量,应始终消费传入参数,例如
<%= tenantSettingsUrl %> - 租户相关过滤器(如
tenantLogoUrl)必须注册为函数,而非模板内写三元表达式:<%= tenantLogoUrl(tenant) %> - href、src 等属性禁止拼接:
href="/t/<%= tenant.slug %>/settings"→ 应由后端生成完整 URL 并传入变量
HTMLHint 规则必须按租户分级配置
某租户要求保留 <font> 标签做邮件模板兼容,但全局启用 font-face-require 规则会导致 CI 失败;盲目启用全部规则只会卡住交付节奏。
- 基础规则(
doctype-html5、tag-pair、id-unique)全局强制启用 - 租户专属规则在
.htmlhintrc中按目录隔离:"pages/tenant-acme/**": { "attr-no-duplication": false } - 禁止在租户模板中使用内联
<script>,所有 JS 必须外链并带integrity属性
真正难的不是写对一行 data-tenant-id,而是确保它从网关头进入、在 SSR 中注入、被浏览器忽略、被 JS 忽略、被缓存层识别、被 HTMLHint 放行——每个环节断掉,都会变成线上事故。



















