微前端中的认证与授权
一个产品不需要每个 Remote 都登录一次
Section titled “一个产品不需要每个 Remote 都登录一次”微前端用来拆分职责。团队可以独立开发、测试和发布不同产品区域。但这并不意味着每一个技术上独立的单元,都必须自己重新实现所有跨产品能力。
Authentication 是最明显的例子之一。
假设一个产品由一个 Host 和十个动态加载的 Remote 组成。如果每个 Remote 都自己登录用户,就会出现十套 Identity Server Integration、十套 Login Lifecycle,以及十个存储、刷新和清理 Token 的位置。所谓 Autonomy,主要只是把同一项基础设施职责复制了十遍。
一个有十个 Remote 的产品,不需要十次 Login。
对于被 Mount 到同一应用中的微前端,更合理的模型是:
不是每个 Remote 自己登录,而是整个应用只登录一次。
Platform 拥有 Login Lifecycle,并建立应用的 Authentication Context。Remote 通过一份很小的 Contract 使用这个 Context,不需要了解 OpenID Connect Flow,也不需要自己管理 Token。
受保护 Backend 则承担另一项职责:决定某个具体操作是否允许作用在某个具体 Resource 上。Remote 可以根据已知 UI Capability 调整可见界面,但它不能替代真正有效的 Authorization。
因此可以把职责明确分成三层:
OpenID Provider└── 认证用户身份
Host / Platform└── 拥有 Authentication Context
Resource Server└── 对受保护 Resource 的访问做授权判断这个边界比你选哪一个 OIDC Library、哪一个 Frontend Framework,甚至比 API 通过 BFF 还是 Browser 直接调用更重要。
真正需要理解的少数 OIDC 概念
Section titled “真正需要理解的少数 OIDC 概念”OpenID Connect 有很多标准术语。对于微前端的架构模型,只需要其中很小一部分。
| 概念 | 在本文模型中的含义 |
|---|---|
| End-User | 使用产品并完成认证的人。 |
| User-Agent | End-User 操作的软件;这里就是 Browser。 |
| Client / Relying Party | 需要 Login,并信任 OpenID Provider 认证结果的应用。 |
| OpenID Provider | 对用户进行认证的 Identity Server。 |
| Authorization Endpoint | OIDC Login Flow 在 OpenID Provider 上的入口。 |
| Redirect URI | Login 结束后 Browser 回到应用的注册地址。 |
| Authorization Code | Login 成功后得到的短生命周期中间结果。 |
| ID Token | OpenID Provider 对“用户已经被认证”这件事提供的可验证声明。 |
| Access Token | 用于访问受保护 Resource 的 Credential。 |
| Resource Server | 受保护 API;它验证 Access Token 是否面向自己,并判断当前业务访问是否允许。 |
首先要理解 Client 的角色。纯 Browser Application 可以由 Host 实现 OIDC Client;BFF Architecture 则可以把真正的 Protocol Handling 放到 Server Side。
无论采用哪种技术方式,对 Remote 都没有本质区别:它们仍然属于同一个应用,而不是自动变成 OpenID Provider 眼中的额外 Client。
一次 Login、一个 Host、多个 Remote
Section titled “一次 Login、一个 Host、多个 Remote”Login Flow 不需要在每个 Remote 中重复。它发生在“应用”与 OpenID Provider 之间。
1. Browser 打开应用。2. Platform 发现当前没有有效 Authentication Context。3. 应用的 Auth Layer 启动 OIDC Flow,把 Browser 重定向到 OpenID Provider。4. OpenID Provider 认证 End-User。5. Browser 携带 Authorization Code 回到 Redirect URI。6. 应用处理结果并建立 Authentication Context。7. 随后激活受保护产品区域。现代 Browser-based Client 通常使用 Authorization Code Flow with PKCE。但对于 Platform 与 Remote 的 Responsibility Split 来说,密码学细节并不是这里的核心。
架构模型依然很简单:
End-User │ ▼Browser │ ▼Host / Platform ─────────► OpenID Provider │ │ 建立 Authentication Context ▼Remotes │ ▼受保护 APIsPlatform 判断是否已经存在有效 Authentication Context;如果没有,则启动 Login。成功返回后,由 Auth Layer 处理结果,并把产品 UI 真正需要的 Context 暴露出来。
Remote 不需要知道 Authorization Endpoint 或 Redirect URI,不需要自己的 OIDC Configuration,也不需要知道 Authorization Code 如何处理,更不需要自己组织 Session 或 Token Lifecycle。
它只需要知道:用户是否已认证,以及自己的 UI 可以使用哪些相关信息。

