OAuth2与OpenID Connect协议详解:授权码模式、ID Token与PKCE实战
发布时间:2026/10/15 3:40:03 锦皓数字建站

这次我准备把 OAuth2 和 OpenID Connect 这套协议体系彻底啃一遍于是整理了一份学习笔记这篇是第一篇。很多后端开发、前端开发者对接第三方登录时都接触过 OAuth2、OpenID 这几个词但真正问起它们之间到底是什么关系授权码模式为什么是主流推荐ID Token 和 Access Token 到底有什么区别时能讲清楚的人并不多。这篇笔记就是围绕这些基础概念展开的把我自己梳理过的思路、动手跑通的流程以及踩过的坑都记下来适合刚接触认证授权体系、或者对接过第三方登录但没系统学过协议细节的同学参考。1. 为什么会有这两个协议授权与认证的本质区别1.1 授权和认证差的不是一星半点很多初学者把 OAuth2 误当成登录协议这是第一个认知误区。OAuth2 的核心是授权OpenID Connect简称 OIDC才是真正解决了认证问题的协议。授权解决的是你能不能做这件事的问题认证解决的是你是谁的问题。我习惯用小区门禁和房卡来类比。认证相当于你进小区门时出示身份证保安核验你的身份确认你是你。授权则是你到某栋楼的某套房门前用房卡开门房卡决定了你能开哪扇门、能进哪个房间它不关心你是谁只关心你有没有对应的权限。OAuth2 就是这张房卡的分发机制OIDC 则是在发房卡之前先帮你完成身份核验然后附带给你一张身份证复印件。如果不区分这两个概念后面看 OIDC 的文档会越看越乱尤其是很多流程里既有 Access Token 又有 ID Token搞不清楚各自用途的话一旦写错就会留下安全隐患。1.2 OAuth2 诞生背后的真实痛点早些年的应用对接第三方资源时最常见的做法是直接把密码交出去。比如某个网站要读取你的通讯录最简单粗暴的方式就是你把自己的账号密码给它它直接拿你的密码去登录、去拉数据。这种做法问题很大密码被第三方应用保存泄露风险极高而且你没法控制它只读取通讯录万一它把你的邮件、相册、好友关系全部拉走了呢更可怕的是如果你想收回它的访问权只能修改密码代价极大。OAuth2 的诞生就是为了解决这种场景。它提出了一种委托授权模型资源所有者也就是用户自己不用交出密码而是向授权服务器确认我同意这个应用代表我去获取某一部分数据授权服务器签发一个令牌第三方应用拿着这个令牌去访问资源服务器。资源服务器只认令牌不认密码。这就像你去酒店办理入住前台给你一张房卡这张房卡只能打开你房间的门不能打开别人的房间也不能打开酒店库房。你退房时房卡自动失效不需要换锁。OAuth2 的令牌设计就借鉴了这个思路细粒度授权、可撤销、有时效性从机制上替代了交密码这种极端方案。1.3 OIDC 补上的那块拼图OAuth2 框架本身没有定义用户身份信息的标准格式不同厂商给出的用户信息接口五花八门返回的字段也千差万别。这就导致纯 OAuth2 的授权服务器无法统一解决用户登录的需求。OIDC 在 OAuth2 的基础上增加了一层身份层它以 OAuth2 授权流程为载体额外签发给客户端一个 ID Token这个令牌是一个经过签名的 JSON 结构里面包含用户的标识符、签发时间、过期时间以及发行者信息客户端可以通过标准化的方式来校验用户身份。可以这么理解OAuth2 只解决了把你的数据委托给第三方应用这种授权问题OIDC 则在同一个流程里解决了第三方应用怎么确认当前用户是谁的认证问题。很多平台提供第三方登录时背后真正跑的就是 OIDC 协议。我们常说的使用某账号登录某网站这个场景下认证身份靠的是 OIDC授权访问用户资料的能力靠的是 OAuth2两者恰好互补。2. OAuth2 核心角色与授权流程2.1 四个角色先把它背下来OAuth2 规范里定义了四个角色所有流程都是围绕这四个角色展开的。资源所有者Resource Owner指拥有受保护资源的实体也就是普通用户。客户端Client指请求访问用户资源的第三方应用比如某个待办事项 App 需要同步你的日历数据。授权服务器Authorization Server负责验证资源所有者身份并向客户端签发令牌。资源服务器Resource Server托管受保护资源的服务说白了就是 API 服务端负责校验访问令牌并返回数据。这四个角色缺一不可。授权服务器和资源服务器在物理上可以是同一套系统也可以分开部署比如同一个平台内部用一套授权服务统一发令牌业务 API 作为资源服务器各自接收令牌并校验。实际对接中最容易被忽略的是授权服务器和资源服务器是不同主体的场景比如客户端拿到的令牌是下游服务签发的但业务 API 是另一个团队维护的这时候令牌校验方式如果不一致就会出现拿了好令牌却调不通接口的问题。2.2 授权码模式最正统的完整流程OAuth2 一共定义了多种授权模式其中最经典、最常用、也是所有规范文档里重点推荐的是授权码模式Authorization Code Flow。它的完整流程可以梳理成这样首先客户端把用户引导到授权服务器的登录页URL 上会带上客户端标识client_id、重定向地址redirect_uri、请求的权限范围scope以及一个随机生成的 state 参数。接着用户在这个页面上登录并确认授权。授权服务器验证通过后通过重定向把用户带回客户端指定的回调地址问号后面带着一个授权码code和之前那个 state 值。客户端在拿到授权码之后在后端直接发起一次 HTTPS 请求把授权码、client_id、client_secret客户端密钥发送给授权服务器的令牌端点token endpoint。授权服务器核验无误后返回两种令牌访问令牌Access Token和刷新令牌Refresh Token。最后客户端带着访问令牌去请求资源服务器的 API拿到用户数据。为什么要绕这么大的圈子不直接返回访问令牌呢关键在于安全边界。授权码模式下访问令牌是通过后端换取的前端浏览器全程看不到令牌授权码本身是一次性的、有效期极短即使被截获没有 client_secret 也无法兑换令牌。客户端密钥必须保存在服务器端绝不能出现在浏览器端或移动端包里这个原则是整个授权码模式的基石。2.3 其他授权模式的取舍哪些还在用、哪些被淘汰除了授权码模式OAuth2 还定义了隐式模式Implicit Flow、密码模式Resource Owner Password Credentials Flow和客户端凭证模式Client Credentials Flow。隐式模式当年是为纯前端应用设计的授权服务器直接通过重定向返回访问令牌没有授权码这层交换过程。问题是访问令牌暴露在浏览器 URL 里容易泄露而且这类应用没法安全保存客户端密钥导致令牌伪造和泄露的风险很高。现在的安全规范已经不推荐使用隐式模式了新版 OAuth2.1 草案甚至直接把它排除在外。如果你的新项目还在用隐式模式建议尽快迁移到授权码模式加 PKCE 的方案上。密码模式本来是给官方客户端用的用户直接把密码交给客户端客户端用密码向授权服务器换令牌。这和 OAuth2 最初想解决的不交密码问题背道而驰只有授权服务器和客户端完全信任、且其他流程不可行时才能考虑。OAuth2.1 同样将它废弃了。客户端凭证模式适合没有真实用户的机器对机器场景比如服务之间互相调用。客户端用自己的凭证直接换令牌没有用户参与。这个模式本身没有安全问题但要注意令牌对应的权限范围要收敛、密钥要妥善保管。我见过有些后端为了省事所有服务间的调用都用一个长期有效的令牌一旦泄露就是全线崩溃教训很深刻。2.4 PKCE把授权码模式的安全短板补上授权码模式虽然安全但对原生应用手机 App、桌面客户端来说有个适配难题这类应用无法安全保存 client_secret。如果没有 client_secret攻击者拿到授权码后任何人都可以去令牌端点换取访问令牌授权码的安全性就断了。PKCEProof Key for Code Exchange就是为了解决这个场景。它的核心思想是客户端在发起授权请求时先自己生成一个随机字符串 code_verifier然后通过一定的哈希算法变换成 code_challenge 一并发送。授权服务器保存这个 challenge。拿到授权码后客户端再带着原始的 code_verifier 去换令牌。授权服务器会验证 code_verifier 是否与之前保存的 challenge 匹配。这样一来即使授权码被截获攻击者没有原始的 code_verifier照样换不到令牌。PKCE 现在已经从推荐使用变成了默认必需即使是有 client_secret 的 Web 应用也不妨把 PKCE 加上等于多加了一道防线。我在实际项目中习惯所有新接入的客户端都强制开启 PKCE省心且安全。3. OpenID Connect在授权之上定义标准身份层3.1 ID Token一张签过名、能防篡改的身份证OIDC 对 OAuth2 最核心的扩展就是引入了一个新的令牌类型ID Token。它是一个 JWTJSON Web Token结构上分为三段头部、载荷、签名。头部声明签名算法载荷承载身份声明claims签名用于保证内容在传输过程中未被篡改。一个典型的 ID Token 载荷大概长这样{ iss: https://auth.example.com, sub: user-123456, aud: web-client, exp: 1713600000, iat: 1713596400, nonce: a1b2c3d4 }其中 iss 是发行者标识sub 是用户的唯一标识符aud 表示令牌的接收方即客户端 IDexp 和 iat 分别是过期时间和签发时间nonce 是防重放攻击的随机数。客户端拿到 ID Token 后第一件事就是校验签名第二件事是校验 iss、aud 是否匹配第三件事是校验 exp 是否过期必要时还要校验 nonce 是否与发起授权请求时发送的 nonce 一致。有个关键点必须记住ID Token 只能用来确认用户是谁这个事实不能拿它去调用 API。调用 API 用的是 Access Token。我最初学的时候经常混淆后来统一了口径ID Token 是证明身份的Access Token 是拿权限的。3.2 OIDC 三种流程与 OAuth2 的对应关系OIDC 协议定义了三种流程分别对应不同的客户端场景。Authorization Code Flow对应 OAuth2 授权码流程适合传统的 Web 应用和服务端应用。Implicit Flow对应 OAuth2 隐式流程适合纯前端应用但和 OAuth2 一样现在已经不推荐使用。Hybrid Flow混合流程是 OIDC 独有的它同时返回授权码和 ID Token允许客户端提前获取部分身份信息同时再用授权码换取 Access Token适合对安全性要求较高、又需要体验优化场景。选择流程时我的建议很简单凡是服务端参与的一律用 Authorization Code Flow 加 PKCE纯浏览器场景因为隐式流程不安全尽量也别用想办法引入后端做代理。混合流程虽然功能强但涉及多种令牌组合校验复杂度高新手不建议从它入手。3.3 从授权码到身份信息OIDC 的关键端点与发现机制OIDC 在 OAuth2 的授权端点、令牌端点之外新增了几个关键端点。UserInfo 端点用于获取用户资料客户端用 Access Token 请求它返回用户的昵称、头像、邮箱等信息。Discovery 端点则通过一个标准化 URL通常是 /.well-known/openid-configuration返回整套配置包括授权端点地址、令牌端点地址、支持签名算法、支持的 scope 等。这套发现机制相当实用。以前对接一个平台要到处翻文档找各种端点地址现在只要知道它的 issuer 标识就能通过 discovery 文件一站式拿到所有配置。很多开发框架比如各种 oidc-client 库都支持直接读取 discovery 文件并自动配置大大降低了接入成本。值得注意请求 OIDC 授权时scope 参数里必须显式带上 openid否则授权服务器只会把它当成普通的 OAuth2 请求处理不会返回 ID Token。这是新手最容易忽略的一个细节我见过有人调了半天接口发现返回的数据里只有 Access Token最后排查发现就是 scope 漏写了 openid。4. 实操记录搭一个最小可用的模拟项目X授权服务器4.1 技术选型与准备工作理论看了不少不动手跑一遍等于白看。我决定搭一个最小可用的授权服务器来验证流程项目代号就叫模拟项目X。技术选型上我选了一个轻量的开源 OIDC 服务端库作为授权服务器配合本地环境的密钥托管工具来模拟真实配置。为什么不用在线第三方平台因为在线平台涉及内网回调问题调试复杂自建一套本地环境可以把整个协议流程完整跑通。我采用本地回环地址作为回调地址这样浏览器跳转和令牌交换整个链路都在本机完成排查问题最方便。模拟项目X的代码结构分三块授权服务器、受保护资源 API、模拟客户端页面。三者独立运行用不同端口区分。4.2 客户端注册与配置参数在授权服务器上注册一个客户端配置如下配置项值说明client_iddemo-web-client客户端标识client_secret仅在服务端保存不暴露给浏览器用于令牌端点认证redirect_urihttp://localhost:3000/callback授权码回调地址scopeopenid profile emailopenid 必填profile 和 email 是用户资料范围授权方式Authorization Code PKCE符合最新安全实践token 有效期Access Token 30 分钟Refresh Token 7 天常规配置这里有三个配置细节值得展开说。redirect_uri 必须和授权请求里携带的 redirect_uri 逐一匹配授权服务器会做完全相等的字符串比对加一个斜杠都会失败。scope 里的 openid 是最关键的它是 OIDC 流程的扳机缺少这个参数就拿不到 ID Token。client_secret 只在服务端使用如果浏览器端能看到整个客户端的信任基础就崩塌了。4.3 授权流程完整跑通请求、回调、换令牌我动手构建并发送了授权请求URL 长这样http://localhost:8080/authorize? response_typecode client_iddemo-web-client redirect_urihttp://localhost:3000/callback scopeopenid%20profile%20email statexyz123 code_challengeKjQm... code_challenge_methodS256response_typecode 表示走授权码模式。state 参数是客户端生成的随机值授权服务器原样带回用来防止跨站请求伪造。code_challenge 和 code_challenge_method 是 PKCE 的核心参数S256 表示对 code_verifier 做一次 SHA-256 哈希之后再进行 Base64 URL 编码。流程走到授权服务器登录页我输入模拟用户名密码之后页面跳出授权确认框列出客户端申请读取的权限范围。点击确认后浏览器被重定向到回调地址我实际观察到的回调 URL 是这样的http://localhost:3000/callback?codeAUTH_CODE_123statexyz123注意这个 code 就是授权码。我立即在命令行用 curl 模拟客户端后端把授权码发给令牌端点curl -X POST http://localhost:8080/token \ -H Content-Type: application/x-www-form-urlencoded \ -d grant_typeauthorization_code \ -d codeAUTH_CODE_123 \ -d redirect_urihttp://localhost:3000/callback \ -d client_iddemo-web-client \ -d client_secretYOUR_SECRET \ -d code_verifierORIGINAL_VERIFIER_STRING授权服务器返回的响应包含 access_token、id_token、refresh_token 和 token_type。我专门打印了 id_token并把它解析出来逐字段核对。整个过程最让我意外的点是授权码只能用一次第二次拿同一个 code 去请求令牌端点会直接报错这个一次性约束是重要的安全防线。4.4 ID Token 校验的完整步骤拿到 ID Token 后我按照规范文档的推荐步骤手动做了校验这个步骤值得每个做接入的人亲手过一遍。第一步是解析 JWT 的三段结构确认头部里的算法是否与预期一致这里要特别提防算法混淆攻击比如明明配置的是 RS256结果收到一个 alg 为 none 的令牌。第二步是验证签名用授权服务器的公钥对签名做验签。公钥可以通过 discovery 文档里的 JWKS 地址获取。第三步是校验 iss必须与授权服务器的 issuer 标识完全一致。第四步是校验 aud必须包含客户端的 client_id。第五步是校验 exp并留出一定的时间偏移量防止客户端和服务器之间的时钟偏差导致误判。第六步是校验 nonce这个值必须和授权请求中发送的 nonce 一致否则就可能是重放攻击。我把这套校验逻辑写成了一段脚本用库做 JWT 验签再手动比对 iss、aud、exp。跑通的那一刻我才真正意识到文档里反复强调的信任边界是什么意思每个校验项都是在回答一个安全问题这个令牌是你签发的吗是发给我的吗过期了吗是本次请求产生的吗5. 我在学习过程中踩过的坑常见问题排查实录5.1 redirect_uri 不匹配查了半天是个斜杠的锅我在第一次配置客户端时注册的 redirect_uri 是 http://localhost:3000/callback但授权请求里因为拼接字符串时手滑多加了一个尾部斜杠变成了 http://localhost:3000/callback/。授权服务器直接报错 redirect_uri_mismatch。这类问题排查时先别怀疑协议复杂把注册值和请求值逐字符对比一遍空格、斜杠、大小写都不能放过。对应到日常工作中如果用户反馈登录跳回后页面报错第一反应应该是去授权服务器的日志里看拒绝原因大多数时候都是重定向地址不匹配。我建议团队在客户端注册环节就做一次强制格式化把用户输入的 redirect_uri 统一去掉尾部斜杠这样能从源头减少配置错误。5.2 ID Token 校验失败原因是对不上号某次我用一个测试客户端收到了 ID Token验签、日期都正常但总是报 audience 不匹配。我对监控数据检查后发现测试客户端注册的 client_id 是 demo-cli但授权请求里我手动拼 URL 时误写成 demo-cli-test。授权服务器签发的 ID Token 里 aud 值就是 demo-cli-test而我的校验逻辑里用的还是旧的 demo-cli自然对不上。audience 校验是 OIDC 安全模型里极重要的一环。如果客户端不校验 aud攻击者可以用另一个客户端的 ID Token 来冒充合法用户。所以我后来都会把校验逻辑拆成独立模块把客户端 ID 作为参数传入而不是硬编码减少这类低级失误。5.3 PKCE 和 state 的典型使用误区我在测试 PKCE 时踩过一个经典坑先把 code_verifier 写在授权请求的前端代码里后来为调试方便把 code_verifier 打到了日志里。结果发现日志系统会把请求参数原样记录一旦日志泄露攻击者同时拿到授权码和 code_verifierPKCE 就形同虚设。正确做法是 code_verifier 只在内存中做短期保存任何日志、上报链路里都不得出现。state 参数的使用误区也很常见。有人把 state 当成简单的随机数校验时不认真。state 的定位是 CSRF 防护它绑定的是本次授权请求与会话的关系。校验逻辑应该是回调里收到的 state 必须与发起请求时存放在会话里的 state 完全一致。如果发现自己写的是只要不为空即通过那这个防护等于没做。6. 容易被忽略的安全实践与后续学习方向6.1 几个值得马上改掉的习惯每次令牌交换必须走 HTTPS 明文传输令牌在任何环境里都不可接受。测试环境没有 HTTPS 证书至少用本机回环地址模拟不能拿真实的非加密 HTTP 接口对接生产平台。Access Token 的权限范围要最小化。很多应用一申请就是全部权限实际只用到其中一两个一旦令牌泄露攻击者能做的就太多了。每次授权请求只申请当前功能真正需要的 scope长期来看是成本最低的安全投入。Refresh Token 的保存位置和有效期设计也要重视。前端任何地方都不该出现 Refresh Token它比 Access Token 权限更大、有效期更长泄露等同整个账号授权被接管。移动端建议存在系统安全存储区Web 端则放在服务端会话由服务端代理续期。6.2 OAuth2.1 与后续演进方向OAuth2 的规范是老版本了OAuth2.1 草案把多年来实践中总结的安全建议整合成了正式要求它有几个明显变化隐式流程被移除密码模式被移除PKCE 对所有类型的客户端都成为必选精确的 scope 匹配、token 有效期限制也都被更严格地约束。团队新项目如果现在开始做授权体系与其按老 OAuth2 资料去搭不如直接按照 OAuth2.1 的安全基线来设计这样未来规范正式发布时迁移成本最小。另外可以关注 JWT 在访问令牌场景的使用边界虽然很多平台用 JWT 作访问令牌但 JWT 一旦签发在过期前无法撤销适合配合短有效期和刷新令牌轮换机制使用不能拿来当长期凭证。这次学习记录先写到这。我自己最大的体会是这类协议光靠看文档很容易产生好像懂了的错觉真正动手跑一遍授权码加 PKCE 的流程、亲手验签一个 ID Token、再亲手把一个校验不过的错误改对很多概念才真正连成线。下一篇我打算把刷新令牌的轮换机制和单点登录的实现细节单独拎出来做一组实验到时候再继续把笔记补上。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。