关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

OAuth 2.0、OIDC 和 SAML 到底分工在哪:按客户端形态选,而不是按"哪个更安全"

发布时间:2026-10-21

"单点登录"这一个词,其实盖住了三件不同的事

甲方招标文件里白纸黑字写着「支持 SAML 2.0 单点登录」,乙方技术方案里通篇是 OIDC 与授权码流程,评审会上两边各执一词,谁都觉得对方没看懂需求。这种场面在身份系统对接中出现得极频繁,而争论本身往往建立在同一个误解之上:把「单点登录」当成一个确定的技术动作。它至少盖住了三件不同的事——用户是谁、用户同意把哪些权限交出去、以及这个结论怎么从身份提供方手里送到业务系统手里并被人信任。

第一件事叫认证,回答的是「当前操作者是不是他声称的那个人」。第二件事叫授权,回答的是「这个应用被允许访问哪些资源、到什么程度」。第三件事叫传递与校验,回答的是「业务系统凭什么相信这张凭据,凭据过期或作废之后怎么办」。OAuth 2.0、OpenID Connect 1.0、SAML 2.0 分别落在这三件事的不同位置上,有时还会叠加使用。所以上面那个评审会里,两边其实都没说错,只是说的不是同一层。

把层次分清之后,很多争论会自然消失。甲方要的往往是一句话:员工在浏览器里点开某个业务系统,不用再输一次密码。这句话只描述了体验,没有描述实现。乙方要的是另一句话:这个系统要能被移动端、命令行工具和后端服务同时接入。两句话都没有指定协议,指定协议的是更后面的那些约束——客户端能不能保守密钥、有没有浏览器可用来跳转、凭据要在哪里被校验、作废时谁能立刻知道。这些约束才是选型的起点,安全强度反而不是。

OAuth 2.0 是授权框架,不是登录协议

OAuth 2.0(RFC 6749)的标题里写得很清楚,它是一个授权框架。它定义了四种经典流程:授权码、隐式、资源所有者密码、客户端凭据,目的是让客户端拿到一枚 access token,然后带着这枚 token 去访问资源服务器。整个框架围绕四个角色展开:资源所有者、客户端、授权服务器、资源服务器,token 上挂的 scope 描述的是「能访问什么」。

关键在于,OAuth 2.0 本身并不规定用户身份信息的标准格式。一枚 access token 只说明「某个客户端在某个范围内获得了访问许可」,它不说明这个用户叫什么、工号是多少、属于哪个部门。不同授权服务器返回的 token 内容各不相同,业务系统拿到之后没有一个统一的地方去读身份字段。于是行业里出现过大量「用 OAuth 2.0 做登录」的土办法:拿 token 去调一个自定义接口换用户信息,然后各自约定字段含义。这种做法能用,但它把身份语义留在了每家自己的约定里,跨组织对接时每次都要重新谈一遍。

四种流程的适用面也值得单独说清。授权码流程把令牌交换放在服务端完成,是通用性最强的一种;隐式流程当初是为无法保守密钥的纯前端客户端准备的,令牌直接经浏览器返回,如今已被更安全的做法取代;资源所有者密码流程要求客户端直接接触用户密码,只适合自家第一方客户端且无法支持多因素认证的过渡场景,新项目不应再采用;客户端凭据流程里没有用户,是服务之间互相调用的形态。选流程时先问「有没有用户参与」和「客户端能不能保密」,两个答案基本就锁定了流程种类。

把授权当成认证用,还会带来几个具体的麻烦。一是受众不明:一枚为 A 系统签发的 token,如果 B 系统只验签名不验受众,就可能被拿来用。二是身份字段无标准:注销、改名、组织调整这些事件没有统一的传递方式。三是有效期与吊销语义缺失:token 过期了只能让用户重新走一遍流程,业务侧没有「这个人的身份已经变更」的通知渠道。这些问题不是 OAuth 2.0 的缺陷,而是它被用在了设计目标之外。

