☰
OAuth2.0与OpenID Connect核心原理梳理:授权码模式与ID Token实战指南
2026/10/15 1:51:23 网站建设 项目流程

在梳理手头项目的登录模块时,我发现一个很有意思的现象:很多调用方的需求描述就是一句"接入一下OAuth登录",但你再追问一句"用的是OAuth2的哪一种模式?要不要顺手把OpenID Connect加上?"对方多半就答不上来了。我自己也是那种先会用、后补原理的人,最早接第三方登录时就是照着文档往配置里填账号密码、回调地址,跑通了就扔在一边。直到后来有一天要自己搭一套授权服务,才真正把这些协议从头过了一遍。这个系列就是想按我的理解思路把这些东西捋清楚,第一篇先集中讲整体框架和最常用的授权码模式。

先说这两个协议到底在解决什么。OAuth2.0解决的是授权问题,OpenID Connect解决的是认证问题。很多人把"OAuth2和OpenID协议"放在一起讲,是因为实际业务里它们几乎总是成对出现:OAuth2给你一张通行证,让你能访问别人负责保管的资源;OpenID Connect在这张通行证上额外印上你是哪个用户,回答"你到底是谁"的问题。

这篇内容适合谁?如果你是后端开发,尤其在做接口设计、基础平台或者登录模块,又或者你只是想在自己的项目里接一个第三方登录,但不想只听前端的安排,这篇都能帮你想明白事情的原委。看完你应该能回答几个问题:OAuth2里有哪几个角色?为什么大家都推荐授权码模式?OpenID Connect和OAuth2差在哪?跳转过程中那一堆参数各自是干什么的?下面按我的学习顺序展开。

1. 从密码共享到授权委托:协议解决的核心问题

1.1 共享密码的思路为什么走不通

回到最原始的方案。假如有一个第三方应用想帮你打印云端相册里的照片,它怎么拿到你的相册访问权?最直接的办法是:你把账号密码交给它,它拿着你的账号密码去云端相册的接口里取数据。这就是密码共享思路。

稍微推演一下就知道问题有多大。第三方应用拿到的是你完整的账号密码,它不仅能读相册,还能改密码、删文件、看你存在云端的其他一切东西。更糟糕的是,你没法在某个时刻只收回"读相册"这一个权限,只能一揽子把密码改掉,那这家第三方应用从此也就用不了了。密码共享方案把风险全部集中在用户手里:为了保护一个低风险的场景,交出了全部钥匙。时间一长,有些服务会把明文密码留在自己的数据库里,有些会把它写进日志,安全问题是根本收不住的。

还有一层更本质的冲突:第三方应用不是你的服务器,它是另一个独立主体。它和你之间没有天然的信任关系。OAuth2要解决的,就是怎么让资源所有者(你本人)在不交出密码的前提下,把有限范围的访问权授予另一个主体。它不只是改了一个交互方式,而是从底层的授权模型上重新定义了这件事。

1.2 一个更方便理解的类比

有个场景大家几乎都遇过。你去商场开车,车钥匙在你手里。代客泊车员说你把钥匙给我,我帮你停。你把钥匙给他之后,他既能帮你停车,也能把车开走去兜风,还能搬空后备箱。这就是密码共享。

OAuth2要规避的正是这一点。它引入一个中间角色——授权服务器,相当于商场的物业。你作为车主,去物业那里登记:我允许这辆代客泊车专用车把我的车从门口停到B3,仅此一次。物业给你发一张凭证,代客泊车拿着凭证去停车,车钥匙始终在你手里。这里,车主就是资源所有者,代客泊车就是客户端,物业就是授权服务器,停车场就是资源服务器。

这套模型的精髓在于:信任被分散到了不同的边界上。客户端不再拥有用户的身份信息,它只拥有"被授权使用某项能力"的许可。协议里所有概念——授权范围scope、授权码authorization code、访问令牌access token、刷新令牌refresh token——都是围绕这个边界感设计的。我后来再看文档,一旦抓住这条主线,很多疑惑就迎刃而解了。

1.3 认证和授权必须分开理解

在我搞明白之前,一直把认证和授权混在一起,结果看OAuth2文档总觉得哪里不对。现在我用一句话就能分清:

  • 认证(Authentication)回答的是:你是谁。
  • 授权(Authorization)回答的是:你能干什么。

