传统邮箱验证通常采用 Double Opt-In:用户提交邮箱后,服务端发送验证邮件,用户切换到邮箱客户端打开邮件,再点击链接返回网站完成验证。这个流程虽然可靠,但存在明显的行为断点。新的 Email Verification API 试图让浏览器在用户不离开当前页面的情况下完成邮箱所有权验证。
它的核心工作流程是:
- 用户填写或选择邮箱地址
- 浏览器根据邮箱域名发现对应的邮箱服务,并检查用户是否已登录该邮箱服务
-
邮箱服务向浏览器签发经过密码学签名的 Email Verification Token(EVT)
-
先根据邮箱域名通过 DNS 找到对应的验证服务,再获取其验证元数据和公钥。
-> % dig +noall +answer txt _email-verification.gmail.com _email-verification.gmail.com. 3592 IN TXT "iss=accounts.google.com" -> % curl https://accounts.google.com/.well-known/email-verification { "issuance_endpoint": "https://accounts.google.com/gsi/email-verification/issue", "jwks_uri": "https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks", "signing_alg_values_supported": ["EdDSA"] }issuance_endpoint: 浏览器向哪里请求 Email Verification Tokenjwks_uri: issuer 的公钥在哪里,用于验证签发的 Tokensigning_alg_values_supported: 支持的签名算法
-
浏览器生成临时密钥对
-
浏览器构造签发请求。Chrome 153 起不再使用
request_tokenJWT,而是对 HTTP 请求做 HTTP Message SignaturesPOST /gsi/email-verification/issue HTTP/1.1 Host: accounts.google.com Accept: application/json Content-Type: application/json Content-Digest: sha-256=:...: Sec-Fetch-Dest: email-verification Signature-Input: sig=("@method" "@authority" "@path" "content-digest" "signature-key");created=1786455840 Signature: sig=:...: Signature-Key: sig=hwk;crv="Ed25519";kty="OKP";x="..." {"email":"alice@gmail.com"}签名覆盖
@method、@authority、@path、content-digest、signature-key。Content-Digest是请求体的 SHA-256,用来把邮箱地址绑进签名。浏览器临时公钥放在Signature-Key的hwk参数里,不放进 JWT header。
-
-
浏览器将 EVT 与网站的 origin 以及服务端生成的一次性 nonce 绑定,并将绑定后的 token 自动写入表单
-
服务端收到邮箱和验证 Token 后,
-
先根据邮箱域名通过 DNS 找到对应的验证服务,再获取其验证元数据和公钥。
-> % dig +noall +answer txt _email-verification.gmail.com _email-verification.gmail.com. 3592 IN TXT "iss=accounts.google.com" -> % curl https://accounts.google.com/.well-known/email-verification { "issuance_endpoint": "https://accounts.google.com/gsi/email-verification/issue", "jwks_uri": "https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks", "signing_alg_values_supported": ["EdDSA"] }issuance_endpoint: 浏览器向哪里请求 Email Verification Tokenjwks_uri: issuer 的公钥在哪里,用于验证签发的 Tokensigning_alg_values_supported: 支持的签名算法
-
随后解析 Token,验证签名及邮箱地址、网站 Origin、nonce 等绑定信息,
- 并检查 nonce 是否与当前 Session 匹配且未被使用。
- 全部通过后,服务器将邮箱标记为已验证。
-
其中,nonce 由服务端生成并与当前 Session 绑定,每次页面渲染都应使用新的随机值。它会被绑定到最终的验证 token 中,用于防止 token 被截获后重放到其他请求或上下文。
例如用户在 Chrome 中登录了 Gmail,同时注册表单填写的也是该 Gmail 地址,浏览器可以协调 Gmail 完成身份确认,并将验证 token 写入隐藏字段。整个过程中不需要发送传统的验证邮件,也不需要用户点击邮件中的链接。
前端只需要增加一个隐藏字段:
<input type="email" name="email" autocomplete="email" />
<input
type="hidden"
name="token"
autocomplete="email-verification-token"
nonce="server-generated-random-value"
/>
因此,这套机制本质上并不是“浏览器替网站发送一封验证邮件”,而是建立了一条新的邮箱所有权证明链路:邮箱服务负责证明用户控制该邮箱,浏览器负责协调和绑定证明,网站服务器最终独立验证证明。
目前该 API 仍处于早期的草案阶段,实际支持范围有限。因此产品层面不应该直接替换传统邮件验证,而应该采用渐进增强(Progressive Enhancement):支持该机制的环境直接完成验证,不支持时继续回退到传统邮件验证。
它最大的价值,是把原本“网站 → 邮箱 → 用户 → 网站”的跨应用验证流程,变成“邮箱服务 → 浏览器 → 网站”的密码学证明流程,从而减少用户操作,同时降低无效邮箱带来的验证和投递成本。
脚本请求签发端点时的 403
浏览器外复现签发请求时,第六步 HTTP Error 403: Forbidden 不是 Cookie 写错,而是请求在进到签发逻辑之前就被 Google 前端拒绝了。规范里签名失败应返回 400,会话无效应返回 401;直接 403 说明打到了错误路径,或者客户端不像浏览器。
有三处必须同时改:
- 元数据请求失败时不能回退到
https://accounts.google.com/email-verification/issuance。Gmail 的真实地址是https://accounts.google.com/gsi/email-verification/issue。不存在的路径在accounts.google.com上经常直接 403,而不是 404。 urllib默认User-Agent: Python-urllib/...。带登录 Cookie 访问 Google 账号域时,这个标识会被前端 403。请求要带浏览器 User-Agent,并且带上Sec-Fetch-Dest: email-verification。- Chrome 153(2026-08)起,签发请求不再是
application/x-www-form-urlencoded的request_token。请求体改成{"email":"..."},并用 HTTP Message Signatures 签名。签名基字符串的每一行覆盖组件都以 LF 结尾,最后一行@signature-params不加换行。Content-Digest和Signature用普通 Base64(带填充),公钥x用 Base64url(不带填充)。
Cookie 过期或邮箱与登录账号不一致时,修正后应看到 JSON authentication_required(401),而不是没有正文的 403。