所以真正该记住的分界是:OAuth 2.0 解决「代持钥匙」的问题——用户不把密码交给第三方应用,而是让第三方拿到一把有时限、有范围的钥匙。至于「钥匙的主人是谁」,它不负责回答。这个回答由它上面的那一层补上。

OIDC 在 OAuth 2.0 之上补的正是"身份"这一层

OpenID Connect 1.0 是在 OAuth 2.0 之上叠加的身份层。它没有另起炉灶设计一套流程,而是复用了 OAuth 2.0 的授权码流程,在请求里多要一个东西:ID Token。ID Token 是一枚 JWT,里面带有签发方、主体、受众、签发时间、过期时间等标准字段,业务系统拿到它就能确认「谁在什么时候、对哪个应用完成了认证」。认证请求里还可以带 nonce,用来把 ID Token 与本次请求绑定,防止重放。

除了 ID Token,OIDC 还提供了几个把对接成本压下来的配套件。UserInfo 端点让客户端在需要更多用户属性时,用 access token 去换一份标准格式的属性集;Discovery 文档(位于 .well-known/openid-configuration)把授权地址、令牌地址、公钥地址、支持的流程与算法全部列出来,客户端可以自动发现,不用手工抄一堆配置项;JWKS 密钥集则以标准格式提供验签公钥,密钥轮换时业务系统不需要改配置。这几样加在一起,才让 OIDC 在跨组织对接里比「自定义用户信息接口」省事得多。

标准字段里值得单独拎出来的是受众与有效期。受众字段标明这枚 ID Token 是签给哪个客户端的,业务系统必须逐一比对而不是只看签名;有效期通常很短,因为它承载的是「刚刚完成了一次认证」这个瞬时结论,后续的用户状态要靠 access token 与刷新令牌维持,两者分工不同。nonce 则要由客户端在发起认证时生成并留存,收到 ID Token 后比对一致才算完成绑定,这一步省略的话,截获来的 ID Token 有可能被重复使用。

从部署视角看,OIDC 的认证流程与 OAuth 2.0 的授权流程是同一次交互的两份产出:一次授权码交换,既拿到 access token 用于访问资源,又拿到 ID Token 用于确认身份。这也是为什么实际项目里很少有人单独部署 OIDC——它通常以授权服务器的一个能力开关存在。理解这一点,就不会再问出「我们该上 OAuth 还是上 OIDC」这种问题:前者是地基,后者是地基上的一层,绝大多数情况下两者同时存在。

SAML 2.0 解决的是浏览器里的跨域断言

SAML 2.0 走的是另一条路。它基于 XML,由身份提供方(IdP)向服务提供方(SP)签发断言,典型用法是浏览器里的 Web 单点登录,通过 POST 或 Redirect 绑定把断言从 IdP 送到 SP。用户在 IdP 完成登录后,浏览器带着一段签过名的 XML 回到业务系统,业务系统验签、检查条件与受众,然后建立自己的会话。

这个模型与 OIDC 的差别不在安全强度,而在承载方式。SAML 的报文是 XML,体量明显偏大,依赖 XML 签名校验;它天然贴着浏览器重定向这条链路设计,用户全程在页面之间跳转,服务端之间通常不需要直接互通。对接双方要交换的是元数据文件与证书,元数据里写清了实体 ID、端点地址、绑定方式与签名算法,配置一次就能长期运行。这也是它在企业内部办公系统、跨组织 Web 应用联邦里沿用多年的原因:需求恰好就是「浏览器里跳一次,之后各自维护会话」。

发起方式也要在对接前谈清楚。由服务提供方发起时,用户先访问业务系统,系统生成请求并把浏览器重定向到身份提供方,这条路径最常见也最符合直觉;由身份提供方发起时,用户先登录门户再点应用图标,身份提供方主动把断言推给业务系统。后者在没有请求上下文的情况下收到断言,重放防护与请求绑定的实现要格外小心,对接时通常要求对断言编号做去重记录,并对时间窗做严格控制。这两种方式是否需要同时支持,应当在联调前就写进接口约定。