一个 Deployment Unit 还不是一个 OIDC Client
Section titled “一个 Deployment Unit 还不是一个 OIDC Client”微前端经常通过独立发布来定义:一个 Remote 可以独立 Build、Deploy,Bundle 也可能来自独立 Server 或 Domain。
但 Deployment Autonomy 并不能说明谁是 OpenID Provider 眼中的 Client。
独立 Deployment Unit 不会自动把一个 Remote 变成独立 OIDC Client。
如果 Remote 被加载进 Host,并在这个 Application Context 内执行,它仍然属于同一个应用。JavaScript 单独 Build,并不会因此获得自己的 Login Lifecycle。这个模型下,Remote 既不需要独立 Redirect URI,也不需要独立 Login Flow。
这与“本身可以独立导航的应用”不同。后者完全可能拥有各自 Client Registration,同时借助 OpenID Provider 的 SSO Session,让用户不必重新输入凭据。
对于这里的 Metamodel,只需要区分:
Client-ID└── 标识一个应用
OpenID Provider 上的 SSO Session└── 识别已经认证的 End-UserDeployment Autonomy 不会自动创建新的 OIDC Client,也不会自动产生一份新的 User Session。
Session 不需要在 Remote 之间共享
Section titled “Session 不需要在 Remote 之间共享”微前端 Authentication 讨论中经常出现一句话:
那 Remote 之间就必须共享 Session。
问题其实从这个句子就开始了。它预设每个 Remote 先拥有自己的 Session,然后再想办法与其他 Remote 同步。
于是模型变成:
Remote A 拥有 Session ⇅Remote B 拥有 Session ⇅Remote C 拥有 Session对于被 Mount 到同一个应用里的 Remote,更合适的是:
应用拥有 Authentication Context │ ├── Remote A 使用它 ├── Remote B 使用它 └── Remote C 使用它一个共同 Application Session 不需要在 Remote 之间传播。它应该被中央拥有,再通过清晰 Contract 提供使用。
这里还要区分三个层次。
OpenID Provider 可以拥有自己的 SSO Session,用于识别此前已认证的 End-User。
应用本身也可能管理自己的 Session 或 Token Lifecycle。具体实现属于应用内部架构。
Remote 看到的则只是它们进行展示和交互所需的 Authentication Context。
所以,Session 不是 Remote 之间需要同步的 State。
它属于整个应用。
Remote 需要 Context,不需要 Token
Section titled “Remote 需要 Context,不需要 Token”Remote 经常需要知道用户是否登录,可能需要 Display Name、稳定 User ID,或者一些决定“哪些 UI Function 应该出现”的信息。
这并不意味着 Remote 应该拿到 ID Token、Access Token 或具体 OIDC Client Instance。
一份 framework-neutral Platform Contract 可以很小:
interface AuthContext { status: 'anonymous' | 'authenticated';
user: { id: string; displayName: string; } | null;
uiCapabilities: readonly string[];
requestLogin(): void; requestLogout(): void;}status 与 user 让 Remote 能决定可见状态:匿名用户也许看到 Login Hint,认证用户则看到 Protected Area 和 Display Name。
uiCapabilities 用来帮助 UI 展示原则上可用的功能:隐藏 Navigation Entry、Disable Action、避免明显无意义的 Request。但它只是 UI Projection,不是最终授权判断。
requestLogin() 和 requestLogout() 表达 Intent。Remote 可以请求 Login 或 Logout,真正执行交给 Platform。
Contract 刻意不包含 Raw Token,也不包含 OIDC Library、Storage Key、Refresh Callback 或 Provider-specific Data Structure。
Platform 提供的是能力,不是它内部的 Authentication Implementation。
这与其他 Platform Capability 是同一种边界:Remote 可以 Navigation,但不拥有 Host Router;可以触发 Notification,但不拥有中央 Notification Infrastructure;同样,它可以响应 Authentication Context,但不会因此成为 OIDC Client。
Remote 需要 User Context,不需要 Token。
Claim 应该被翻译成 Product Contract
Section titled “Claim 应该被翻译成 Product Contract”Authentication 成功后,ID Token 或 Identity Provider 的其他 Response 可能包含 Claim。Claim 是关于已认证用户或 Authentication Context 的声明。
技术上可以把整个结构直接传给所有 Remote。
架构上通常不应该这么做。
OIDC Claims │ ▼Platform Adapter │ ▼稳定 AuthContextPlatform Adapter 把 Provider-specific 技术信息翻译成产品自己的稳定 Contract。
因此,Remote 不应该直接依赖任意 Role Path、Group Structure 或 Custom Provider Claim。否则产品 UI 会直接绑定某个具体 Identity Provider 及当前 Configuration;Provider 或 Permission Model 一变,大量 Remote 都要跟着修改。
Role Name 也不自动适合作为 Product Contract。一个技术 Role 可能聚合多种业务含义,反过来,一项 UI Capability 也可能由多个 Role、Attribute 或 Business Rule 派生。
Adapter 正是用来封装这种差异:
Provider-specific Claims │ ▼Product-specific UI Capabilities这样 Remote 与具体 Identity Infrastructure 解耦,只获得自己的 UI 真正需要的信息。
它还限制了 Identity Data 的传播范围。并不是每个 Remote 都需要知道用户的所有 Claim;一个小 Contract 会明确哪些信息真正属于共同 Product Platform。
ID Token、Access Token 和 JWT 不是一回事
Section titled “ID Token、Access Token 和 JWT 不是一回事”Frontend Project 经常把 ID Token、Access Token 与 JWT 几乎当成同义词,从而模糊不同职责。
ID Token 描述 Client 侧的 Authentication Result,是 OpenID Provider 对“这个 End-User 已经在某个 Context 中成功认证”提供的可验证声明。1
Access Token 则面向受保护 Resource。Resource Server 要验证 Credential 是否针对自己,并判断它是否可以用于当前访问。
因此,ID Token 并不会自动成为调用 Business API 的 Credential。
JWT 首先只是一个承载结构化 Claim 的格式。ID Token 通常用 JWT 表示;Access Token 也可以是 JWT,但并非必须。2
JWT 描述格式,不描述 Token 的用途,也不描述 Session Architecture。
所以,仅仅看到一个由三个点分隔的 String,并不能推导出合理架构。真正重要的是:谁签发它、面向哪个 Audience、用于什么 Purpose,以及哪一个 Component 拥有它的 Lifecycle。
为什么 localStorage 里的 Token 不是共同 Login
Section titled “为什么 localStorage 里的 Token 不是共同 Login”Browser Application 中常见这样的模式:
localStorage.setItem('token', accessToken);Credential 于是看起来“全应用都能访问”:同一个 Document 中的每个 Remote 都可以读取它,再附加到 Request。
问题恰恰就是太容易了。
localStorage 对当前 Document 中运行的 JavaScript 可见。对于被 Mount 的微前端,这些 JavaScript 可能来自多个独立开发、独立发布的 Remote。Bearer Credential 放在这里,就会变成应用范围内任何这些代码都可以访问的 Global Technical Resource。
但它并没有因此获得一个明确 Owner。相反,形成的是隐式约定:很多地方都可以读 Token,真正负责 Token Lifecycle 的边界却变模糊。
localStorage里的 JWT 不是 Shared Session,而是 JavaScript 可以访问的 Credential。
这句话不是在断言所有 Browser-based Authentication 都不可接受,它只是在描述“自由可访问 Token Storage”会带来的架构后果。
Credential 会成为所有 Remote 的共同基础设施。它的 Name、Format、Refresh 或 Expiration 变化,都可能影响整个 Frontend。Remote 还可能各自推断 Validity,或者误把 Token 写进 Log、复制到其他 State 中。
更清晰的 Platform Boundary 应该允许 Remote 发起受保护调用,而不需要把底层 Credential 作为 Global Resource 暴露给它。
Token 不应该通过 Global Event 分发
Section titled “Token 不应该通过 Global Event 分发”同样的错误也可以换一种“更解耦”的形式出现:Host 不把 Token 放在 Global Browser Storage,而是通过 Event 分发。
Host└── 发布 “token-changed”
Remote A 保存 TokenRemote B 保存 TokenRemote C 保存 Token形式上 Remote 彼此没有直接依赖,实际上却各自拥有同一 Credential 的副本,并必须同步响应同一个技术 Lifecycle。
Token 过期谁 Refresh?某个 Remote 处理了过期 Event 怎么办?Logout 后如何确保所有副本都被清理?Refresh Failure 谁处理?
从一个 Owner 变成多个 Owner,从一个 Lifecycle 变成多个部分同步的 Lifecycle。
同时,Global Channel 又制造横向耦合。每个 Remote 都必须知道 Token Event 和 Payload,于是直接绑定 Platform Authentication Implementation。
Token Sharing 往往不是微前端 Authentication 的解决方案,而是 Authentication Responsibility 没有切清楚的信号。

