跳转到内容

Credential Management API(凭据管理接口)

发布于 更新于

一句话: 根据 MDN 的说明,Credential Management API “使网站能够创建、存储和获取 凭据”(“enables a website to create, store, and retrieve credentials”),核心接口是 CredentialsContainer,通过 navigator.credentials 暴露,并且只在安全上下文 (HTTPS)中可用。

MDN 将凭据描述为“使系统能够做出身份验证决定的项目”——用户向网站出示的、用以证明 自己身份的凭证。核心接口 CredentialsContainer 提供三个主要方法:create() (创建新凭据)、store()(在本地存储新凭据)以及 get()(获取凭据以登录用户)。

CredentialsContainer 的参考文档在这三个方法之外还列出了第四个实例方法,而它恰恰 最容易被忽略:preventSilentAccess()「设置一个标志,用于指定后续访问当前源时是否 允许自动登录,然后返回一个空的 Promise」。这四个方法都仅在安全上下文中可用。

这三个主要方法最好按它们各自 resolve 出什么来理解,因为形态并不相同:

  • create() 以依据所给选项创建的新 Credential 实例 resolve——或者在无法创建 任何 Credential 对象时以 null resolve。
  • get() 以匹配所给参数的 Credential 实例 resolve。如果无法无歧义地取得单一 凭据,则以 null resolve。
  • store() 把一组用户凭据存入所提供的 Credential 实例,并在 Promise 中返回 该实例。

根据 MDN,该 API 支持四种凭据类型,均为 Credential 的子类:

  • 密码 —— PasswordCredential
  • 联合身份 —— IdentityCredential(以及已弃用的 FederatedCredential)
  • 一次性密码(OTP) —— OTPCredential
  • Web Authentication —— PublicKeyCredential

本页仅涵盖 CredentialsContainer 的通用能力;关于 WebAuthn/passkey 这一具体凭据 类型,请参阅专门的 WebAuthn 与 passkey 参考文档。

根据 MDN 针对 CredentialsContainer 的 browser-compat-data,该接口本身从 Chrome 51、Edge 18、Firefox 60 与 Safari 13 开始支持。具体方法的支持时间可能晚于接口 本身,因此应针对你实际需要的方法查阅对应的兼容性数据,而不要假设整个接口一次性 全部就绪。BCD 对同一接口的记录是:

方法 Chrome Edge Firefox Safari
CredentialsContainer 51 18 60 13
get() 51 18 60 13
store() 51 mirror 60 13
create() 60 18 60 13
preventSilentAccess() 60 18 60 17(13 为部分实现)

store() 的 Edge 单元格写的是 mirror,因为 BCD 在那里记录的就是这个值:另外三个方法 各自带有一条显式的 Edge 记录(18),而 store() 没有自己的 Edge 数字,而是镜像对应的 Chromium 数据。

真正会咬人的是 preventSilentAccess() 的 Safari 单元格,因为它的故障模式并不是「方法不 存在」。BCD 为该方法记录了两条 Safari 条目:从 17 起完整支持,以及一条覆盖 Safari 13 到 16 的 partial_implementation,其说明写的是「该方法存在,但总是以 NotSupportedError 异常拒绝」。因此在这些版本上,typeof navigator.credentials.preventSilentAccess === 'function' 这类检查会通过,调用却照样失败——方法确实存在,而且永久性损坏。要检测它,靠的 是捕获调用抛出的 NotSupportedError,而不是探测属性是否存在。

真正最容易让实际实现翻车的缺口,在更下一层——在凭据类型而不是容器上。MDN 把 PasswordCredential 标为有限可用(limited availability):「该特性不属于 Baseline,因为它在一些使用最广泛的浏览器中并不工作。」BCD 则说明了具体是哪些:它 记录 PasswordCredential 自 Chrome 51 起支持,而在 Firefox 与 Safari 中记录为 不支持,两者给出的都是一个尚未关闭的实现缺陷链接,而不是版本号。因此 'credentials' in navigator 为真,并不能说明你可以构造 PasswordCredential:这两个 引擎提供了容器,却没有提供这种类型。

有三个时刻值得关注:登录成功后存储、再次访问时取回,以及登出时清除自动登录标志。

// 1. 在成功登录后存储一个密码凭据。
async function rememberPasswordCredential({ id, password, name, iconURL }) {
if (!('PasswordCredential' in window)) return;
const credential = new PasswordCredential({ id, password, name, iconURL });
await navigator.credentials.store(credential);
}

PasswordCredential 以只读属性暴露 password、name(一个人类可读的字符串,提供 用于在凭据选择器中展示的公开名称)与 iconURL(指向一张图标图片的 URL),此外还有 它从 Credential 继承来的 id 与 type。

// 2. 页面加载时尝试静默登录。
const credential = await navigator.credentials.get({
password: true,
mediation: 'silent',
});
if (credential) {
await signInWith(credential);
} else {
// 没有任何凭据能在不询问用户的前提下交出——那就展示登录按钮,
// 而不是弹出一个用户并没有要求的对话框。
showLoginButton();
}
// 3. 登出时,阻止浏览器把用户直接再登回去。
async function signOut() {
await serverSignOut();
if (!navigator.credentials?.preventSilentAccess) return;
try {
await navigator.credentials.preventSilentAccess();
} catch (err) {
// Safari 13–16 提供了该方法,但总是以 NotSupportedError 拒绝。
// 上面的属性检查看不出这一点,所以这里只吸收这一种拒绝,
// 并退回到清理你自己的会话状态。
if (err.name !== 'NotSupportedError') throw err;
forgetLocalSession();
}
}