反过来看它不擅长的地方:移动端与后端服务之间没有浏览器可跳,命令行工具更是如此;XML 解析与签名校验在资源受限的客户端上是额外负担;移动端 App 里内嵌 WebView 做跳转还会牵扯到凭据暴露与会话隔离的问题。所以不是 SAML 落后,而是它的主场是浏览器里的跨域 Web 单点登录,出了这个主场,它的优势就不明显了。选型时把「有没有浏览器、要不要跨组织联邦」这两个问题先答出来,答案往往自己就出来了。

令牌与断言的形态,直接决定了后端怎么校验

很多人把协议选型理解成「换一个库」,实际上换掉的是校验位置和校验成本。OIDC 的 ID Token 是 JWT,自包含:签名、签发方、受众、有效期都写在里面,业务系统本地就能完成校验,不需要每次回调身份提供方。代价是自包含也意味着「发出去就收不回」——JWT 一旦签发,在过期之前服务端没有天然的方式让它失效。要支持吊销,就得额外配机制:缩短有效期配合刷新令牌、调用令牌内省端点、或者维护一份黑名单。这三条路各有成本,短有效期会让内省或刷新更频繁,内省把本地校验变成了一次网络调用,黑名单则要求所有校验点共享同一份状态。

SAML 的断言是一次性的:浏览器送过来、验签通过、换成本地会话,之后身份提供方不再参与。校验的重点落在 XML 签名上,包括证书信任链、签名覆盖范围,以及断言里的时间条件——NotBefore 与 NotOnOrAfter 构成的生效窗口,还有受众限制条件。XML 这一层还需要额外提防解析侧的坑:外部实体引用、签名包裹一类的结构攻击,都要求校验代码严格限定解析范围并明确指定签名所覆盖的节点,而不是「解析完再看有没有签名」。

校验顺序也容易被写错。本地校验 JWT 时,正确顺序是先确认令牌结构与算法在允许清单内,再验签名,然后才读里面的字段去比对签发方、受众与有效期;反过来先解析再验签,等于把不可信内容先喂给了解析器。SAML 侧的对应要求是:先确定签名节点与参考范围,完成签名校验,再解释断言内容。两边共同的原则是「先建立信任,再解释内容」,这条原则写成代码评审的检查项会省掉很多返工。

还有一种常见形态是不透明令牌:token 本身只是一串无意义的标识,业务系统必须拿着它去授权服务器做内省才能知道有没有效、属于谁。这种形态把校验集中到了一个点,好处是吊销即时生效,代价是每次校验都多一跳网络调用,那个内省端点也就成了必须重点保障可用性的组件。把它与 JWT 放在一起对比,架构上的取舍就很清楚:JWT 把能力下放到每个校验点、牺牲即时性;不透明令牌把能力收回中心、牺牲延迟与可用性依赖。

这三条路对应三种后端结构。JWT 适合在网关层统一校验,业务服务只信网关传下来的结果;不透明令牌要求所有服务都能访问内省端点,或者由网关代做内省;SAML 则只在建立会话的那一次交互里出现,之后完全靠自己的会话体系运行。先想清楚「凭据在哪里被校验」,比纠结协议名字有用得多。

按客户端形态选:服务端 Web、单页应用、移动端、命令行与机器对机器

真正决定流程的,是这个客户端能不能保守密钥,以及它有没有可用的浏览器。这两条判断线比任何安全强度的比较都实在。

服务端 Web 应用:代码跑在服务器上,客户端密钥可以安全保存,用授权码流程并配合 PKCE 是标准做法。授权码经浏览器回传、令牌交换在服务端完成,令牌不必暴露给前端,这是几种形态里最干净的一种。

单页应用:全部代码都在浏览器里,没有任何地方能藏住密钥。历史上常用的隐式流程会把令牌直接放在重定向片段中,容易通过浏览器历史、引用地址、第三方脚本等途径泄漏,因此现在不再推荐,改为授权码流程配合 PKCE。PKCE 用一次性校验值把「发起授权的那个客户端」与「换取令牌的那个客户端」绑定起来,即使授权码被截获,没有原始校验值也换不出令牌。

