Electron 42.1.0:macOS Touch ID WebAuthn 修复与实战要点

2026-05-15 43 预计阅读时间:1 分钟
来源:oschina.net AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:8 分钟

Electron 42.1.0 是一个以修复为主的补丁版本,但其中一项改动对依赖生物识别认证的桌面应用影响不小——macOS Touch ID 在 WebAuthn 流程中的提示异常被修复了。如果你正在用 Electron 构建需要本地身份验证的桌面产品(比如加密钱包、企业内部工具、密码管理器),这个版本值得立刻跟进。

修复了什么

此次版本的核心修复是:macOS Touch ID WebAuthn 提示缺失问题。此前在某些场景下,当应用通过 WebAuthn API 请求生物识别认证时,系统级的 Touch ID 弹窗不会正常出现,导致认证流程卡住或直接失败。这对用户体验和安全性都是硬伤——用户无法完成身份验证,应用只能回退到备用方案或直接报错。

这个问题的根因与 Chromium 层在 macOS 上对 platform authenticator 的调用路径有关。Electron 内嵌的 Chromium 版本在特定条件下未能正确触发 macOS Security Framework 的 Touch ID 交互界面,42.1.0 对这一调用链做了修正。

WebAuthn 在 Electron 中怎么用

WebAuthn(Web Authentication API)是 W3C 标准化的无密码认证协议,支持平台认证器(如 Touch ID、Windows Hello)和漫游认证器(如 YubiKey)。在 Electron 中,渲染进程的 navigator.credentials API 可以直接调用,但有几个细节需要注意:

  • webPreferences 中必须启用相关特性:默认配置下,部分安全 API 可能受限。
  • 需要 HTTPS 或 localhost:WebAuthn 要求安全上下文(Secure Context),否则 navigator.credentials 对象不可用。
  • macOS 权限声明:如果打包后的应用要使用 Touch ID,需要在 Info.plist 中声明 NSFaceIDUsageDescription

下面是一个最小可运行的 Electron + WebAuthn 示例项目:

项目结构

electron-webauthn-demo/
├── main.js
├── preload.js
├── index.html
├── package.json

package.json

{
  "name": "electron-webauthn-demo",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "^42.1.0"
  }
}

main.js

const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      // 不建议在生产环境打开 nodeIntegration,这里仅作演示
      nodeIntegration: false,
      contextIsolation: true
    }
  });

  // WebAuthn 要求安全上下文,加载 localhost 或 HTTPS 地址
  // 本地开发用 file:// 协议在部分版本下也可工作,但推荐用本地服务器
  win.loadFile('index.html');
}

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

preload.js

const { contextBridge } = require('electron');

// 暴露安全 API 给渲染进程(演示中直接在 HTML 内调用 navigator.credentials)
contextBridge.exposeInMainWorld('electronDemo', {
  platform: process.platform
});

index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>WebAuthn Touch ID Demo</title>
  <style>
    body { font-family: -apple-system, sans-serif; padding: 2rem; }
    button { padding: 0.6rem 1.2rem; font-size: 1rem; cursor: pointer; }
    #log { margin-top: 1rem; white-space: pre-wrap; background: #f5f5f5; padding: 1rem; }
  </style>
</head>
<body>
  <h2>WebAuthn 注册与验证</h2>
  <button id="register">注册(Create Credential)</button>
  <button id="authenticate">验证(Get Credential)</button>
  <div id="log"></div>

  <script>
    const log = document.getElementById('log');
    // 演示用的挑战值,生产环境应从服务器获取随机 challenge
    function randomBuffer(length) {
      const buf = new Uint8Array(length);
      crypto.getRandomValues(buf);
      return buf;
    }

    let savedCredentialId = null;

    document.getElementById('register').addEventListener('click', async () => {
      try {
        const credential = await navigator.credentials.create({
          publicKey: {
            challenge: randomBuffer(32),
            rp: { name: 'Electron Demo', id: 'localhost' },
            user: {
              id: randomBuffer(16),
              name: 'demo@example.com',
              displayName: 'Demo User'
            },
            pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
            authenticatorSelection: {
              authenticatorAttachment: 'platform',  // 使用 Touch ID / Windows Hello
              userVerification: 'required'
            },
            timeout: 60000
          }
        });
        savedCredentialId = credential.rawId;
        log.textContent = '注册成功!Credential ID: ' + 
          Array.from(new Uint8Array(credential.rawId)).map(b => b.toString(16).padStart(2, '0')).join('');
      } catch (e) {
        log.textContent = '注册失败: ' + e.message;
      }
    });

    document.getElementById('authenticate').addEventListener('click', async () => {
      if (!savedCredentialId) {
        log.textContent = '请先注册';
        return;
      }
      try {
        const assertion = await navigator.credentials.get({
          publicKey: {
            challenge: randomBuffer(32),
            allowCredentials: [{
              type: 'public-key',
              id: savedCredentialId,
              transports: ['internal']
            }],
            userVerification: 'required',
            timeout: 60000
          }
        });
        log.textContent = '验证成功!签名长度: ' + assertion.signature.byteLength + ' bytes';
      } catch (e) {
        log.textContent = '验证失败: ' + e.message;
      }
    });
  </script>
</body>
</html>

macOS Touch ID 权限声明

如果你用 electron-builder 打包,需要在 extraResources 或构建配置中确保 Info.plist 包含 Face ID / Touch ID 使用说明:

# 在 electron-builder 的 afterSign 钩子中,或手动修改 Info.plist
# 添加以下键值:
/usr/libexec/PlistBuddy -c "Add :NSFaceIDUsageDescription string '用于 WebAuthn 生物识别认证'" your-app.app/Contents/Info.plist

不声明这个权限,macOS 会直接拒绝 Touch ID 弹窗,和之前 Electron 的 WebAuthn bug 表现类似——但根因不同,别混淆了。

运行示例

mkdir electron-webauthn-demo && cd electron-webauthn-demo
# 把上面四个文件放进去
npm install
npm start

点击"注册"按钮,macOS 上应弹出 Touch ID 提示(这正是 42.1.0 修复的场景),Windows 上则弹出 Windows Hello。验证流程同理。

升级建议与注意事项

  • 直接升级 42.1.0 的风险很低:这是补丁版本,没有 API 变动或破坏性改动,只修 bug。如果你的项目卡在 42.0.x 且用户反馈了 Touch ID 问题,升级是零犹豫的选择。
  • 确认你的 Chromium 基线:Electron 42 对应 Chromium 136,大版本跳跃意味着底层行为变化可能比你想象的多。从更早的版本(比如 30.x)跨多版本升级时,建议逐个大版本验证,不要一步到位。
  • WebAuthn 的安全上下文要求:打包后的应用如果加载远程页面,必须确保该页面走 HTTPS。加载本地 file:// 内容时,Electron 在某些版本下会放宽 Secure Context 限制,但这不是规范保证的行为,生产环境应自建本地 HTTPS 服务器或使用自定义协议(protocol.registerSchemesAsPrivileged)。
  • Touch ID / Face ID 权限声明是 macOS 独立要求:和 Electron 版本无关,不声明就不弹窗。这是很多开发者踩的坑,容易误以为是 Electron 的 bug。

如果你不涉及生物识别认证,42.1.0 对你来说只是例行更新,升级与否影响不大。但保持补丁版本的跟进本身就是低成本的安全实践——Chromium 的安全补丁总是随 Electron 版本一起发布的。


相关推荐