继续原理篇的话题。本篇从 OAuth 诞生的痛点开始,完整介绍如何在一个 SaaS 系统中接入 Google 和 Apple 社交登录,覆盖 Web 应用和 Chrome Extension 两种场景。

多平台的痛点 → OAuth 的诞生

进入 Web 2.0 时代,资源提供方越来越多。用户在不同平台需重复注册账号和设置密钥,带来两大问题:

  1. 效率低:频繁在多个平台注册账户,操作繁琐。
  2. 安全差:用户在不同平台设置同样的访问密钥,一个平台泄露,所有平台都暴露。

OAuth 方式认证应运而生,它分两个部分:

  • 认证(Authentication):为用户生成全局唯一标识,验证用户身份;
  • 授权(Authorization):代理用户授权给第三方平台使用用户信息(如用户标识、昵称等),用户无需直接注册、也无需重复提交信息或设置访问密钥。
┌─────────────┐     ┌──────────────────┐     ┌─────────────┐
│用户 account  │────▶│  资源提供方       │────▶│  认证平台    │
│             │     │  Resource Owner  │     │  Auth       │
└─────────────┘     └──────────────────┘     └─────────────┘

用现实场景理解信任链

我和系统张三互相认识。我想从系统李四那里借钱,但我和李四不熟。好在系统张三人人认识,我和李四都信任张三。李四从张三那里确认我的身份后,我就可以借钱了。

后续多了王五、赵六也一样。我在不同平台使用同一个身份,并且可以控制——张三不能把我存钱在别处的秘密告诉李四。这就是授权和信任传递。

支持多社交账号登录的账号系统

核心思路

在全球范围内,大部分互联网用户要么使用 Google(Gmail),要么使用 Apple(Apple Mail),或者两者都用。因此,如果 SaaS 系统接入它们的账号体系,就意味着支持了全球大多数用户。这类第三方账号统称为 Social Account。

结合"一处注册,到处可用"的思路,引入一个第三方角色:

                   Google / Apple
                   Social Account System
                        │
        ┌───────────────┼───────────────┐
        │               │               │
    Platform A      Platform B      Platform C

用户只需在 Google/Apple 注册一次账号,就可以在多个平台使用。

信任链模型

用户 aa 在 Google 注册了账号(email + password),Google 分配唯一 ID。
用户 aa 想在应用系统 bar 上使用服务,只需让 bar 去 Google 确认 "我是 aa"。

这个过程包含三个关键点:

  • 认证:验证用户身份
  • 授权:让用户控制哪些信息可以共享
  • 信任传递:认证平台作为可信第三方

OAuth 授权码模式 vs 隐式模式

在进入实战之前,先了解两种主流的 OAuth 流程。它们在不同的客户端场景下各有用途:

  • Authorization Code Flow(授权码模式):前端拿到一次性 code,后端再用 code + client_secret 换 token。更安全,适合有后端的 Web 应用。
  • Implicit Flow(隐式模式):直接返回 id_token,不需要 client_secret,适合纯前端应用或没有后端的场景(如 Chrome Extension)。

参考:Stack Overflow - OAuth Authorization Code vs Implicit

下面你会在 Web 应用中看到授权码模式的应用,在 Chrome Extension 中看到隐式模式的应用。

实战:Sign in with Google

Google Cloud 项目配置

前往 Google Cloud Console 新建项目。

关键概念:

  • 一个 Project 可以对应多个 OAuth Client ID,以适配多种客户端类型(Web, iOS, Android, Extension)
  • 同一个 Project 内,不同 Client ID 生成的用户唯一标识是一样的。也就是说,用户 foo 在 Web 客户端上使用 Gmail 注册得到 UID,在 iOS / Extension 端登录得到的 UID 值相同。
  • 一个 OAuth Client ID 可以生成多个对应的 Client Secret。
  • Client ID 可以对外公开(实际编码中一定会公开),但 Client Secret 绝对不能对外公开。