OAuth2本身只做授权。在授权码的交互过程中,授权服务器当然需要确认用户身份才能让他同意授权,但协议并不关心这个"登录"的动作是怎么发生的。它把用户登录当作一个隐含前提。OpenID Connect的作用,就是专门把这环补上,这个区别到第4章再展开。

2. OAuth2的角色模型与四种授权模式选型

2.1 四个角色,边界不能乱

OAuth2规范定义了四个角色,记不住这四个,后面看什么都容易糊:

  • 资源所有者(Resource Owner):拥有受保护资源的人或系统,最常见的就是用户本人。
  • 客户端(Client):想要访问受保护资源的应用程序。这个词容易把人带偏,觉得"客户端"就是浏览器。实际上协议里的客户端可以是网页后端、移动App、桌面软件,也可以是一个纯后端的批处理任务。它代表的是"发起授权请求的一方"。
  • 授权服务器(Authorization Server):负责验证资源所有者身份并颁发令牌的一方。
  • 资源服务器(Resource Server):保存受保护资源的服务,负责校验令牌并决定是否放行。

实际部署里这四个角色经常在同一个进程里。比如你既写了授权服务器,又写了业务接口,那两者从部署层面看是同一个服务,但协议层面角色边界必须清晰,因为redirect_uri校验、token验签、scope判定都依赖这种边界。我见过有人把登录会话管理塞进资源服务器里,结果token过期逻辑和session过期逻辑打架,排查起来非常痛苦。

2.2 四种模式:各自解决的场景完全不同

RFC 6749定义了四种授权模式,它们不是可以随便替换的选项,而是对应不同的客户端环境和信任模型。

模式典型场景核心凭证推荐度
授权码模式(authorization_code)有后端服务的Web应用先用code,后端再换token推荐,绝大多数场景首选
隐式模式(implicit)纯前端单页应用(旧方案)直接在URL返回access_token不推荐,已逐渐被授权码+PKCE替代
客户端凭证模式(client_credentials)服务与服务之间的调用客户端自己作为主体可用,但只适用于没有用户参与的场景
密码模式(password)高信任的内部系统用户直接把账号密码交给客户端慎用,规范注明仅在特殊场景使用

我做个选择题给你:接入方信不信任我们?客户端环境能不能安全保存client_secret?token要不要代表某个具体用户?按这三问,一般就能定位到正确模式。

过去很多人图省事用隐式模式,因为前端跳转一下就直接拿到token,不用后端参与。但token暴露在浏览器地址栏里,历史记录、代理日志、前端脚本都可能泄露,刷新token也没法安全保存。现在主流的做法已经是"授权码模式加PKCE",连单页应用都用这套,隐式模式可以说正在被淘汰。

密码模式我多说一句。它本质上就是当初的"密码共享",只不过在协议层面给了个正式名字。规范里白纸黑字写着,它只适用于客户端和资源所有者之间有高度信任关系且其他模式都不可用的场景。我自己的判断标准是:只有自研的两套内部系统之间,且网络环境可信,才会考虑它。面向第三方开放平台,这个模式能不开就不开。

3. 授权码模式全流程拆解:为什么它是最稳的

3.1 六步流程走一遍

授权码模式的核心思想其实就一句话:token不经过浏览器,只经过后端。完整流程是这样的:

  1. 客户端把用户浏览器重定向到授权服务器的authorize端点,带上自己的client_id、redirect_uri、请求的scope、随机生成的state,以及respone_type=code。
  2. 授权服务器处现用户,要求登录(如果还没登录),显示授权确认页,把用户要授予的权限列清楚。
  3. 用户同意后,授权服务器把浏览器重定向回客户端预注册的redirect_uri,地址栏里带一个一次性code。
  4. 客户端后端收到这个code,拿着它加上自己的client_id和client_secret,直接向授权服务器的token端点发请求。
  5. 授权服务器校验通过,返回真正的access_token,通常还带refresh_token;如果是OIDC流程,还会返回id_token。
  6. 客户端后端用access_token去资源服务器请求数据。

为什么要绕这么大一圈?关键在第4步。code只在浏览器里闪了一下,换token发生在后端服务器之间的直接通信。攻击者就算在浏览器层面截获了这个code,因为他没有client_secret,这个code对他而言就是一段废字符串。而且code是一次性的,有效期也很短,授权服务器还会校验它绑定的client_id和redirect_uri,这多重限制叠在一起,把token暴露在浏览器的风险降到了最低。

