Email Verification Protocol(电子邮件验证协议)让网站无需发送邮件即可确认用户是否控制某个电子邮件地址:浏览器从用户的电子邮件服务提供商处获取一个签名令牌,并随表单一起提交。
大多数注册流程仍会提示“请查收邮件”,而部分用户切换标签页去找验证码后就再也没有回来。Chrome 正在测试一种替代方案。Chrome 于 2026 年 7 月 8 日宣布的源试用(origin trial)从 Chrome 150 开始,此后 Chrome 又发布了 2026 年 8 月更新。协议本身由 IETF 互联网草案 draft-hardt-email-verification 定义。
这与 SMTP 或 MX 列表清洗不同。那些工具检查的是邮箱是否存在,而该协议证明的是填写表单的人已登录该邮箱。下文将介绍三方模型、表单标记、一个拆分为五个步骤的完整 Node.js 验证器,以及在试用期间可以在多大程度上依赖它。
要点速览
- Email Verification Protocol 返回一个带密钥绑定的 SD-JWT:由签发方签名的 Email Verification Token(EVT),接一个波浪号,再接一个由浏览器签名的密钥绑定 JWT(KB-JWT)。
- 浏览器位于你的网站与电子邮件服务提供商之间,因此提供商在签发令牌时无从得知是哪个网站发起的请求。
- 有效令牌证明的是该地址在电子邮件服务提供商处存在活跃会话,并不能证明发往该地址的邮件一定能送达。
- 该功能目前仍是一项提案,处于 Chrome 源试用阶段,有流量限制,Chrome 的博文中未给出结束日期。草案作者(而非 Chrome)在 IETF 126 的幻灯片中估计为三到六个月。Chrome 文档中列出的参与提供商仅有 Gmail。
- 如果隐藏的令牌字段为空,或任何验证步骤失败,请照常发送确认邮件。
浏览器邮箱验证取代的是什么?
浏览器邮箱验证取代的是那种把用户引离你网站的一次性验证码邮件或魔法链接(magic link)邮件。IETF 草案详细描述了验证码流程对用户的要求:离开网站打开邮件应用、等待邮件送达、可能还要翻找垃圾邮件、复制验证码,然后返回并粘贴。草案还指出,很多人会在中途放弃。魔法链接省去了复制粘贴这一步,但用户仍需前往收件箱等待邮件。
社交登录则完全跳过了邮件环节。草案指出了它的代价:用户必须在该提供商处拥有账户,而你必须为每个提供商单独构建一套集成。Email Verification Protocol 采用相同的原理(由提供商为地址背书),但通过浏览器实现,你的网站无需引入任何特定提供商的 SDK。
Email Verification Protocol 的工作原理:三方参与
Email Verification Protocol 涉及三方。你的网站是验证方(即依赖方,relying party)。浏览器位于你和签发方之间,签发方即运营用户电子邮件账户的服务。由于每次交互都经由浏览器中转,签发方永远不会知道是哪个网站请求了令牌。
Verifier (your site) <--> Browser <--> Issuer (e.g. accounts.google.com)
nonce, validation binds token checks session,
to your origin signs EVT
电子邮件服务提供商和签发方可以是不同的主机。在 Chrome 的示例中,提供商是 gmail.com,而签发方是 accounts.google.com。
在签发方一侧,邮件域名会发布一条 DNS TXT 记录,将验证委托给签发方。签发方提供 /.well-known/email-verification 元数据,通过 Login Status API 向浏览器报告登录状态,并在签名任何内容之前检查用户现有的第一方会话 Cookie(关于这些 Cookie 应如何配置,请参阅 Secure Cookies and HttpOnly)。草案还允许浏览器请求按站点区分的私密地址,EVT 会用 is_private_email: true 对其进行标记。Chrome 的试用文档并未介绍私密地址。草案的早期修订版允许签发方在没有有效 Cookie 时回退到 WebAuthn 质询,但 -02 修订版删除了该选项,因此现在没有会话的请求会直接失败。
网站端:表单标记与 Nonce
在你这一侧,表单需要两个输入框:一个带有 autocomplete="email" 的普通邮箱字段,以及一个带有 autocomplete="email-verification-token" 的隐藏字段,后者携带一个与用户会话绑定的 nonce。表单提交时,浏览器会将令牌填入该隐藏字段。以下是 Chrome 博文中的标记,其中 nonce 由服务端渲染:
<input
name="email-address"
type="email"
autocomplete="email">
<input
type="hidden"
name="token"
nonce="rAnD0m-VaLuE"
autocomplete="email-verification-token">
草案的会话绑定规则要求使用至少具有 128 位熵的随机 nonce。每次验证请求都应生成新的 nonce,并在短时间后使其过期。同时把渲染时间与它一起存储,因为验证器的第 2 步需要用到:
import { randomBytes } from "node:crypto";
const nonce = randomBytes(32).toString("base64url"); // 256 bits
req.session.evp = { nonce, renderedAt: Date.now() };
页面还需要包含你的源试用令牌。Chrome 的源试用文档说明了如何提供该令牌。根据 8 月更新,此功能不支持第三方源试用。
在服务端验证令牌
在服务端验证 Email Verification Protocol 令牌需要五个步骤:解析令牌、检查预期值、检查密钥绑定、检查 DNS 委托,以及验证签发方签名。表单值的格式为 <EVT>~<KB-JWT>。EVT 是由签发方签名的令牌,包含 email、email_verified、iss、iat 以及一个 cnf.jwk 浏览器密钥。KB-JWT 由浏览器签名,包含 aud、nonce、iat 和 sd_hash。在下面每一步都通过之前,不要信任任何解码出来的声明(claim)。代码使用 jose 和 Node 内置模块。每个代码块都运行在同一个 verifyEmailToken() 函数的 try 块中,一旦有任何异常抛出,函数即返回 false。
以下是这五个步骤所需的导入:
import { createHash } from "node:crypto";
import { resolveTxt } from "node:dns/promises";
import {
createRemoteJWKSet,
decodeJwt,
decodeProtectedHeader,
importJWK,
jwtVerify,
} from "jose";
1. 解析。 以波浪号拆分该值,并检查 typ 头部。
const [evt, kb, ...rest] = token.split("~");
if (!evt || !kb || rest.length) return false;
const evtHeader = decodeProtectedHeader(evt);
const kbHeader = decodeProtectedHeader(kb);
if (evtHeader.typ !== "evt+jwt" || kbHeader.typ !== "kb+jwt") return false;
const evtClaims = decodeJwt(evt);
const kbClaims = decodeJwt(kb);
目前的令牌不包含选择性披露(selective disclosure),因此该值可以干净地拆分为两部分。8 月更新指出这种简便做法较为脆弱,一旦将来加入披露内容就会失效。Chrome 建议使用 SD-JWT 库(如 @sd-jwt/core)解析令牌,Chrome 自己的演示现在也在使用该库。
2. 预期值。 Chrome 的验证步骤会检查 email_verified、email、nonce、aud,以及 iat 是否晚于表单渲染时间。比较邮箱时应不区分大小写:8 月更新提醒,提供商返回的地址在大小写上可能与用户输入的不同。
const { nonce, renderedAt } = req.session.evp ?? {};
delete req.session.evp; // single use
if (evtClaims.email_verified !== true) return false;
if (String(evtClaims.email).toLowerCase() !== submittedEmail.toLowerCase()) return false;
if (!nonce || kbClaims.nonce !== nonce) return false;
if (kbClaims.aud !== "https://example.com") return false;
if (kbClaims.iat < Math.floor(renderedAt / 1000)) return false;
// Any maximum age is your own policy; the sources define none.
3. 密钥绑定。 sd_hash 是 EVT(包括其末尾的 ~)的 SHA-256 哈希值。此外,KB-JWT 还必须能通过 cnf.jwk 中密钥的验证。
const hash = createHash("sha256").update(evt + "~").digest("base64url");
if (hash !== kbClaims.sd_hash) return false;
const holderKey = await importJWK(evtClaims.cnf.jwk, kbHeader.alg);
await jwtVerify(kb, holderKey, { typ: "kb+jwt" });
4. DNS 委托。 查询 _email-verification.<domain>。草案要求恰好存在一条 TXT 记录,且其值必须以 iss= 开头。可以使用 dig +short TXT _email-verification.gmail.com 手动检查某个域名。
const domain = submittedEmail.split("@").pop().toLowerCase();
const records = (await resolveTxt(`_email-verification.${domain}`))
.map((chunks) => chunks.join(""));
if (records.length !== 1 || !records[0].startsWith("iss=")) return false;
const issuer = records[0].slice(4);
5. 发现与 EVT 签名。 获取签发方元数据,然后使用其 jwks_uri 验证 EVT。草案将签发方标识符定义为 https:// 加上 DNS 记录中的主机名,EVT 的 iss 声明必须与之完全一致。
const meta = await (await fetch(`https://${issuer}/.well-known/email-verification`)).json();
const jwks = createRemoteJWKSet(new URL(meta.jwks_uri));
const { payload } = await jwtVerify(evt, jwks, {
algorithms: meta.signing_alg_values_supported,
typ: "evt+jwt",
}); // written so it does not depend on a kid header
// The draft's issuer identifier is "https://" plus the DNS host, compared byte for byte.
if (payload.iss !== `https://${issuer}`) return false;
return true;
| 检查项 | 涉及内容 | 失败时 |
|---|---|---|
| 解析 | ~ 拆分、typ 头部 | 发送确认邮件 |
| 预期值 | email_verified、email、nonce、aud、iat | 发送确认邮件 |
| 密钥绑定 | sd_hash、基于 cnf.jwk 的 KB-JWT 签名 | 发送确认邮件 |
| DNS | 唯一的 iss= TXT 记录 | 发送确认邮件 |
| 签发方签名 | 元数据、jwks_uri、iss | 发送确认邮件 |
Node DNS 模块会将每条 TXT 记录以分块数组的形式返回,这就是第 4 步需要将它们拼接起来的原因。
基于浏览器的邮箱验证有哪些局限?
Email Verification Protocol 检查通过,表明用户当前正以该地址登录其电子邮件服务提供商,但并不能说明你发往该地址的邮件一定能送达。Chrome 和草案都强调了这一点,所以请继续发送欢迎邮件。
用户填写表单所用的浏览器配置文件必须已经在提供商处保持登录会话,并且某个地址首次验证时,用户会看到一个权限提示,每个地址只提示一次。在 7 月的版本中,地址必须来自自动填充。根据 8 月更新,现在手动输入或粘贴地址后,字段失去焦点时也会触发验证。草案还指出了一个轻微的隐私泄露问题:令牌是否到达,会让你的网站知道用户是否已登录签发方。
现在该怎么做
Email Verification Protocol 目前还无法取代你的确认邮件。该浏览器 API 正在 WICG 中孵化,而协议本身是一份个人互联网草案(-02 修订版,2026 年 8 月)。Chrome 在试用期间限制了流量,以防网站在功能正式发布前就依赖它进行构建。Chrome 的博文没有给出结束日期。三到六个月的估计来自草案作者在 IETF 126 DISPATCH 幻灯片中的表述,而非来自 Chrome。Chrome 还警告可能会出现不向后兼容的变更,8 月更新指出,截至 Chrome 152,该功能仅支持桌面端,Android 支持仍在探索中。Chrome 文档中列出的参与提供商仅有 Gmail。草案还将 Hellō 列为已实现的签发方,并提到了 Microsoft Edge 的源试用。
令牌签发方式已经变更过一次。7 月的博文描述的是一种表单编码的 request_token JWT。草案用 HTTP Message Signatures(RFC 9421)取代了它,并将旧格式标记为已弃用。从 Chrome 153 开始,Chrome 会发送 application/json,附带 Signature、Signature-Input 和 Signature-Key 头部,请求体仅包含电子邮件地址。这是签发方一侧的变更,不会影响你的验证器代码。
请继续将 OTP 或魔法链接流程作为默认路径。如果你加入试用,应将令牌视为渐进增强:验证通过时,跳过邮件环节。验证完成后,按照你惯常的方式创建会话(请参阅 Cookies vs localStorage for JWT Auth)。EVT+KB 是一次性凭证,不应用作会话令牌。
该协议用一次在你自己服务器上运行的签名校验,取代了“去查收邮件”这一环节,而验证器只需五个步骤。添加隐藏输入框和 nonce,将 verifyEmailToken() 部署在现有确认邮件流程之前,并在每次 Chrome 发布前查阅 Chrome 更新博文以了解变更。
常见问题
Email Verification Protocol 与「使用 Google 账号登录」有何不同?
「使用 Google 账号登录」是针对单个提供商的登录集成,会在你的网站与 Google 之间建立身份关联。Email Verification Protocol 只确认用户控制某个电子邮件地址。它不需要在你的网站上引入任何提供商 SDK,而且由于交互由浏览器中转,签发方在签名令牌时无从得知是哪个网站发起的请求。
可以使用 crypto.randomUUID() 生成 nonce 吗?
不可以。版本 4 的 UUID 只包含 122 个随机位,因为版本和变体字段占用了其余 6 位。IETF 草案要求 nonce 至少具有 128 位熵。请改用 node:crypto 中的 randomBytes(32).toString('base64url'),它可提供 256 位熵。将 nonce 存储在服务端会话中,并在使用一次后删除。
Email Verification Protocol 是否适用于自定义域名或 Gmail 以外提供商的地址?
仅当该邮件域名发布了恰好一条以 iss= 开头的 _email-verification TXT 记录,并将验证委托给支持该协议的签发方时才适用。Chrome 文档中列出的参与电子邮件服务提供商为 Gmail。对于任何没有该记录的其他域名,浏览器无法获取令牌,因此你的验证器应将其视为检查失败,并发送常规确认邮件。