更好的边界是:
Remote└── 使用 Platform 或 API Contract
Platform└── 管理 Session 或 CredentialToken 不应该通过 Global Event、Shared Store 或自由可访问 Browser Object 分发。换成 Global Store 只是换了 Transport,并没有回答谁拥有 Credential。
受保护 API 调用可以是一项 Platform Capability
Section titled “受保护 API 调用可以是一项 Platform Capability”Remote 必须能调用受保护 API,但不必因此自己持有 Access Token。
边界可以是:
Remote└── 业务 Gateway └── Authenticated Transport Adapter └── 受保护 APIRemote 调用的是业务能力:
interface InvoiceGateway { approve(invoiceId: string): Promise<void>;}Implementation 内部可以使用 Application Session、调用 Server-side BFF、附加 Access Token,或者处理必要 Refresh。这些技术细节留在 Gateway / Transport Adapter 后面。
Remote 需要能够执行受保护 Operation,但不需要知道 Platform 为此使用哪个 Credential。
某些产品使用 Generic HTTP Client 完全合理。但 Business Gateway 往往更准确表达所需 Capability,也能防止 URL Structure 与 Authentication Detail 渗进 Presentation Logic。
重点不在接口长什么样,而在 Responsibility 是否被封装。Remote 可以依赖“受保护通信”这项能力,不应该依赖应用内部 Token Management。
BFF 还是 Browser Client,不应该改变 Remote Contract
Section titled “BFF 还是 Browser Client,不应该改变 Remote Contract”应用可以用不同方式组织 Session 与 API Access。
一种方案使用 Server-side BFF。Browser 只与 BFF 通信,OIDC 与 Token Management 在 Server Side 封装。
另一种方案在 Browser Host 中中央实现 OIDC Client 与必要 Access Token。
哪种方式合适取决于产品条件,本文不需要替这项决策给出统一答案。
但对于 Remote,最好不可见。
API Access 是通过 Server-side Session + BFF,还是通过 Browser 中央 Token Handler,是后续架构决策。Remote Contract 最好不因此变化。
两种方案里,都不是每个 Remote 自己登录,也不是每个 Remote 自己管理 Token Lifecycle。Authentication Implementation 更换,也不应该迫使所有 Product Area 一起 Migration。
这正是好的 Platform Contract 的价值:把“需要什么能力”与“当前怎么实现”分离。
Authentication 不是 Authorization
Section titled “Authentication 不是 Authorization”Authentication 与 Authorization 回答的是两个不同问题:
Authentication└── 这个人是谁?
Authorization└── 她能否在这个 Resource 上执行这个具体 Action?OpenID Provider 认证 End-User,向应用确认某个 Identity 已经过验证。
但这并不会自动决定这个人能否批准某张 Invoice、删除某个 Project、修改别人的数据或打开 Confidential Report。
应用可以用 Authentication Context 合理组合 UI。Remote 可以隐藏 Navigation、Disable Action、提供易理解提示。
最终 Authorization Decision 仍然属于受保护 API / Resource Server。
隐藏一个 Menu Item 不是 Authorization。
UI 可以绕过,Request 可以脱离正常 User Flow 直接生成,URL 可以直接访问,API 也能被绕过 Remote 直接调用。
一个没有被 Mount 的 Remote,也阻止不了直接 API Request。
因此,Resource Server 必须检查 Identity、Requested Action、Resource 以及 Business Context,只有它能真正 Allow 或 Deny。
Identity Provider 可以提供 Role、Attribute 或其他 Identity Information,但业务 Decision 仍属于受保护 Resource。Identity Provider 不会自动知道整个 Domain Model 的所有 Rule。
UI 可以帮助,Backend 必须决定
Section titled “UI 可以帮助,Backend 必须决定”AuthContext 中的 UI Capability 仍然很有用。它可以改善 UX,避免用户一次次撞进可预见的错误。
Frontend 可以把已知限制投影出来:
Frontend├── 设计允许的操作路径├── 隐藏无关功能├── 展示可理解状态└── 避免不必要的操作错误Backend 才执行真正边界:
Backend├── 检查 Identity├── 检查 Action 与 Resource├── 考虑 Business Context└── Allow 或 Deny例如 invoice:approve 可以告诉 Remote:原则上可以展示 Approve Button。但某一张具体 Invoice 是否真的允许 Approve,还可能取决于当前 Status、Business Unit、Amount Limit 或是否已经批准过。
这不能只从 Login 时签发的 Claim 推导出来,而是受保护 Operation 的业务规则。
Frontend 设计允许的路径。Backend 强制执行它。