移动端:同样无法保守密钥,同样推荐授权码加 PKCE,而且应当使用系统浏览器或系统提供的认证会话组件完成跳转,而不是在 App 内嵌的 WebView 里做,后者会让凭据与 App 自身的会话边界变得模糊。回调地址方面,需要用自定义方案或已声明的 HTTPS 类回调把用户送回应用。

命令行与本地工具:没有浏览器交互界面的场景要另想办法,常见做法是借助系统浏览器完成授权后通过本地回环地址收回授权码,或者使用面向输入受限设备设计的授权流程,具体取决于授权服务器是否支持。选择前先确认授权服务器的能力清单,而不是先写代码。

还有一种容易被漏掉的形态:既有浏览器界面、又要在后台长期访问接口的桌面或服务端工具。这类客户端属于能保守密钥的一类,可以用授权码流程换取令牌,并把刷新令牌妥善保存在本机或服务端的安全存储里,通过刷新来维持长期访问,避免把用户凭据反复拿出来用。判断时看的是「密钥存在哪里」,不是「界面长什么样」。

机器对机器:没有用户参与,讨论登录没有意义,直接用客户端凭据流程:客户端用自己的身份去授权服务器换取令牌,scope 描述它能访问哪些服务接口。这类令牌的生命周期管理、凭据存储与轮换,是运维侧要单独立一项的工作。

把这五种形态排成一列,SAML 的位置也随之清晰:它主要服务「浏览器里的服务端 Web 单点登录」这一格,尤其是涉及跨组织、需要交换元数据与证书建立长期联邦关系的那类场景。出了这一格,OIDC 的覆盖面明显更广。下面这张表按协议维度把客户端形态、令牌形态与服务端常驻能力对应起来,可以直接对照着填。

协议 主要适用客户端形态 令牌或断言形态 服务端需要的常驻能力
OAuth 2.0 服务端 Web、机器对机器,以及各类需要代持访问许可的客户端 access token,可为 JWT 或不透明令牌,格式由授权服务器决定 令牌内省或本地校验能力、scope 与资源服务的映射关系、客户端凭据存储
OpenID Connect 1.0 服务端 Web、单页应用、移动端、命令行工具,覆盖面最广 ID Token(JWT)加 access token,可选调用 UserInfo 端点取属性 稳定时间源、JWKS 与 Discovery 文档的缓存与刷新、会话与刷新令牌存储
SAML 2.0 浏览器里的服务端 Web 单点登录,尤其是跨组织联邦场景 XML 断言,经 POST 或 Redirect 绑定由 IdP 送到 SP,依赖 XML 签名校验 元数据与证书管理、XML 签名校验库、会话存储与断言重放防护
组合用法 同一组织内既有浏览器 Web 系统,又有移动端与后端服务 Web 侧用 SAML 建立会话,其余客户端用 OIDC,身份源保持同一套 两套元数据与密钥的轮换节奏要统一,会话与吊销状态要能跨协议对齐

三种协议在服务端部署上的常驻能力差异

协议选完,落到部署上要养的东西差别不小。第一项是稳定时间源。JWT 的 exp 与 nbf、SAML 断言的 NotBefore 与 NotOnOrAfter 都依赖时钟判断,服务器时钟偏移一旦超出容忍范围,就会出现两种方向的误判:偏慢的机器认为凭据还没生效而拒绝,偏快的机器认为凭据还没过期而放过。这不是调优问题,而是必须把时间同步与偏移监控当作常驻组件来维护,跨地域部署时尤其要注意各节点是否指向同一套可信时间源。

第二项是密钥轮换与缓存刷新。OIDC 侧要缓存 JWKS 与 Discovery 文档,缓存的意义是减少每次校验的网络调用,但缓存必须支持被动刷新:当校验时遇到未知的密钥标识,要能立刻重新抓取一次密钥集,否则密钥轮换的那一段时间里新签发的令牌会全部校验失败。SAML 侧对应的是元数据与签名证书,证书有到期时间,元数据里可能包含多个签名证书以支持平滑替换。两套东西的共同要求是:轮换期间新旧密钥并存,并在监控里明确「距离下一次到期还有多久」。