3.2 授权请求里那些参数为什么一个都不能省

拿到授权服务器文档时,第一步跳转的URL长这样:

GET /oauth2/authorize? response_type=code& client_id=WEB_APP& redirect_uri=https://example.com/callback& scope=openid%20profile%20email& state=a34fe30d9c1a& code_challenge=K2-ltwP0Z4q3YkQ5aInExample& code_challenge_method=S256

几个关键参数逐个看:

  • response_type=code:告诉授权服务器你要走授权码模式。
  • client_id:客户端的公开标识,相当于你的门牌号。注意它不是密钥,可以暴露在浏览器端。
  • redirect_uri:用户同意后跳回哪里。这个参数必须严格等于你预注册的回调地址,授权服务器要用它来做匹配校验。
  • scope:你要申请的权限范围。OIDC流程里一定要带上openid,这样才能拿到身份信息。
  • state:客户端自己生成的随机字符串,用于防止跨站请求伪造。用户被跳回来的时候,这个参数会原样带回来,客户端要校验它和自己发出去的一致。
  • code_challenge与code_challenge_method:PKCE相关的参数,后面单独说。

state参数我见过太多人直接置空。你想想,如果跳回去的时候没校验state,攻击者可以构造一个带参数的链接诱导用户点击,然后这个链接里带着一个攻击者自己的code,客户端把攻击者的code当成用户的来用,那整个认证链路就对着一个陌生人建立了。state就是为了拦住这种伪造回跳。

3.3 换token这一步的隐藏细节

拿到code之后,后端开始干活。这一步是标准后端到后端交互:

POST /oauth2/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code& code=x0P9qP3wE2jJ& redirect_uri=https://example.com/callback& client_id=WEB_APP& client_secret=some_secret_value& code_verifier=...

grant_type必须写死是authorization_code。把redirect_uri再传一次不是多余的,授权服务器会拿它和authorize阶段传的比对,防止掉包。client_secret通常放在请求体里,更规范的做法是用HTTP Basic认证放在Authorization头里,具体看各授权服务器的要求。

这一段的返回值大概是这样的:

{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "e0e6a1f6-9b6d-4f0a...", "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6..." }

有的同学问过:access_token和refresh_token是不是都要存起来?我建议访问令牌能短就短,最好本地存一份拷贝,真正使用时放内存或短期Cookie里就好。refresh_token的保存位置更敏感,因为它可以换新token,丢了等于授权长期被接管,能放后端就别放前端。

3.4 PKCE:给授权码再加一道保险

最早设计PKCE是为了解决移动端和单页应用这类公共客户端不能安全保存client_secret的问题。它的思路很巧妙:客户端自己生成一个随机串code_verifier,把它哈希后的值code_challenge在授权请求阶段发出去;换token的时候再把原始code_verifier发出去,授权服务器验证两者是否匹配。

翻译成人话就是:授权码就算被人截获了,拿到它也没用,因为换token时要拿出验证器,这个验证器只有真正的客户端才持有,而且它不会出现在任何URL里。

现在很多授权服务器已经默认要求所有授权码流程都带PKCE,包括有后端的Web应用。我建议你也这样要求自己,别嫌麻烦,两行代码的事。

4. OpenID Connect:把认证能力补上来

4.1 协议关系:OAuth2加一张身份证

标题里写的"OpenId",规范名其实是OpenID Connect,一般简称OIDC。它构建在OAuth2之上,目的非常单一:增加认证能力。

一个场景就能说明这个区别。你用某个第三方网站登录一次之后,这个网站拿到了访问你资料的access_token。OAuth2能告诉你"这个token能用,你去调吧",但拿token的人到底是谁?它不知道。OIDC就是在OAuth2的流程里额外签发一个id_token,用这个token告诉你:这位用户是谁、认证是什么时候完成的、他在授权服务器那边被记录成了什么标识。

所以OIDC不是OAuth2的替代品,而是它的一个认证层。两者结合使用时,OAuth2继续负责授权流程,OIDC负责身份信息传递。

4.2 ID Token、JWT和UserInfo端点