Web 应用配置

在 Google Cloud 左侧导航栏进入 APIs & Services → Credentials,点击顶部 Create Credentials → OAuth Client ID:

  • Application type: Web application
  • Name: 随便写

配置 Authorized JavaScript origins:

  • 本地测试 URI:http://localhost:8787
  • 生产环境域名 URI:https://foo.bar

保存后记录下 Client ID 和 Client Secret。

Web 应用编码

官网文档:Google Identity Services

完整流程:

[Main Web App] ───▶ Opens Popup ──▶ [Google Auth Server]
      ▲                                         │
      │                                         │ Authenticates User
      └─── postMessage(auth_code) ◄─────────────┘

分步说明:

  1. 页面渲染 — 渲染登录按钮,传入 client_id
  2. 浏览器侧 — Google Identity Services 弹窗,让用户选账号,拿到一次性授权码 code
  3. 后端实现 /api/account/oauth/google
    • 接收浏览器 POST 过来的授权码 code
    • 用 code + client_id + client_secret 向 Google 换 id_token
    • 校验 id_token(验签、校验 aud)
    • 写入或更新 account 和 social_account 表
    • 写入浏览器 session cookie
  4. 浏览器跳转 — 登录成功,重定向到应用首页

Chrome Extension 应用配置

OAuth Client 的配置可以复用 Web 的,也可以独立新建。复用便于管理,独立便于统计不同客户端的注册登录数据。下面以独立新建为例。

在 Google Cloud 新建 OAuth Client:

  • Application type: 选 Web application(而非 Chrome Extension)

    注意:Application type 选 Chrome Extension 在非 Chrome 浏览器下不通用。如果你的目标是跨浏览器(Edge、Chrome、Safari),应选择 Web application。参考 Chrome Extension OAuth 文档

保存后记录 Client ID 和 Client Secret。

生成 Extension 骨架代码后,用 Chrome 打开 chrome://extensions/,勾选 Developer mode,点击 Load unpacked 加载已解压插件,得到 Extension ID。

重要:Extension ID 和已解压路径有关,路径变了 ID 也会变。如果需要固定 ID,参考 Keep consistent ID。

修改 OAuth Client 配置,在 Authorized redirect URIs 中添加:

https://<extension-id>.chromiumapp.org/

基于 Vite 构建时,配置环境变量:

VITE_SIGN_IN_WITH_GOOGLE_CLIENT_ID=xxx-ooo.apps.googleusercontent.com
VITE_SIGN_IN_WITH_GOOGLE_REDIRECT_URI=https://<extension-id>.chromiumapp.org/

更多参考:wxt-dev GitHub Discussion #505

Chrome Extension 应用编码

参考文档:

完整流程:

Extension                Google               Backend (/xxx.AccountService/Auth)
   │  launchWebAuthFlow     │                      │
   │──────────────────────▶ │  consent screen       │
   │  #id_token=<JWT>       │                      │
   │◄────────────────────── │                      │
   │  auth({provider:"google;<EXT_ID>", idToken})  │
   │──────────────────────────────────────────────▶│
   │                        │  GET /oauth2/v3/certs │
   │                        │◄─────────────────────│  (fetch JWKS)
   │                        │─────────────────────▶│  verify sig + iss + aud + exp
   │           { jwt }                             │
   │◄──────────────────────────────────────────────│
   │  store jwt, reload tab │                      │