第三项是会话与刷新令牌的存储。SAML 断言校验通过后要建立自己的会话,OIDC 的长生命周期访问依赖刷新令牌,这两者都需要一个可靠的存储层,并且要能被主动失效——用户登出、管理员禁用账号、凭据泄漏,这三种情况都要求立刻让已发出的会话或令牌失效。存储层同时决定了横向扩展方式:多台应用服务器之间如果会话不共享,就必须在网关做会话绑定,否则用户会在节点之间被反复踢出。

多实例与多地域部署时,这三摊事还要再叠加一层:每个实例都会各自缓存密钥与元数据,轮换期间的刷新节奏可能不一致,导致同一枚令牌在 A 节点通过、在 B 节点失败;会话存储如果按地域分库,跨地域切换时用户可能被要求重新登录。缓解方式无非两类——要么把这几项状态收敛到共享存储,要么在网关侧做会话绑定让同一用户始终落到同一节点。选哪类取决于你对可用性的要求,但必须明确作出选择,不能让两个节点各自为政。

把这三摊事合起来看,所谓「自建身份服务的成本」主要不在软件本身,而在这些常驻能力上。一万网络深耕 IDC 行业 19 年(成立于 2007 年),在华南、华东、华北及中国香港、海外多地整合自营节点与 BGP 多线资源;身份服务这类常驻组件对机型与带宽的实际占用,建议按自有的并发量级向服务商核实后再定。身份服务这类常驻组件对机型与带宽的实际占用,建议按自有的并发量级向服务商核实后再定。

落地时最容易出错的五处配置

回调地址对不齐。授权服务器里登记的回调地址与客户端实际发起请求时带的地址必须严格一致,这是最常被低估的一条。协议尾部的斜杠、路径大小写、查询参数的有无,都会导致匹配失败;反过来,为了省事在授权服务器侧配置通配回调,又会留下被诱导跳转的口子。正确做法是每个客户端登记确定的、完整的回调地址清单,一个环境一条,不要用通配。

时间窗与时钟漂移。前面说过时间源的重要性,落到配置上还有一层:令牌有效期与断言生效窗口设得太短,会把轻微的时钟偏移放大成随机失败;设得太长,又拉长了凭据泄漏后的可用时间。合理的做法是先用较短的有效期,配合刷新令牌或重建会话来续期,同时把时钟偏移监控纳入日常巡检项。

只验签名,不验签发方与受众。这是最危险的一处。签名合法只说明「这枚令牌确实是某个可信密钥签的」,不说明「它是签给我的」。必须为每一类凭据明确校验签发方与受众,并绑定自己期望的算法,不接受凭据自身声明的算法。漏掉受众校验,等于允许为其他系统签发的凭据在本系统使用。

密钥轮换时缓存不更新。本地缓存 JWKS 或元数据能省掉网络调用,但如果没有「遇到未知密钥标识就回源刷新」的逻辑,也没有定期主动刷新的兜底,轮换当天就会出现大面积校验失败。另一种反向错误是每次校验都实时抓取密钥集,把密钥端点变成了可用性瓶颈,一旦它抖动,全站登录一起失败。稳妥的形态是缓存加失效回源,并给缓存设上限。

登出只做了一半。前端清空了本地状态,浏览器看起来已经退出了,但服务端的会话与刷新令牌仍然存在,身份提供方那一侧的会话也还在,用户下次点开又会被静默登录回去。完整的登出链路要同时覆盖三个地方:业务系统自己的会话、身份提供方的会话、以及仍然存在效期的刷新令牌。SAML 侧有对应的单点登出机制,OIDC 侧也有相应的登出与会话管理端点,是否启用、如何串联,要在设计阶段定清楚,而不是上线后补。

关于协议选型,几个常被问到的问题

能不能只用 OAuth 2.0 做登录?技术上可以做出能跑的版本,代价是身份字段、受众语义、登出与吊销都要自己约定一遍,跨组织对接时每个新伙伴都要重新谈。如果只是在自己内部几个系统之间打通,这套约定或许够用;一旦涉及外部组织或多形态客户端,补上 OIDC 这一层的收益远大于成本。