OIDC新增的核心产物是ID Token,格式上是JWT(JSON Web Token)。JWT由三段组成:头部、载荷、签名。头部说明签名算法,载荷放用户相关信息,签名保证内容没被篡改。

ID Token里面关键的字段有这些:

  • iss:签发者,要和你配置的授权服务器地址一致。
  • sub:用户的唯一标识,稳定且不会变,以后要用它做用户映射。
  • aud:受众,必须等于你的client_id,防止这个token被用在别处。
  • exp和iat:过期时间和签发时间。
  • nonce:客户端发起认证请求时带的随机串,用来防重放攻击。

我刚开始验ID Token时吃过亏:只验了签名和过期时间,忘了校验aud,等于让一个本来发给别人的身份证明在自己服务里也能通过,这是典型的漏洞。完整校验流程应该是验签、验iss、验aud、验exp、验nonce,一个都不能跳。

除了ID Token,OIDC还提供了UserInfo端点。客户端拿着access_token请求这个端点,可以拿到email、昵称、头像等详细信息。为什么这些信息不一股脑放ID Token里?因为ID Token有签名、有大小限制,只放核心身份信息更安全也更精简,需要更多画像数据时按scope去UserInfo拉就行。

4.3 一个容易踩的概念坑

经常有人搞混ID Token和access_token的用途,我拿个类比说清楚。

access_token是给资源服务器看的"门票",它证明你有权限进某个区域,资源服务器认它。ID Token是给客户端自己看的"身份证",它告诉你用户是谁。ID Token绝不能当成API调用凭证去用,因为资源服务器根本不认识它。反过来,access_token也别当身份凭证存,因为那里面压根不含用户身份信息。

还有一个细节:授权请求里不写scope=openid,OIDC整套流程都不会启动,你只会拿到access_token,不会有id_token。这话听起来简单,但实际项目里真有人配了OAuth2却怎么拿都拿不到id_token,查来查去就是scope漏了。

5. 实操配置:一份能照着填的检查清单

5.1 关键字段速查表

我把自己在接入和搭建授权服务时经常要核对的项目整理成了一张表,遇到问题可以先对着查:

检查项正确做法违规后果
redirect_uri预注册且完全匹配,授权请求和token请求保持一致授权失败或回调被劫持
state每次请求随机生成,回调时严格比对可遭遇CSRF伪造回跳
scopeOIDC场景必须包含openid拿不到id_token
code一次性使用,短时有效,用完立即作废重放攻击
client_secret只出现在后端,不允许进浏览器代码凭证泄露,令牌被冒领
PKCE授权码模式下推荐全程启用公共客户端风险升高
ID Token校验验签、iss、aud、exp、nonce被注入伪造身份

5.2 我踩过的坑和应对办法

和授权服务联调时如果报错,别急着四处问人,先按顺序排查这几点:

  • 跳转回回调页却没有code。看一眼是不是授权请求少了scope或response_type,多数是URL拼错。
  • error=unauthorized_client。通常client_id配置错或者redirect_uri没在授权服务器里预注册。
  • error=invalid_grant。多半是code过期了、用过了,或者token请求里的redirect_uri和授权阶段不一致。
  • 换token后调资源接口返回401。优先查access_token是不是过期,其次查验签用的密钥和算法是否匹配。
  • ID Token验签失败。确认你的jwks_uri地址有没有配错,另外签发算法和验签算法是否一致也要留意。
  • refresh_token换来new token后,老token还能不能用?这取决于服务端是否开了refresh_token rotation。如果开了,旧token一用即失效,你手里存了旧token再拿去换就会被拒。

我自己调试时还有个习惯:先把授权服务器日志打开,看它具体在哪个环节拒绝的请求。大多数OAuth2问题本质是参数不匹配,日志里会写明expected和actual的差异,跟着日志一步步对齐,比瞎猜快得多。

最后再分享一个个人经验。与其在接入第三方时一个参数一个参数地试,不如先花半小时把OAuth2的授权码流程和OIDC的ID Token校验过一遍。state、code、nonce这几个参数背后都是具体的安全攻击场景,读懂它们比背UI的配置步骤有用得多。我第二篇准备深入讲JWT的签名与验签细节,还有把授权服务器从业务代码里剥离开时要注意的边界问题。学习协议没有捷径,但按自己的方式建立一条主线,后面每次遇到新问题都能挂回到这条主线上来,会越学越顺。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询