分步说明:

  1. 插件渲染登录页面按钮,生成授权链接:

    https://accounts.google.com/o/oauth2/auth
      ?client_id=<VITE_SIGN_IN_WITH_GOOGLE_CLIENT_ID>
      &response_type=id_token                      ← implicit flow(无 code,无 secret)
      &redirect_uri=https://<ext-id>.chromiumapp.org/
      &scope=openid email profile
    
  2. 插件调用 browser.identity.launchWebAuthFlow() 弹窗让用户授权

  3. 用户授权后,插件重定向到 https://<ext-id>.chromiumapp.org/#id_token=<JWT>&...

  4. 插件从 URI 提取 id_token,向后端发起 auth() 请求

  5. 后端实现 auth():

    • 验证 id_token(JWKS 验签、校验 iss/aud/exp)
    • 写入或更新 account 和 social_account 表
    • 生成自有服务的、经过签名的 JWT,返回给插件后续使用
  6. 插件存储 JWT,刷新页面

实战:Sign in with Apple

基本概念

官网文档:

Apple 的账号体系层级关系:

一个 Apple Developer Account
  └── 可以生成多个 App ID
        └── 多个 App ID 可以归为一组
              └── 其中一个为 Primary App ID
              └── 同一个 Primary App ID 下,账户 UID 相同
        └── 一个 App ID 可以关联多个 Service ID
              └── 每个 Service ID 可对应不同的平台(Web, iOS 等)

应用配置

前提:付费订阅 Apple Developer Program。

进入 Certificates, Identifiers & Profiles 进行配置:

Step 1 — 新建 App ID
例如 info.my-domain.app

Step 2 — 新建 Service ID
例如 info.my-domain.web

  • 勾选启用 Sign in with Apple
  • 修改 Web Authentication Configuration:
    • 关联 Primary App ID
    • Register Website URLs:
      • Domains and Subdomains:添加 Web 应用域名,如 foo-bar.ngrok-free.app
      • Return URLs:添加 Web 处理回调链接,如 https://foo-bar.ngrok-free.app/api/account/oauth/apple

        注意:链接必须是 HTTPS 开头。本地开发可使用 ngrok 等工具临时映射到公网。个人开发账号最多关联 10 个链接,企业开发账号可关联 100 个。

Step 3 — 新建 Key
前往 Auth Keys,用于校验用户授权登录后 Apple 生成的 JWT。

  • Key Name:如 my-domain
  • Key Usage Description (optional):my-domain sign in with xxx
  • 勾选启用 Sign in with Apple → Configure Key → 勾选前面新建的 Primary App ID

Step 4 — 完成设置

Depending on your product, you may need to configure multiple components for Sign in with Apple – from registering domains for Web Authentication to providing email sources to communicate with your users through the Private Email Relay service. Learn more

应用编码

参考文档:Sign in with Apple REST API

后端流程:

  1. /api/account/oauth/apple — 接收登录回调
  2. 用户授权登录后得到 code,后端用 code 向 Apple 服务器校验,换取 id_token
  3. 使用 Apple 公钥校验 id_token 签名,解释得到账号信息
  4. 写入或更新 account 和 social_account 表

常见问题:

现象 原因
invalid_request / “Invalid client id or web redirect url” Client ID 或 Redirect URI 配置不匹配
授权后弹窗没有自动关闭 APPLE_OAUTH_REDIRECT_URI 配置错误

如果遇到"发生错误,无法完成你的请求"提示,首先检查 Redirect URI 是否完全一致(包括 HTTPS 前缀、路径、无多余斜杠)。

Apple:跨团队迁移应用和用户

如果你需要将应用和用户迁移到另一个 Apple Developer 账号:

总结

本篇从多平台密码管理的痛点出发,完整介绍了如何通过 OAuth + 社交登录解决这些问题。核心要点:

  1. OAuth 的核心价值:一处认证,多处使用;平台隔离,一处泄露不会波及全部。
  2. 信任传递:通过 Google/Apple 等可信第三方,实现跨平台身份验证。
  3. 实战要点:
    • Google Web 应用走 Authorization Code Flow(code + secret)
    • Chrome Extension 走 Implicit Flow(直接返回 id_token)
    • Apple 登录需注意 HTTPS + Redirect URI 精确匹配
  4. 安全底线:Client Secret 不暴露、JWT 验签、生产环境限制域名。