OIDC认证与Secrets轮换联动实践 - 第3节

OIDC 认证流程:三种模式的深度理解

好,咱们直接切入主题。OIDC 的认证流程,说白了就是客户端怎么去身份提供商(OP)那里拿身份证的过程。

这三种流程——授权码流程(Authorization Code Flow)、隐式流程(Implicit Flow)和混合流程(Hybrid Flow),核心区别就在于令牌怎么交到你手上。

一句话总结:

  • 授权码流程——后端做中间人,最安全,我一般默认选它。
  • 隐式流程——前端直接拿 access ,适合纯 SPA,但后来被 PKCE 替代了。
  • 混合流程——前后端分着拿,高级用法,遇到特殊业务才用。

为什么要有三种流程?

回想一下 OAuth 2.0 的几个 grant type,OIDC 直接沿用了它们。授权码流程对应 authorization_code,隐式流程对应 implicit,混合流程则是两者的组合。你想想看,不同的客户端能力不一样——有的有后端服务器(比如 Web 应用),有的只有浏览器(比如纯静态页面),还有的既需要即时拿到 ID 又想在后台换 refresh 。所以协议得给够选择。

我遇到过不少开发者,上来就说“我用隐式流程,简单”。嗯,后来出了安全漏洞,又得改。所以咱们得把每个流程的坑摸清楚。

三种流程核心对比

维度 授权码流程 隐式流程 混合流程
令牌传递方式 后端通过 code 换取,不暴露给浏览器 浏览器 get 请求直接拿到 fragment 中的令牌 前端拿部分令牌,后端通过 code 拿其余
client_secret 使用 必需(后端凭证) 不使用(全在浏览器) 按需(后端环节使用)
access 泄露风险 低 高(URL 历史、referer 泄漏) 中等
支持 refresh 支持 不支持(不允许) 支持(需后端配合)
适用场景 服务端渲染 Web 应用、原生应用 纯前端 SPA(已被 PKCE 授权码 + code_challenge 替代) 需要前端快速获取 id_ 又希望后端管控 access_ 的场景

授权码流程(Authorization Code Flow)

这是 OIDC 最正统的流程,也是最安全的。核心思想:认证过程在 OP 侧完成,最终只会给客户端一个临时的 code,真正的令牌由后端通过 client_secret + code 换取,全程不经过浏览器。

完整步骤(以 Web 应用为例)

  1. 用户点击“登录”,浏览器被重定向到 OP 的授权端点,带上 client_id、redirect_uri、response_type=code 和 scope=openid。
  2. OP 验证用户身份(比如弹出登录页面),用户确认授权。
  3. OP 将浏览器重定向回你的 redirect_uri,并在 URL query 参数中附上一个 authorization code。
  4. 你的后端服务器接收到这个 code,直接通过后台通道(HTTP POST)向 OP 的 端点发起请求,附上 client_id、client_secret、code、redirect_uri。
  5. OP 验证 client_secret 和 code 合法后,返回 id_ + access_ + refresh_(如果申请了)。
  6. 后端验证 id_ 签名后,建立本地 session,然后响应给浏览器(可以设置 cookie 或者返回 session_id)。

我习惯的做法:把 code 交换令牌的 endpoint 专门限制为只允许服务端 IP 访问,并且强制使用 HTTPS。client_secret 不能硬编码到前端代码里,否则拿了 code 白拿。

避坑指南

我曾经在一个项目中遇到过一个奇葩问题:redirect_uri 在 OP 配置的是 https://example.com/callback,但测试时因为本地开发用了 http://localhost:8080,结果 code 根本发不回来。OIDC 规定 redirect_uri 必须严格匹配,包括端口和协议。所以别忘了在 OP 管理后台把多个合法 redirect_uri 都注册上,或者开发环境用 https 代理。

另外,code 是一次性的,而且有有效期(通常 5-10 分钟)。收到 code 后立即发起交换请求,不要存着回头再用。如果重放攻击,OP 应该拒绝重复的 code。

隐式流程(Implicit Flow)

这个流程是 OAuth 2.0 时期留给纯浏览器应用的解决方案。特点:不经过后端,access 直接通过 URL fragment 回到前端。