这个 try/catch 会处理 Safari 13–16 的 NotSupportedError,因为存在性检查无法区分一个 可用的方法和 BCD 记录的那种部分实现。注意这里只吞掉了 NotSupportedError——其他异常仍会 继续抛出,真正的故障不会被藏起来。

MDN 给出的正是这个理由:「你可以在用户登出网站之后调用它,以确保他们在下次访问站点 时不会被自动登录。」该方法以 undefined resolve。

get() 接受一个 mediation 选项——一个字符串,表示在取回凭据的过程中用户以何种方式 参与。默认值是 "optional"。根据 MDN,四个取值分别是:

  • "silent" —— 不会要求用户进行身份验证;用户代理会在可能的情况下自动重新验证并 登录用户,而如果需要用户同意,该 promise 会以 null 兑现。适用于希望用户一进入 应用就尽可能自动登录,但在做不到时也不要弹出令人困惑的登录对话框的场景。
  • "optional" —— 如果凭据能在无需用户介入的情况下交出,就直接交出,从而实现自动 重新验证;如果需要用户介入,用户代理则会要求用户进行身份验证。适用于你有合理把握 用户不会因为看到登录对话框而感到意外的场景——例如用户刚点击了「登录/注册」按钮。
  • "required" —— 总是要求用户进行身份验证。适用于你想强制身份验证的场景,例如在 执行敏感操作前要求重新验证(MDN 给的例子是确认一笔信用卡付款),或在切换用户时。
  • "conditional" —— 已发现的凭据会以非模态对话框的形式呈现给用户,同时标明请求 凭据的来源。实践中这意味着对可用凭据进行自动填充。

get() 另外两个值得了解的选项:password 是那个布尔值,用于请求浏览器以 PasswordCredential 形式取回已存储的密码;federated 接受 { protocols, providers }——但 MDN 指出 FederatedCredential「现已被取代,开发者 应优先使用 identity 选项(如果可用)」。

get() 接受 signal——一个 AbortSignal,可用于中止一个正在进行的请求。被中止的 操作可能仍正常完成(通常是因为中止发生在操作已经结束之后),也可能以该 signal 的 reason 拒绝,默认是一个 AbortError DOMException。把它与 AbortSignal.timeout() 搭配,就能把一个无人应答的提示变成可捕获的错误:

try {
const credential = await navigator.credentials.get({
password: true,
signal: AbortSignal.timeout(10_000),
});
// …
} catch (err) {
if (err.name === 'TimeoutError') return showLoginButton();
if (err.name === 'AbortError') return; // 请求被取消
throw err;
}

因 AbortSignal.timeout() 设定的超时而被自动中止的请求,拒绝时给出的是 TimeoutError 而不是 AbortError——如果你用了期限,请对两者都做分支处理。

在调用之前先对 navigator.credentials 做特性检测,因为在不支持该 API 的浏览器 或上下文(例如非安全源)中,它是未定义的。又因为容器可能存在、而你需要的凭据类型 并不存在,所以两者都要检测:

async function getStoredCredential() {
if (!('credentials' in navigator) || !('PasswordCredential' in window)) {
// 要么此处无法使用 Credential Management API(浏览器不支持,或处于
// 非安全上下文),要么该引擎只提供了容器而没有密码凭据——
// 回退到手动登录表单。
return null;
}
return navigator.credentials.get({ password: true, mediation: 'silent' });
}
  • 根据 MDN,该 API 仅在安全上下文(HTTPS)中可用——在依赖它之前,请确认页面是 通过安全方式提供的。
  • 不要把 'credentials' in navigator 当作密码凭据可用的证据:BCD 记录 PasswordCredential 在 Firefox 与 Safari 中不受支持,而这两者都提供了 CredentialsContainer。要检测的是凭据类型,而不只是容器。
  • 把 resolve 出 null 当作正常结果而非错误:无法无歧义取得单一凭据时 get() 以 null resolve;需要用户同意时 mediation: 'silent' 以 null 兑现;无法创建任何 Credential 对象时 create() 以 null resolve。
  • MDN 在四种凭据类型中将 FederatedCredential 标记为已弃用,并指出 federated 选项已被 identity 选项取代(在可用的情况下)——在基于它构建新的登录流程之前, 请查阅 MDN 上的最新状态。
  • 各方法的支持可能滞后于接口本身:BCD 把 create() 与 preventSilentAccess() 记在 Chrome 60,而容器本身是 Chrome 51;preventSilentAccess() 记在 Safari 17,而容器 是 Safari 13。请针对你实际需要的方法逐一确认。
  • 不要用检查属性的方式来对 preventSilentAccess() 做特性检测。BCD 把 Safari 13–16 记录 为部分实现:方法存在,但总是以 NotSupportedError 拒绝,所以检查会通过而调用照样失败。 要把调用包起来,捕获 NotSupportedError 后退回到清理你自己的会话,并让其他异常继续抛出。
  • 在登出时调用 preventSilentAccess(),而不是在登录时——它正是那个阻止下次访问把 用户直接登回去的标志位。注意对于 PublicKeyCredential,它通常不起作用,因为这类 验证器通常本就需要用户交互。
  • 如果你在读更老的代码或更老的文章:在早期版本的规范中,preventSilentAccess() 叫 requireUserMediation()。
  • 对 get() 的各类拒绝分别处理:NotAllowedError 覆盖用户取消请求、调用被 identity-credentials-get、publickey-credentials-get 或 otp-credentials 权限 策略阻止,以及调用方来源是不透明源(opaque origin);SecurityError 表示调用方 域名不是有效域名;AbortError 与 TimeoutError 来自 signal 选项。
  • 对于 PublicKeyCredential(WebAuthn/passkey)这一具体类型,请遵循下方专门的 WebAuthn 参考文档,而不要将其当作通用凭据处理。