WebAuthn 与 Passkey
WebAuthn 是什么?
Section titled “WebAuthn 是什么?”WebAuthn 是浏览器、操作系统和硬件安全密钥支持的无密码/多因素认证标准。用户看到的产品形态通常叫:
- passkey
- 通行密钥
- 安全密钥
- 指纹/面容/系统解锁登录
SyncTV 的配置字段使用标准名 webauthn,产品文案可以叫 passkey。
开启前需要什么?
Section titled “开启前需要什么?”生产环境必须满足:
- 使用 HTTPS。
- 有稳定域名。
rp_id与rp_origin的域名关系正确。- 多副本部署必须使用 Redis 保存 challenge 状态。
webauthn.enabled
Section titled “webauthn.enabled”默认值:
webauthn: enabled: false开启后,passkey 注册和登录接口可用。
Passkey 登录使用 WebAuthn discoverable credential 和 conditional UI。登录开始请求不包含用户名或邮箱;浏览器或操作系统的 passkey UI 会让用户选择当前 RP ID 下的凭据,服务端再通过返回的 credential ID 解析账号。客户端应将它作为 passkey 登录,并要求浏览器/认证器组合支持 discoverable credential。
webauthn.rp_id
Section titled “webauthn.rp_id”Relying Party ID。可以理解为“认证器认为这是哪个网站”。
通常填写主域名,不带协议、不带端口、不带路径。
示例:
webauthn: rp_id: "example.com"如果前端是:
https://app.example.comrp_id 可以是:
example.com或在某些场景中是:
app.example.com不要填写:
https://example.comexample.com/loginlocalhost:8080
webauthn.rp_origin
Section titled “webauthn.rp_origin”用户实际访问的前端 origin。
示例:
webauthn: rp_origin: "https://app.example.com"origin 只能包含:
- 协议
- 域名
- 可选端口
不能包含路径、query、fragment。
正确:
https://app.example.com错误:
https://app.example.com/loginwebauthn.rp_name
Section titled “webauthn.rp_name”显示给用户的服务名称。
默认:
webauthn: rp_name: "SyncTV"用户在系统 passkey 管理界面可能会看到这个名称。
webauthn.allowed_origins
Section titled “webauthn.allowed_origins”额外允许的 origin。
例如你同时有 Web 前端和桌面客户端开发地址:
webauthn: rp_origin: "https://app.example.com" allowed_origins: - "https://admin.example.com"保持最小集合,不要随便添加不使用的域名。
原生 App 关联
Section titled “原生 App 关联”Apple 平台通过 webauthn.apple_app_ids 配置允许使用该 RP ID 的原生 App。每一项使用 Apple application identifier,格式为 TeamID.BundleID:
webauthn: apple_app_ids: - "ABCDE12345.org.synctv.app"服务器会在 /.well-known/apple-app-site-association 返回 webcredentials.apps。App 的 Associated Domains entitlement 需要包含 webcredentials:example.com。
Android 平台通过包名和发布证书 SHA-256 指纹配置:
webauthn: android_apps: - package_name: "org.synctv.app" sha256_cert_fingerprints: - "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"服务器会在 /.well-known/assetlinks.json 返回 delegate_permission/common.get_login_creds 关联声明。证书轮换期间可以同时配置多个指纹。
对应环境变量接受 JSON:
SYNCTV_WEBAUTHN_APPLE_APP_IDS=["ABCDE12345.org.synctv.app"]SYNCTV_WEBAUTHN_ANDROID_APPS=[{"package_name":"org.synctv.app","sha256_cert_fingerprints":["AA:BB:..."]}]官方 App 与自托管服务器
Section titled “官方 App 与自托管服务器”Android 官方 App 可以动态支持任意自托管 SyncTV 域名。每个正式签名的 GitHub Release 都会附带 SyncTV-<version>-android-passkey-server-config.yaml,其中包含公开的官方包名和发布证书 SHA-256 指纹。将文件中的 android_apps 合并到服务器的 webauthn 配置,并为服务器自己的域名设置 rp_id 和 rp_origin:
webauthn: enabled: true rp_id: "tv.example.com" rp_origin: "https://tv.example.com" android_apps: - package_name: "org.synctv.app" sha256_cert_fingerprints: - "<官方 Release 配置文件中的 SHA-256 指纹>"包名和证书指纹属于公开身份信息。Android 使用它们验证当前 App 是否获得域名授权;keystore、签名私钥和密码继续保密。development 或 fork 构建拥有各自的签名身份,需要配置对应安装包的实际证书指纹。
证书指纹由签名密钥决定,应用版本升级不会改变指纹。官方 Release 持续使用固定发布密钥;fork 可以创建自己的固定 release keystore,并将签名配置保存为 fork 仓库的 GitHub Secrets。该 fork 的 Release workflow 会自动发布对应的服务器配置文件。CI development 签名属于临时身份,可能随着 runner 和构建环境变化,因此只适合临时测试。
同一服务端可以同时授权官方 App 和 fork App。相同包名使用一个 android_apps 条目并列出多个指纹:
webauthn: android_apps: - package_name: "org.synctv.app" sha256_cert_fingerprints: - "<官方 App 证书 SHA-256>" - "<fork App 证书 SHA-256>"使用自定义包名的 fork 增加独立的 android_apps 条目。官方签名私钥仅由官方发布流程持有;fork 使用自己的签名密钥即可与同一自托管服务器稳定共存。
GitHub APK 使用 Release 配置文件中公布的证书。未来通过 Google Play 分发时,Google Play App Signing 可能使用另一份安装包签名证书,服务端需要将 Play Console 中的 App signing certificate SHA-256 一并加入 sha256_cert_fingerprints。
Apple 将允许的 RP ID 固定在签名 App 的 Associated Domains entitlement 中。SyncTV 官方预构建的 iOS/macOS App 只能对构建时列入 SYNCTV_PASSKEY_RP_IDS 的域名使用原生 Passkey,因此任意独立域名的自托管服务器无法动态加入官方 Apple App。自托管部署可以选择以下路径:
- 自行构建并使用 Apple Developer Team 签名 App,将服务器 RP ID 加入
SYNCTV_PASSKEY_RP_IDS,同时将<TeamID>.org.synctv.app加入服务端apple_app_ids。 - 使用系统浏览器承载 WebAuthn,让浏览器按自托管域名管理 Passkey;当前官方 App 仅提供原生 Passkey,采用此路径需要后续实现浏览器认证流程。
- 使用已由官方 Apple App entitlement 覆盖的 RP 域名。
Android 的授权方向由每个自托管服务器控制,因此官方预构建 App 可以在运行时扩展到新域名。Apple 的原生授权同时受构建签名和服务器 AASA 约束,因此域名集合需要在构建时确定。
webauthn.allow_subdomains
Section titled “webauthn.allow_subdomains”默认值:false。
作用:是否允许配置域名的子域名也通过验证。
普通部署保持 false 更安全。
webauthn.allow_any_port
Section titled “webauthn.allow_any_port”默认值:false。
作用:验证 origin 时是否忽略端口。
只建议本地开发使用。例如前端开发服务器端口经常变化。
生产环境不要开启。
webauthn.timeout_seconds
Section titled “webauthn.timeout_seconds”默认值:300。
作用:passkey 注册或登录 challenge 的有效时间。
用户需要在这个时间内完成系统弹窗确认。
与两步验证的关系
Section titled “与两步验证的关系”Passkey 可以作为本地验证方式之一。用户开启两步验证时,至少需要两种本地验证方式,常见组合:
- password + email
- password + webauthn
- email + webauthn
OAuth2 不参与本地两步验证。
Redis 依赖
Section titled “Redis 依赖”WebAuthn challenge 必须一次性使用。多副本部署时,如果 challenge 保存在某个 Pod 内存里,用户下一步请求打到另一个 Pod 就会失败。
因此:
- 单机:可以不用 Redis。
- 多副本或
cluster.enabled=true:必须配置 Redis。