OIDC 出现之后,SAML 是不是该淘汰?两者主场不同。SAML 在浏览器里的跨组织 Web 单点登录上依然成熟,存量系统大量存在,元数据加证书的对接模式在企业联邦里已经跑了很多年;OIDC 在移动端、单页应用、后端服务与 API 场景里覆盖更广。现实里更常见的组合是:同一个身份源同时提供两种协议,Web 老系统走 SAML,新客户端走 OIDC,两者共享同一套用户与凭据。

用了 JWT 是不是就不用存会话了?不存会话意味着失去了主动失效的能力。JWT 在过期之前不会因为服务端的状态变化而失效,用户被禁用、权限被回收、凭据泄漏,这些情况下你拿一枚已经发出的 JWT 没有办法。所以只要业务需要「立刻让某人下线」,就仍然需要一个服务端状态,或者接受很短的有效期并承担刷新频率上升的开销。

移动端为什么不再用隐式流程?隐式流程把令牌直接交到前端手上,暴露面包括浏览器历史、引用传递、页面脚本与日志,而且无法证明换取令牌的客户端就是发起授权的那个客户端。授权码加 PKCE 用一次性校验值补上了这个证明,同时令牌交换过程不经过浏览器地址栏,因此成为推荐做法。

多个业务系统要各接一次吗?对接次数不是问题,状态一致性才是。真正要统一的是三件事:身份来源、受众标识规则、以及登出与吊销的联动方式。这三件事统一之后,各系统独立对接反而更简单;不统一的话,接得再多也只是把不一致复制到更多地方。

先定场景再定协议:一份可以直接照着填的清单

把前面的内容收拢成一份可以逐项填写的清单,选型会快很多。以下为典型部署思路,并非特指某一真实客户,按自己的约束逐条替换即可。

第一步,列出所有客户端形态。服务端 Web、单页应用、移动端、命令行工具、后端服务,每种形态单列一行。有浏览器的和没有浏览器的分开,能保守密钥的和不行的分开。这一步没做完,后面的讨论都是空的。

第二步,按形态定流程。能保守密钥的服务端 Web 用授权码加 PKCE;单页应用与移动端同样是授权码加 PKCE,不再考虑隐式流程;命令行工具按授权服务器支持的能力选择本地回环或面向输入受限设备的流程;后端服务之间用客户端凭据。纯浏览器的跨组织 Web 单点登录,若对方要求或已有存量对接,走 SAML 2.0。

第三步,定凭据在哪里校验。需要网关统一拦截并信任下游的,适合自包含的 JWT;需要即时吊销、且能接受每次校验多一跳的,考虑不透明令牌加内省;只在建立会话那一次用到身份结论的,SAML 断言换本地会话即可。把校验点画在架构图上,确认每个入口都被覆盖。

第四步,定吊销与登出。写出三个动作分别由谁执行:业务会话失效、刷新令牌作废、身份提供方会话结束。这三件事如果没有串成一条链路,登出就是假的。

第五步,定常驻能力。时间源指向哪里、偏移怎么监控;JWKS 与元数据缓存多久、遇到未知密钥如何回源;证书与密钥什么时候到期、轮换期间是否新旧并存;会话与刷新令牌存在哪个存储层、多节点之间如何共享。这四项写进运维文档,而不是留在实施人员的脑子里。

第六步,定验收项。至少覆盖:错误的回调地址是否被拒绝、过期凭据是否被拒绝、为其他受众签发的凭据是否被拒绝、密钥轮换期间新旧令牌是否都能通过、登出后已发出的刷新令牌是否还能换取新令牌。这五条能过,配置才算立住。

协议之间没有高低,只有位置不同。把客户端形态、校验位置、吊销需求这三件事先答清楚,OAuth 2.0、OIDC、SAML 2.0 各自该出现在哪里,答案会自己浮出来。


上一篇:多个进程怎么共享同一个监听端口:SO_REUSEPORT 解决了什么,又留下了什么问题

下一篇:DPDK 换来的不是吞吐:轮询模式把什么换掉了,又让你失去了什么