因此,Frontend 展示的是一份尽可能有帮助的 Permission Projection,但仍然必须处理被 Backend 拒绝的 Operation。Permission 与业务 State 可以变化,而 Browser 中 Authentication Context 不一定立刻重建。
Login 与 Logout 仍然属于 Platform
Section titled “Login 与 Logout 仍然属于 Platform”Remote 可以表达 Login / Logout Intent,但不会因此自己实现 OIDC Flow。
例如公共区域发现某项操作需要认证用户,就调用 auth.requestLogin()。Platform 决定如何保存当前 State、使用哪个 Redirect URI、如何启动 OIDC Flow。
同样,Remote 可以调用 auth.requestLogout(),真正结束 Authentication Context 和执行 Logout Logic 的仍然是 Platform。
Remote 只响应可见 State:
switch (auth.status) { case 'anonymous': return showLoginHint();
case 'authenticated': return showProtectedContent();}真实应用中还可以有 Loading 或 Error State,但不应该因此每个 Remote 都建立自己的 Authentication State Machine。
中央 Authentication Context 的 Flow、Refresh 与 Error Handling 属于 Platform。否则多个 Remote 可能同时发起 Redirect、同时 Refresh,或者基于不同假设 Logout 用户。
Session 始终属于应用,而不是被分布到各 Remote。
Dynamic Remote 的每个 Test 不需要真实 Login
Section titled “Dynamic Remote 的每个 Test 不需要真实 Login”Dynamic Remote 应该能独立开发和测试,因此有人会推导出:每个 Remote 本地也必须实现完整 OIDC Login。
真正需要独立的是 Contract,不是 Production Implementation。Remote 只需要能在 Host 外初始化自己的 Authentication Integration,而不必复制 Shell 的真实 Auth State。Mini-Host 或 Local Mount Point 可以提供受控 Test Identity,从而直接测试:
- anonymous;
- 普通 User;
- 具有特定 UI Capability 的 User;
- expired 或 unavailable Context。
Mini-Host 不需要真实 Production Token,只需要模拟稳定 User Context 与可预测 API Response。
Mini-Host 模拟 Platform Contract。它不会把 Remote 变成独立 Authentication System。
反而,这会迫使 Contract 变得显式。一个只有拿到具体 OIDC Client Instance 或 localStorage 中特定 Token 才能工作 Remote,并不真正独立;它只是隐式绑定 Production Environment。
看起来像 Autonomy 的东西,经常只是在复制 Infrastructure
Section titled “看起来像 Autonomy 的东西,经常只是在复制 Infrastructure”如果 Authentication 被下放到每个 Remote,并不会自动产生业务自治,只会产生多份同一 Platform Function。
每个 Team 可能选不同 OIDC Library,或同一 Library 的不同 Version。Callback Handling、Token Storage、Refresh 和 Logout 被重复实现;Failure State 各自不同。有些 Remote 立即 Redirect,有些显示 Hint,还有些停留在不一致 Loading State。
对 Identity Provider 的耦合也继续扩大。Provider-specific Claim、Configuration Key 与 Role Structure 扩散进整个产品。以后 Migration 时,需要修改的就不再是一个 Platform Component,而是许多独立发布 Remote。
额外技术自由因此转化为更高 Security、Maintenance 与 Coordination Cost。
看起来像 Remote 多了一份 Autonomy,很多时候只是复制了同一项 Infrastructure Responsibility。
中央 Platform Contract 则建立明确 Owner。Login / Logout 在全产品保持一致,Identity Provider 可以被替换或重新集成,而 Remote 不必知道 Internal Structure。
Contract 更小,Test 更简单。Remote 可以使用定义好的 Identity,而不需要 Production Token。用户体验的是一个应用,而不是一堆互相竞争的 Login Implementation。
Authentication 是 Platform Capability
Section titled “Authentication 是 Platform Capability”微前端应该拆分业务 Responsibility,让团队能够独立演进 Product Area。
这种独立性并不来自把每个 Global Capability 都复制一遍。
对于被 Mount 的微前端,Authentication 属于应用 / Platform。
Platform 拥有 OIDC Client Responsibility,并建立 Authentication Context——无论具体实现是在 Browser 还是通过 BFF。Remote 获得的是小、稳定、framework-neutral Contract:User 与 UI Context,而不是 Raw Token、Redirect URI 或自己的 OIDC Client Instance。独立 Deployment Unit 不会自动变成 OIDC Client,共同 Session 也不是需要在 Remote 之间同步的 State。
这条边界继续延伸到 Backend:UI 可以用 UI Capability 隐藏明显不可用 Function,但受保护 Operation 的最终 Decision 仍由 Resource Server 执行。
Authentication 是 Platform Capability。Authorization 仍然属于受保护 Resource 的责任。
Footnotes
Section titled “Footnotes”-
OpenID Foundation, OpenID Connect Core 1.0 – ID Token 的定义以及它与 Access Token 的区别。 ↩
-
IETF, RFC 9068 – JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens – Access Token 可以使用 JWT 格式,但并非必须。 ↩