步骤

  1. 浏览器重定向到 OP 授权端点,response_type=id_ 或者 id_,具体看你想要什么。
  2. OP 验证用户后,直接重定向回 redirect_uri,在 URL fragment(# 后面)附上 id_ 和/或 access_。
  3. 前端 JavaScript 读取 window.location.hash,提取令牌,然后删掉 hash(避免泄露到历史记录里)。

注意:隐式流程无法使用 client_secret,因为 secret 暴露在浏览器里毫无意义。所以所有安全性都依赖 redirect_uri 的严格校验和 PKCE(但隐式流程原生不支持 code_challenge)。现在主流安全建议已经不再推荐隐式流程了,除非你不得不兼容非常老的 OAuth 2.0 实现。

实操中,access 会暴露在浏览器历史、referer 头里,甚至被中间人截获。我曾经测试过一个国产 SSO 平台,发现他们把 暴露在页面底部的一个隐藏 input 里——这等于开门揖盗。

混合流程(Hybrid Flow)

混合流程有点像“既要又要”。它让你可以在前端直接拿到 id_(用于快速判断用户身份),同时通过 code 在后端安全地换取 access_ 和 refresh_。

触发方式

将 response_type 参数设置为混合值,比如 code id_、code 或者 code id_ 。注意,OIDC 规定混合流程必须包含 code(因为这是授权码流程的基础)。

步骤示意

  1. 浏览器带着 response_type=code id_ 去授权端点。
  2. OP 验证后,一次重定向回来,URL 里同时包含 fragment 中的 id_ 和 query 中的 code。
  3. 前端立即解析 id_,可以立刻展示用户信息(比如用户名、头像)。但注意,此时还不能信任 id_,因为它没有经过后端验证签名(前端没有 client_secret)。
  4. 后端收到 code 后,通过 端点换回 access_ 和 refresh_,并验证 id_ 的一致性(比如 nonce 匹配)。
  5. 后端验证通过后,把真正的用户身份写入 session,前端拿到 session 后再展示敏感数据。

我在设计一个需要即时展示用户昵称、但后续 API 请求又需要高安全凭证的系统时,用了混合流程。前端拿到 id_ 直接显示“欢迎回来,小明”,后端则走 code 换 的完整路径。这样用户体验和安全性都兼顾了。但要注意,id_ 中的 claims 在后端验证之前不能用于任何授权决策。

SVG: OIDC 三种流程对比结构图

OIDC 三种认证流程对比 授权码流程 隐式流程 混合流程 浏览器 → OP 授权端点 OP → 浏览器 (code in query) 后端 → OP (code + secret) OP → 后端 (id+access+refresh) 后端建立 session 浏览器获得 session cookie 浏览器 → OP (response_type=) OP → 浏览器 (access_ in #) JS 读取 hash, 删除 # 前端直接调 API ⚠ 不安全: 暴露在浏览器中 浏览器 → OP (response_type=code id_) OP → 浏览器: ①id_ in # ②code in query 前端解析 id_ 展示 后端用 code 换 access+refresh 后端验证 id_ & 建立 session 绿/蓝色 = 后端通道;红色/橙色 = 浏览器通道;紫色 = 会话建立

安全关键点与个人经验

核心原则:不要在前端暴露任何需要 client_secret 保护的令牌。

我曾经参与审计一个金融级应用,发现开发团队把授权码流程的 client_secret 写在了前端 JavaScript 里,美其名曰“方便调试”。结果扫描工具直接报高危漏洞。记住:client_secret 等同于后端密码,只能放在服务器环境变量或密钥管理服务里。

各流程的选择建议

  • 面向用户的 Web 应用(有后端):无脑选授权码流程 + PKCE。即使你没有 client_secret(比如原生应用),也请使用 PKCE 扩展。我的默认配置就是 authorization_code + S256 code_challenge。
  • 纯前端 SPA 但需要 refresh :用授权码流程 + PKCE,不要被“需要后端”吓到。很多 OIDC 库(比如 oidc-client-ts)都支持在浏览器端执行 authorization code exchange,但缺陷是 refresh 仍会暴露在浏览器内存中。不过比隐式流程好。
  • 混合流程只在有明确需求时用:比如你需要在页面渲染前就拿到 id_ 做路由守卫,同时又需要后端安全地换取 access_。注意验证 nonce 防止重放,并确保前端拿到的 id_ 不包含敏感信息(sub 和 email 就够了)。

nonce 的作用

三种流程都需要检查 nonce。nonce 是个一次性的随机字符串,由客户端生成并传入授权请求,OP 原样放进 id_ 里。客户端验证 id_ 时必须确认 nonce 一致,防止令牌被偷用。我习惯每次认证前用 crypto.randomUUID() 生成一个 nonce,存到 sessionStorage 里。

令牌轮换与联动

这是后面几章会深入的内容。不过这里先提一句:授权码流程天然支持 refresh ,而 refresh 的轮换策略(Rotation)和回撤(Revocation)是安全联动的关键。隐式流程不支持 refresh ,所以一旦 access 过期,用户必须重新登录——体验很差。混合流程虽然支持,但需要后端参与 refresh 过程,不能全在浏览器里做。

嗯,到这里三种流程的基本逻辑和实战要点就覆盖完了。你心里应该已经有数了:选授权码流程做主力,把隐式流程当历史知识了解,混合流程作为高阶工具备用。