跳转到内容
本站为社区维护的非官方中文翻译,与 Cloudflare, Inc. 无关联。本站不提供账户登录,请勿在任何非官方站点输入您的 Cloudflare 账户信息。官方文档:developers.cloudflare.com

为 HTML 页面加水印

最后更新 查看 MarkdownAgent 设置

为您控制的应用程序中的 HTML 页面添加可见的用户标签。Worker 检索 Gateway 身份,并通过 HTMLRewriter 流式处理响应。

配置 Worker

在 Worker 项目中,将 @cloudflare/zt-types ↗ 2026.10.1 或更高版本安装为开发依赖项:

npm i -D @cloudflare/zt-types

保留 src/index.ts 中 GatewayMiddlewareContext 和 GatewayMiddlewareNext 的 import type 声明。它们描述 Gateway 上下文和延续函数,并会在 TypeScript 编译时被移除。从 cloudflare:workers 导入运行时 WorkerEntrypoint 类。

将以下脚本用作 Worker 项目中的 src/index.ts。它不需要绑定(binding)。

src/index.jsjs
import { WorkerEntrypoint } from "cloudflare:workers";

function escapeHtml(text) {
	return text
		.replaceAll("&", "&")
		.replaceAll("<", "&lt;")
		.replaceAll(">", "&gt;")
		.replaceAll('"', "&quot;")
		.replaceAll("'", "&#39;");
}

export class GatewayMiddleware extends WorkerEntrypoint {
	async handle(request, context, next) {
		const response = await next(request);
		const contentType = response.headers.get("content-type") ?? "";
		if (
			request.method !== "GET" ||
			response.status !== 200 ||
			response.headers.has("content-encoding") ||
			!response.body ||
			!/^text\/html\b/i.test(contentType)
		) {
			return response;
		}
		const identity = await context.identity().catch(() => null);
		const label = escapeHtml(identity?.email ?? "Unidentified user");
		const headers = new Headers(response.headers);
		for (const name of [
			"content-length",
			"etag",
			"content-md5",
			"digest",
			"content-digest",
			"repr-digest",
		]) {
			headers.delete(name);
		}
		headers.set("cache-control", "private, no-store");
		const rewritten = new HTMLRewriter()
			.on("body", {
				element(element) {
					element.prepend(
						`<aside style="position:fixed;bottom:1rem;right:1rem;z-index:2147483647;pointer-events:none">Viewed by ${label}</aside>`,
						{ html: true },
					);
				},
			})
			.transform(
				new Response(response.body, { status: response.status, headers }),
			);
		return rewritten;
	}
}
src/index.tsts
import { WorkerEntrypoint } from "cloudflare:workers";
import type {
	GatewayMiddlewareContext,
	GatewayMiddlewareNext,
} from "@cloudflare/zt-types";

function escapeHtml(text: string): string {
	return text
		.replaceAll("&", "&amp;")
		.replaceAll("<", "&lt;")
		.replaceAll(">", "&gt;")
		.replaceAll('"', "&quot;")
		.replaceAll("'", "&#39;");
}

export class GatewayMiddleware extends WorkerEntrypoint {
	async handle(
		request: Request,
		context: GatewayMiddlewareContext,
		next: GatewayMiddlewareNext,
	): Promise<Response> {
		const response = await next(request);
		const contentType = response.headers.get("content-type") ?? "";
		if (
			request.method !== "GET" ||
			response.status !== 200 ||
			response.headers.has("content-encoding") ||
			!response.body ||
			!/^text\/html\b/i.test(contentType)
		) {
			return response;
		}
		const identity = await context.identity().catch(() => null);
		const label = escapeHtml(identity?.email ?? "Unidentified user");
		const headers = new Headers(response.headers);
		for (const name of [
			"content-length",
			"etag",
			"content-md5",
			"digest",
			"content-digest",
			"repr-digest",
		]) {
			headers.delete(name);
		}
		headers.set("cache-control", "private, no-store");
		const rewritten = new HTMLRewriter()
			.on("body", {
				element(element) {
					element.prepend(
						`<aside style="position:fixed;bottom:1rem;right:1rem;z-index:2147483647;pointer-events:none">Viewed by ${label}</aside>`,
						{ html: true },
					);
				},
			})
			.transform(
				new Response(response.body, { status: response.status, headers }),
			);
		return rewritten;
	}
}

该脚本在将用户标签插入 HTML 之前会对其进行转义。它会移除过期的长度和完整性标头,并将个性化响应标记为不可缓存。其他响应标头(包括 Content Security Policy)会被保留。

此模板有意允许匿名流量。当身份为 null 或 Gateway 无法检索身份时,它会使用 Unidentified user。

转发并测试

部署 Worker,并为测试应用程序的 HTML 页面以及测试用户组创建 Forward 策略。访问该页面,确认标签包含您的 Gateway 身份电子邮件。测试图像或 JSON 端点,确认其响应体未发生变化。

页面的 Content Security Policy 可能阻止内联样式生效。标签仍会被插入,但其外观取决于页面。请在您自己的应用程序中配置样式,而不是削弱其安全策略。客户端渲染也可能移除注入的元素。

此模板仅更改状态为 200 的未压缩 HTML 响应。请将测试源配置为提供未压缩 HTML。它不会为 PDF 文件、图像或其他二进制下载加水印,并且可见标签可以被用户移除。在扩大策略范围之前,请测试身份验证流程和应用程序渲染。

这篇文档对您有帮助吗?