恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

OAuth2与OIDC核心概念详解:授权码流程、Token类型与实战

  • 首页
  • 资讯中心
  • /
  • OAuth2与OIDC核心概念详解:授权码流程、Token类型与实战

相关资讯

制造业研发数字化:工艺参数与经验沉淀的技术解法 2026/10/15 15:31:21
C# OpenCvSharp摄像头开发实战:从实时采集到图像处理完整指南 2026/10/15 15:31:21
2026-2032 钥匙胚市场规模与增长预测深度洞察 2026/10/15 15:31:21

最新资讯

C语言银行系统实战:消息队列实现进程间通信与并发控制
智能车摄像头图像处理:五种阈值算法选型与代码实现
把区域建模成服务半径参数而不是计价参数
AI辅助创作占比怎么算、信息增量怎么看:以媒介果发稿流程为参照的审核自查指南
nRF52832 GCC编译环境搭建:工具链选型、Makefile调优与CI集成
爆火的 OpenClaw 到底怎么用?完整安装与使用教程(TaoToken 统一 Key 接入版)

今日推荐

AI视频生成的时间一致性难题与时空建模演进
基于bbox的PDF文档解析:多栏重排与水印过滤实战
探矿数据进RAG前的清洗全攻略:TXT、Word、PDF、网页四类文档处理实践

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

OAuth2与OIDC核心概念详解:授权码流程、Token类型与实战

发布时间:2026/10/15 15:31:21
OAuth2与OIDC核心概念详解:授权码流程、Token类型与实战 做后端这些年我一直把第三方登录当成“一杯奶茶的事”——调接口、拿资料、建账号看起来三分钟就能搞定。直到有个项目要求严格区分“这是第三方用户”和“这是本平台用户”我才发现自己把 OAuth2 和 OpenID 混着用已经很长时间了。所以有了这篇学习记录。这是 OAuth2 与 OpenID Connect 协议学习系列的第一篇主要记录我从零补基础时最困惑的几个问题授权码流程为什么绕、token 到底有几种、OAuth2 和 OIDC 的分界线到底在哪里。适合刚接触这两个协议、或者准备接入第三方登录但总被概念绕晕的后端同学。1. 授权码模式一次登录为什么要来回跳这么多次1.1 四个角色与一个生活类比我刚开始看 OAuth2 时最先被“授权码模式”的跳转次数整懵了用户点了一下登录怎么浏览器先跳到 A再跳到 B再回到自己的服务器最后才拿到一个能用的 token想理解这套流程先把角色分清。OAuth2 里有四个角色资源所有者Resource Owner普通用户决定是否允许第三方应用访问自己的数据。客户端Client我们写的应用想替用户去访问资源。授权服务器Authorization Server存放用户身份、负责登录和授权的中心。资源服务器Resource Server真正保存用户数据、提供 API 的地方。你可以想象成一个储物柜的场景。用户是柜主客户端是一个跑腿小哥授权服务器是储物柜服务台。柜主不愿意把钥匙直接给跑腿小哥小哥也没资格直接打开柜门。于是服务台给小哥一张一次性的“取件凭证”小哥拿着凭证回服务台换成真正的钥匙token再去开柜。这张一次性凭证就是 OAuth2 里的授权码code。1.2 完整流程拆解最常见的授权码流程一共六步。我用自己的话写一遍然后给出真实 HTTP 参数。第一步客户端把用户引导到授权服务器的/authorize端点URL 大概长这样https://auth.example.com/oauth2/authorize?response_typecodeclient_idmy-clientredirect_urihttp://localhost:8000/callbackscopeopenid%20profilestatexyz注意几个关键参数response_typecode告诉授权服务器我要走授权码模式请不要直接返回 token而是返回一个 code。client_id客户端注册时拿到的公开标识。redirect_uri授权完成后回调到客户端的地址必须提前注册且要精确匹配。scope请求的权限范围比如 openid、profile、email。state一个随机字符串后面讲坑的时候会重点说。第二步用户在授权服务器上登录并看到授权确认页点“同意”或“允许”。这一步其实是整个流程里唯一一次让用户正经输密码的地方。第三步授权服务器把用户浏览器重定向回客户端地址是http://localhost:8000/callback?code8f3a2b9c1dstatexyz这里出现的code就是授权码它是短期、一次性、且绑定到某个redirect_uri和client_id的。第四步客户端后端拿到 code 后在服务器端向授权服务器的/token端点发 POST 请求把 code 换成 tokencurl -X POST https://auth.example.com/oauth2/token \ -H Content-Type: application/x-www-form-urlencoded \ -d grant_typeauthorization_code \ -d code8f3a2b9c1d \ -d redirect_urihttp://localhost:8000/callback \ -d client_idmy-client \ -d client_secretmy-secretclient_secret是客户端在授权服务器注册后拿到的机密信息只能存在后端绝不能暴露到浏览器。这一步必须在服务器端完成不能放在前端去发这个请求。第五步授权服务器校验通过后返回 token。响应 JSON 类似{ access_token: eyJhbGciOiJSUzI1NiIs..., token_type: Bearer, expires_in: 3600, refresh_token: 7c9b3a1e5f, scope: openid profile }第六步客户端拿着access_token去访问资源服务器的 API比如请求用户资料接口。请求头里加Authorization: Bearer eyJhbGciOiJSUzI1NiIs...1.3 为什么不能直接在前端发 token很多人第一次看完这套流程都会冒出一个疑问干嘛不直接在授权成功后让我前端重定向回客户端时带上 token何必多一道 code 换 token 的工序因为 OAuth2 最初设计时主要保护的是机密类型客户端也就是后端能安全保存client_secret的 Web 应用。如果授权服务器把 token 直接放在重定向 URL 里token 会出现在浏览器历史记录、服务器访问日志、Referer 头里泄露风险高很多。而授权码是短期且一次性的就算它被截获攻击者也需要同时拿到client_secret才能把 code 换成 token这就把风险压低了不少。另外把 token 换发的请求放在后端还能保证浏览器只接触 code不直接接触长期凭据。对 SPA 这类没法保密的公共客户端OAuth2 又提出了 PKCE 扩展核心是用code_challenge和code_verifier额外做一道绑定验证我后面学习到公共客户端流程时再单独写一篇。2. 三种令牌的职责边界access_token、refresh_token、id_token2.1 到底有多少种 token我最初以为 token 就是一种东西反正拿到就能调接口。实际情况完全不是这样。OAuth2 语境下有两个基础 tokenaccess_token和refresh_token。到了 OIDC又多出一个id_token。这三个东西职责完全不同搞混就会出大问题。令牌类型谁需要消费它生命周期主要用途典型内容access_token资源服务器短分钟到小时级访问资源 API授权范围、会话信息、目标资源标识refresh_token授权服务器长天到周级换取新的 access_token客户端身份、授权范围、续期凭据id_token客户端自己短分钟级证明用户身份认证结果用户标识、签发者、受众、认证时间一句话总结access_token是给资源服务器看的refresh_token是给授权服务器用的id_token才是给客户端确认“眼前这个人是谁”的。2.2 JWT 的基本认知access_token长得可以很随意。有的授权服务器给一个随机字符串数据库里存一份对应信息这叫不透明 token客户端拿它只当字符串用别去解析。有的授权服务器给的是 JWT三段式结构Header.Payload.Signature。JWT 的三段分别是Header声明签名算法和 token 类型。Payload放声明比如用户 ID、过期时间、签发者等。Signature用指定算法对前两段签名防篡改。一个典型 JWT 大概是eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoiYXV0aC5leGFtcGxlLmNvbSIsImV4cCI6MTcxMDAwMDAwMH0.MIIB...签名但我要强调一个坑就算 access_token 是 JWT 格式客户端也不要随便去解析它。因为 access_token 是签发给资源服务器的用什么签名密钥、里面放哪些字段都取决于授权服务器与资源服务器之间的约定。客户端一旦依赖了 token 内部的某个字段而对方某天调整了内容你的代码就会莫名其妙崩掉。正确做法是把 access_token 当不透明字符串直接放到请求头里。2.3 客户端什么时候该用哪个 token在我的学习过程中最实用的判别方式就是看“这个 token 要发给谁”。要调 API走Authorization: Bearer access_token。要续期用refresh_token重新 POST 给授权服务器的/token端点。要确认登录用户是谁靠 OIDC 的id_token。我用 refresh_token 换新 access_token 的标准请求长这样curl -X POST https://auth.example.com/oauth2/token \ -H Content-Type: application/x-www-form-urlencoded \ -d grant_typerefresh_token \ -d refresh_token7c9b3a1e5f \ -d client_idmy-client \ -d client_secretmy-secret返回的响应里一般包含一个新的access_token和一个可选的新的refresh_token。注意有些授权服务器会在刷新时轮换 refresh_token拿到新的就要重新保存旧的失效。3. 我在本地写 Demo 时踩过的三个 OAuth2 坑3.1 坑一回调地址必须精确匹配第一次搭测试环境时我在授权服务器后台把回调地址配成了http://localhost:8000/callback然后本地启动服务时不小心监听的是http://127.0.0.1:8000结果授权服务器每次都说 redirect 地址不匹配。OAuth2 对redirect_uri的校验非常严格通常要求完全匹配。不是域名一样就可以端口、路径、协议、大小写、末尾斜杠任何一点不同都会被拒绝。这其实是安全设计如果允许模糊匹配攻击者就能把授权码重定向到自己的域名。解决办法很简单多配几个精确的回调地址开发环境、测试环境、生产环境各一个http://localhost:8000/callback http://127.0.0.1:8000/callback https://api.example.com/oauth/callback3.2 坑二忘了校验 state等于给 CSRF 留门state参数是 OAuth2 里用来防止跨站请求伪造的。我一开始图省事把它写死成一个固定字符串甚至直接不传。后来看了一篇文章才意识到这意味着什么。场景是这样的攻击者提前构造好一个授权链接诱导用户点击。用户正常授权后授权服务器重定向到攻击者指定的客户端回调。如果回调逻辑只认 code 不认 state攻击者就能拿这个 code 干坏事了。正确做法客户端在发起授权之前生成一个不可预测的随机值存到用户会话里重定向回来时比对state是否与之前保存的一致不一致就直接拒绝。随机值用系统安全随机源生成比如 Node 里的crypto.randomBytes或 Python 里的secrets.token_urlsafe。3.3 坑三access_token 过期后的刷新竞态access_token 一般几十分钟到几小时就过期了refresh_token 则能活好几天甚至更久。过期后客户端用 refresh_token 去换新的 access_token这是正常操作。但我踩过这样的坑多个 API 请求同时发现 access_token 过期同时发起刷新请求结果授权服务器的刷新 token 被用了一次之后失效了立刻返回invalid_grant而其他线程手里还拿着旧 refresh_token。解决方案有两种把刷新操作串行化同一时刻只允许一个刷新请求其他请求等待刷新完成再重试。刷新后立即替换内存中的 access_token 和 refresh_token让所有请求都用最新 token。我本地写了一个简单的 Python 示例把 state、授权请求、换 token、刷新 token 串起来步骤如下import secrets import requests import urllib.parse # 1. 生成不可猜测的 state state secrets.token_urlsafe(16) # 2. 构造授权 URL auth_endpoint https://auth.example.com/oauth2/authorize params { response_type: code, client_id: my-client, redirect_uri: http://localhost:8000/callback, scope: openid profile email, state: state, } auth_url auth_endpoint ? urllib.parse.urlencode(params) print(打开这个地址:, auth_url) # 在实际流程里浏览器会跳转到上面地址用户完成授权后回到回调地址并携带 code 和 state # 3. 回调中校验 state # 假设此时从 URL 里取到了 code 和 state state_from_callback 从回调URL里读取的state assert state_from_callback state, state 不匹配拒绝 # 4. 用 code 换取 token token_endpoint https://auth.example.com/oauth2/token resp requests.post( token_endpoint, data{ grant_type: authorization_code, code: 从回调URL里读取的code, redirect_uri: http://localhost:8000/callback, client_id: my-client, client_secret: my-secret, }, timeout10, ) tokens resp.json() access_token tokens[access_token] refresh_token tokens[refresh_token] print(拿到的 access_token:, access_token) # 5. access_token 过期后用 refresh_token 刷新 refresh_resp requests.post( token_endpoint, data{ grant_type: refresh_token, refresh_token: refresh_token, client_id: my-client, client_secret: my-secret, }, timeout10, ) new_tokens refresh_resp.json() print(刷新后的 access_token:, new_tokens.get(access_token))这段代码只是骨架真正生产环境还要考虑 token 存储、错误重试和并发刷新。但跑通一次这个流程你对协议的理解会扎实很多。3.4 附一个可跑的最小客户端示例再给一个 Flask 回调的例子方便你把上面的流程串起来。核心逻辑就两步回调里校验 code 和 state再去换 token。这里不展示完整代码只把关键部分写出来。from flask import Flask, request, redirect import secrets, requests app Flask(__name__) app.route(/callback) def callback(): code request.args.get(code) state request.args.get(state) # 校验 state 是否等于发起授权时保存的值 # 这里假设保存在 session 中用常量简写 if state ! 之前保存的state: return state 校验失败, 403 token_resp requests.post( https://auth.example.com/oauth2/token, data{ grant_type: authorization_code, code: code, redirect_uri: http://localhost:8000/callback, client_id: my-client, client_secret: my-secret, }, timeout10, ) tokens token_resp.json() return f登录成功access_token 前缀: {tokens[access_token][:20]}...4. OpenID Connect 在 OAuth2 之上到底补了什么4.1 授权和认证不是一回事这个区分我花了不少时间才真正想明白。OAuth2 解决的是“授权”它的主语是你允许什么应用访问什么资源但它并不关心“你是谁”也不负责告诉客户端一个标准化的用户身份。OIDC 解决的问题是“认证”也就是确认用户身份。可以这样理解OAuth2 是“我给你这把钥匙你能开哪个门”OIDC 是“我验证一下你的确是本人然后告诉你我的姓名和编号”。看图可能更直观OAuth2 的角色资源所有者、客户端、授权服务器、资源服务器。OIDC 在 OAuth2 基础上增加了身份层它复用 OAuth2 的授权流程但额外返回一个id_token同时增加了一个UserInfo端点。所以接入 OIDC 时授权请求里的scope必须包含openid否则就退化成一个普通 OAuth2 流程。4.2 ID Token 的验证套路id_token是 JWT客户端拿到后要做验证不能直接信任。验证逻辑如下验签从授权服务器的公钥端点拿到签名公钥确认 JWT 签名有效。校验iss签发者必须是你打算连接的授权服务器。校验aud受众必须包含你的client_id。校验exp和iat过期时间和签发时间过期token直接拒绝。校验nonce和发起认证时传的随机值比对防止重放攻击。一个典型id_token的 payload 长这样{ iss: https://auth.example.com, sub: 1234567890, aud: my-client, exp: 1710000000, iat: 1709996400, nonce: n-0S6_WzA2Mj, auth_time: 1709996400, email: userexample.com, email_verified: true }其中sub最重要它是授权服务器里用户的唯一标识客户端应该用它作为本地用户表关联的键而不是用邮箱。邮箱可能变sub不会。4.3 Discovery 和 UserInfo让客户端“自动适配”OIDC 还有一个很实用的设计发现文档。授权服务器通常会在/.well-known/openid-configuration返回一份 JSON里面列出授权端点、令牌端点、UserInfo 端点、公钥端点、支持的 scope 等。这样客户端可以动态发现配置不用把所有 URL 都写死。对应的配置片段{ issuer: https://auth.example.com, authorization_endpoint: https://auth.example.com/oauth2/authorize, token_endpoint: https://auth.example.com/oauth2/token, userinfo_endpoint: https://auth.example.com/oauth2/userinfo, jwks_uri: https://auth.example.com/oauth2/jwks, response_types_supported: [code, id_token, code id_token], subject_types_supported: [public], id_token_signing_alg_values_supported: [RS256] }当你需要用 access_token 调 UserInfo 端点拿更详细的用户资料时请求也很简单curl -H Authorization: Bearer access_token \ https://auth.example.com/oauth2/userinfo返回的通常是标准化的用户声明比如sub、name、email、locale。所以 OIDC 不只是多了一个 token还提供了一整套身份声明规范。5. 搭一套本地最小授权服务器把流程真正跑通5.1 为什么要自己造轮子我见过很多人接入第三方登录时直接引官方 SDK对着文档填几个参数就调通了。这当然省事但如果出了问题完全不知道协议层哪里报错。反过来说自己搭一套极简授权服务器哪怕功能不完整也能让你把每个 HTTP 请求、每个 token 内容看得清清楚楚。我建议的学习路径是先用手写 HTTP 请求跑通一次完整流程再引入生产级框架。下面的实现是用 Python 的 Flask 做的只用于学习不做任何生产安全保证。5.2 授权服务器的极简实现授权服务器只需要两个端点/authorize和/token。/authorize负责校验 client_id 和 redirect_uri生成一次性 code 并重定向/token负责用 code 换取 access_token。为了模拟 OIDC再顺便生成一个简化的 id_token。from flask import Flask, request, redirect, jsonify import secrets, time, json, base64 app Flask(__name__) CLIENTS { my-client: {client_secret: my-secret, redirect_uris: [http://localhost:8000/callback]} } codes {} tokens {} app.route(/authorize) def authorize(): client_id request.args.get(client_id) redirect_uri request.args.get(redirect_uri) state request.args.get(state) client CLIENTS.get(client_id) if client is None or redirect_uri not in client[redirect_uris]: return 无效的客户端或回调地址, 400 code secrets.token_urlsafe(16) codes[code] {client_id: client_id, redirect_uri: redirect_uri} return redirect(f{redirect_uri}?code{code}state{state}) app.route(/token, methods[POST]) def token(): code request.form.get(code) client_id request.form.get(client_id) client_secret request.form.get(client_secret) client CLIENTS.get(client_id) if client is None or client[client_secret] ! client_secret: return jsonify({error: invalid_client}), 401 info codes.pop(code, None) if info is None or info[client_id] ! client_id: return jsonify({error: invalid_grant}), 400 access_token secrets.token_urlsafe(32) refresh_token secrets.token_urlsafe(32) id_token _make_id_token(client_id, user-123) tokens[access_token] {client_id: client_id, scope: openid profile} return jsonify( access_tokenaccess_token, refresh_tokenrefresh_token, id_tokenid_token, token_typeBearer, expires_in3600, scopeopenid profile ) def _make_id_token(aud, sub): header {alg: HS256, typ: JWT} payload { iss: http://localhost:5000, sub: sub, aud: aud, exp: int(time.time()) 3600, iat: int(time.time()), nonce: fake-nonce, } def b64(data): return base64.urlsafe_b64encode(json.dumps(data, separators(,, :)).encode()).rstrip(b).decode() return f{b64(header)}.{b64(payload)}.fake-signature if __name__ __main__: app.run(port5000)这个实现刻意省略了签名、会话管理、PKCE、刷新 token 校验等内容只保留最核心的流程骨架。你完全可以用它来观察每个参数是如何传递的。5.3 用 curl 走一遍完整流程启动上面的授权服务器后在浏览器打开http://localhost:5000/authorize?response_typecodeclient_idmy-clientredirect_urihttp://localhost:8000/callbackscopeopenid%20profilestateabc123授权服务器会立刻重定向到http://localhost:8000/callback?code生成的codestateabc123浏览器地址栏里看到的 code 就是授权码。然后手动把这个 code 填入下面的 curl 命令curl -X POST http://localhost:5000/token \ -H Content-Type: application/x-www-form-urlencoded \ -d grant_typeauthorization_code \ -d code上一步拿到的code \ -d redirect_urihttp://localhost:8000/callback \ -d client_idmy-client \ -d client_secretmy-secret你会收到带有access_token、refresh_token、id_token的 JSON 响应。把 access_token 放到下面这句里就能模拟访问受保护资源curl -H Authorization: Bearer access_token http://localhost:5000/me虽然示例没有实现/me但思路已经通了。5.4 故意制造错误看协议如何拒绝你学习协议时建议你把能试的错都试一遍把redirect_uri改成没注册过的地址观察授权服务器返回400。拿同一个 code 换两次 token第二次大概率会返回invalid_grant因为 code 已经一次性消费掉了。在/token请求里把client_secret写错看它返回invalid_client。在授权请求里省略state再回来看看回调里 token 是不是也正常发然后问自己这真的安全吗把错误路径也跑一遍你对 OAuth2 的信任模型会记得非常牢。6. 下一步规范怎么读练习怎么做6.1 官方文档阅读顺序学到这里我已经把授权码流程和 OIDC 的核心差异理清了。接下来建议按这个顺序读官方资料不用从头到尾啃但要精读关键章节RFC 6749 的 4.1 节授权码授权和 6 节刷新令牌。RFC 6750理解 Bearer Token 怎么用。OIDC Core 规范里的 Protocol 和 ID Token 部分重点看到底增加了哪些字段。最后再回头看你所使用语言的 SDK 文档这时候你会发现 SDK 背后每一个参数你都大概知道在做什么。6.2 我的实操建议有一个小技巧在浏览器开发者工具里打开“网络”面板然后执行一次第三方登录观察跳转过程中的所有请求尤其是302重定向链和/token请求。这一步能帮你把协议文档和实际流量对上号。另外不要急着在项目里引入重型的授权服务框架先把手写的 demo 跑通哪怕只是 localhost 上的玩具版本。跑通了你对 token 格式、过期时间、state 校验这些细节才有真实体感。最后想说的是协议学习和搭积木很像授权码模式是第一块积木PKCE 是第二块JWT 验证是第三块。这篇记录只完成了前面一部分等我把公共客户端、PKCE、以及如何在真实项目中落地这些内容消化完再继续更新下一篇。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号