OAuth2四种授权模式详解:从概念到Spring Security 6实战
1. 为什么我建议仔细研究OAuth2从一次第三方登录接入说起过去几年我在做企业级应用的时候几乎每隔一段时间就会遇到“第三方登录怎么接”“怎么允许其他系统访问我们的用户数据”“怎么用一个机构账号打通多个平台”这类需求。早期很多项目走的是最原始的“把用户名密码给出去”的路子后来开始有人自己搞一个签名算法让两个系统互认再后来就有客户直接提要用OAuth2。OAuth2协议四种授权模式这个主题看起来是安全领域里很窄的一块但真正用到生产环境你就会发现它几乎决定了整个系统间信任边界的设计方式。OAuth2解决的其实是一个很朴素的问题资源所有者不想把密码直接交给第三方应用但又想让第三方应用在限定权限范围内访问自己的资源。说白了你用某个App登录另一个平台的时候没把密码交给那个平台而是交给你信任的认证服务器去确认身份再由认证服务器发一个令牌给第三方应用。这个令牌有有效期、有权限范围、可以被撤销这种机制比传统“共享账号共享密码”的方式要安全得多也灵活得多。从学习路径上看OAuth2有四种授权模式授权码模式Authorization Code、隐式模式Implicit、密码模式Resource Owner Password Credentials和客户端凭证模式Client Credentials。每一种模式对应不同的客户端类型、不同的信任级别、不同的使用场景。很多人一开始搞混是因为只记住了“有四种模式”这个结论却不清楚为什么协议设计者要设计出这四套东西也不清楚在选择时到底看哪些维度。这篇内容就是要把协议本身拆开讲透结合我在Spring Security 6环境里实际搭建OAuth2授权服务器和客户端的经验把每种模式背后的设计逻辑、参数细节、适用边界都讲清楚。既适合刚接触OAuth2的后端工程师作为完整入门材料也适合已经用过一段时间的开发者做一次系统性梳理看完你会发现自己对协议细节的理解会扎实很多。2. OAuth2协议里的四个角色与三种令牌先建立统一认知框架在拆四种授权模式之前我建议先把OAuth2协议里的角色和令牌体系搞明白。后面所有模式的差异本质上都是这几个角色之间交互方式的不同以及令牌使用方式的不同。2.1 四个角色分别是干什么的OAuth2定义了四种角色资源所有者Resource Owner资源的拥有者通常是自然人用户也就是我们常说的“最终用户”。在协议交互中资源所有者负责授权第三方应用去访问自己的资源。客户端Client第三方应用可以是Web应用、移动App、桌面程序、服务器端服务等。它想做的是代表资源所有者去访问受保护资源。授权服务器Authorization Server负责认证资源所有者身份并颁发令牌的系统。它的职责包括验证客户端的身份、获取资源所有者的授权、颁发访问令牌和刷新令牌。资源服务器Resource Server托管受保护资源的系统负责验证访问令牌的有效性并根据令牌携带的权限范围决定是否放行请求。这里容易搞混的是授权服务器和资源服务器可以是同一个应用也可以是两个分开的服务。实践中很多系统把这两个角色拆开部署比如一个访问令牌在授权服务器签发但资源服务器的校验逻辑独立运行。这也带来一个实际问题资源服务器怎么验证令牌常见方案是调用授权服务器的校验接口、本地解析JWT、或者用共享密钥做对称校验。这在后文Spring Security 6的配置里会具体体现。2.2 三种令牌和它们的使用场景OAuth2里的“令牌”并不是单指一个东西实际使用中至少分三种访问令牌Access Token客户端用来访问受保护资源时出示的凭证类似于一把“有限时长的钥匙”。它有时间限制通常从几分钟到几小时不等。刷新令牌Refresh Token访问令牌过期后客户端可以用它向授权服务器换取新的访问令牌支持离线续期能力。可以理解为“备用钥匙”或者“会员卡”不需要用户重新登录就能续上访问能力。刷新令牌的有效期一般比访问令牌长得多要求存储在绝对安全的客户端环境里。授权码Authorization Code授权码模式中临时出现的一个中间凭证生命周期极短只有几十秒到几分钟一次性使用通过它才能换取访问令牌和刷新令牌。它存在的核心意义在于“把难保密的令牌发往不安全的地方之前先经过一个受控的中间步骤”。我记忆这套关系的方式很简单授权码是“临时通行券”访问令牌是“当前有效钥匙”刷新令牌是“续期凭证”。三者之间的关系其实就看客户端拿到授权码后做了什么、访问令牌失效后又做了什么。2.3 三个核心端点授权端点、令牌端点、资源端点四种授权模式虽然交互顺序不一样但最终都离不开三个核心端点端点作用典型路径授权端点Authorization Endpoint与资源所有者交互收集授权同意/oauth2/authorize令牌端点Token Endpoint签发访问令牌、刷新令牌/oauth2/token资源端点Resource Endpoint校验访问令牌返回受保护数据/api/**授权码模式和隐式模式主要走授权端点密码模式和客户端凭证模式直接走令牌端点。后面讲具体模式的时候其实就是围绕“走哪个端点、带什么参数”展开的。3. 授权码模式为什么它是生产环境中的首选四种模式里授权码模式是设计得最完整、使用范围最广的一个。从Spring Security 6的默认配置来看授权码流程也是官方主推的流程。我自己的经验是只要客户端类型是Web应用或原生应用几乎都应该优先考虑授权码模式它在安全性和用户体验之间取得了很好的平衡。3.1 授权码模式的完整交互链路授权码模式完整跑一遍大概是这样的用户访问第三方应用客户端点击“使用XX账号登录”。客户端将用户重定向到授权服务器的授权端点并拼接好参数response_typecode、client_id、redirect_uri、scope、state。授权服务器展示登录页面用户提供凭证完成认证认证通过后展示授权页面询问“是否允许该应用访问你的数据”。用户点击同意授权服务器通过浏览器重定向回到客户端的redirect_uri并在URL上追加一个code参数这就是授权码。客户端拿到授权码后在后端直接调用授权服务器的令牌端点用client_id、client_secret、redirect_uri、code换取访问令牌和刷新令牌。客户端携带访问令牌访问资源服务器获取受保护数据。关键点在第5步换取令牌的请求必须从客户端后端服务器发出不能经过浏览器。这是授权码模式和隐式模式最本质的区别也是为什么它安全client_secret不会暴露在浏览器环境里用户的各种敏感凭证也都不经过第三方应用。3.2 为什么用授权码而不是直接把令牌还给客户端很多初学者会问既然最终目的是让客户端拿到访问令牌为什么不直接把令牌放在重定向URL里返回省掉中间这一步答案和“令牌暴露面”有关。如果访问令牌直接出现在浏览器的地址栏它就有可能通过浏览器历史记录、服务器访问日志、Referer头等渠道被泄露。授权码生命周期极短且一次有效就算被别人截获攻击者还需要同时拿到client_id和client_secret才能获取令牌。这一层中间步骤本质上是把“暴露在浏览器环境中的凭证”和“最终访问资源的凭证”解耦。我在实际项目里遇到过一个反例某个团队为了实现“少一次重定向”把response_type直接设置成token也就是走了隐式模式的路子把访问令牌放到了前端单页应用里。这在老旧的SPA架构里还能勉强接受但在现代安全审查里基本不会通过原因后面具体讲隐式模式时展开。3.3state参数和PKCE两个必须重视的补充机制授权码模式里state参数是官方强烈建议带的。它的作用是防止CSRF攻击跨站请求伪造。具体的攻击思路是攻击者先把用户引导到授权服务器完成授权然后再把授权码拼接到受害者的客户端回调地址上如果客户端不校验state攻击者就能让客户端误以为用户主动完成了授权流程进而绑定攻击者的账号。我的习惯是在发起授权请求时生成一个随机字符串存入会话回调回来时比对URL上的state和会话里的state是否一致不一致直接拒绝。这个步骤看着小但缺了就会被安全审计直接打回来。另一个规避不了的概念是PKCEProof Key for Code ExchangeRFC 7636。它本来是给原生App和SPA这类“无法安全保存client_secret”的客户端设计的原理是客户端先生成一个code_verifier再通过SHA-256生成code_challenge在授权请求阶段随参数带过去换令牌阶段再用原始code_verifier去证明自己就是最初发起请求的那个客户端。即使授权码被截获没有code_verifier也无法兑换令牌。现在的实践趋势是即使是后端Web应用也推荐启用PKCE。不是因为别的就是因为它能显著降低授权码被重放利用的风险。Spring Security 6里也直接内建了PKCE支持组件配置成本很低。3.4 授权码模式在后端的落地细节授权码模式在后端实现时有几个地方容易被忽略一是redirect_uri必须精确匹配授权服务器上注册的回调地址。不能把redirect_uri设计成“包含子路径就放行”的逻辑这是安全漏洞的老来源。二是一旦授权码被使用了它就应该立即失效重复使用同一个授权码的请求需要直接拒绝。没有实现这一点的授权服务器等于给人留了后门。三是访问令牌和刷新令牌要分开管理。Web应用环境下刷新令牌要存储在安全后端前端永远不应该接触刷新令牌。四是刷新流程的异常处理。刷新令牌过期时不能默默重新生成一个而应该走“要求用户重新授权”的完整流程否则令牌吊销机制就形同虚设。4. 隐式模式为什么SPA场景走向了没落而不是复兴隐式模式曾经是单页应用SPA时代的经典选择但如今它已经不再被推荐使用RFC 6749的后续修订版本甚至直接在文档里指出该模式的弃用建议。我自己在做SPA前端资源访问时也经历过从隐式模式迁移到授权码PKCE的过程体会很深。4.1 隐式模式的流程与授权码模式有什么异同隐式模式的流程简化了很多客户端直接把用户重定向到授权端点。授权服务器完成认证和授权后在URL片段fragment里直接返回访问令牌。客户端通过前端脚本从URL中提取并保存令牌。注意这里的差异隐式模式的response_type是token而不是code访问令牌直接落在浏览器环境里没有授权码这个中间层。对于纯前端页面想要换授权码就得发出向令牌端点的跨域请求而这个请求必然暴露client_secret这在纯浏览器环境里根本做不到所以早期设计者才直接用令牌替代授权码。4.2 隐式模式的三大风险隐式模式有三大不可回避的硬伤一是访问令牌直接暴露在URL中容易通过浏览器历史记录、日志、Referer头泄露。即使放在fragment里比query参数稍好一些但依然不够安全。二是缺少客户端认证。如果不验证client_id与client_secret攻击者完全可以伪造一个客户端来发起授权流程拿到属于合法客户端的令牌。虽然资源服务器可以通过scope限制令牌权限但客户端本身的信任边界已经模糊。三是无法使用刷新令牌。因为不存在授权码兑换步骤授权服务器没有理由颁发刷新令牌给一个纯前端环境而一旦访问令牌过期用户就只能重新授权。这个体验在很多场景下用户根本不能接受。4.3 现代SPA的正确替代方案现在SPA的标准做法是授权码模式PKCE流程看起来加了步骤但换来的是令牌不出现在URL的query参数中而且客户端在令牌端点需要通过PKCE证明身份安全性明显更强。Spring Security 6对SPA的支持也直接指向这个方向。我在配置前端对接时核心流程就是后端维护一个OAuth2客户端配置前端通过后端发起授权码请求拿到授权码后后端再与令牌端点交互。令牌始终保存在后端或安全的浏览器内存/会话存储中而不是让前端JavaScript直接持有长期有效的访问令牌。如果你还在维护老旧的隐式模式接入我的建议是尽早规划迁移。虽然隐式模式在当前协议版本里还能被部分兼容但主流授权服务器都在逐步收紧对它的支持边界。5. 密码模式与客户端凭证模式不同信任等级下的令牌获取方式第三种和第四种模式的共同点是不需要浏览器交互客户端直接与授权服务器交换凭证。但它们的“信任前提”完全不同。我习惯把它们放在一起对比思考能避免很多误用。5.1 密码模式用户名密码直接交给客户端信任要求极高密码模式的流程是客户端直接提示用户输入用户名和密码。客户端拿着这套凭证连同自身client_id和client_secret一起提交到授权服务器的令牌端点。授权服务器验证通过后签发访问令牌可选签发刷新令牌。为什么说这个模式纯度最高因为用户名密码出现在客户端手里这和非OAuth2时代的普通表单登录其实已经没什么结构区别了。唯一多出来的内容是多了一层令牌机制以及授权服务器可以在这个环节做一些增强认证比如二次验证、设备指纹等。这也就决定了密码模式的适用面很窄只有当一个客户端是官方出品、和授权服务器之间存在充分信任关系时才适合使用。典型的例子是同一公司自己推出的移动App和自家认证后端、遗留系统的内部迁移场景。在Spring Security 6环境里密码模式虽然官方默认组件支持度不算高因为它本质上从安全角度并不推荐需要额外自定义认证过滤器去实现用户校验逻辑。如果非要使用建议必须在传输层启用HTTPS并要求客户端定期轮换client_secret同时对登录失败的频率做严格限制。曾经有段时间很多人的接口服务为了“省事”直接对第三方公开密码模式把第三方应用当成了收集密码的入口这在我看到过的安全审计里几乎必然判为高风险。因为第三方应用一旦被攻破攻击者拿到的不是一个人的密码而是所有用户的密码集合。这种事情一旦发生波及面根本不是“授信应用被攻击”那么简单。5.2 客户端凭证模式没有用户参与的机器对机器认证客户端凭证模式与密码模式最大的区别在于没有资源所有者参与的环节客户端自身就是资源所有者所以也不需要用户名密码只需要携带自己的client_id和client_secret访问令牌端点换取令牌。它的流程简洁到只有一步请求POST /oauth2/token grant_typeclient_credentials client_idyour_client_id client_secretyour_client_secret scoperead_write适合的场景是服务端到服务端的API调用比如定时任务批量拉取数据、内部服务之间同步配置、第三方系统获取公开授权数据等。此时不需要“用户”的身份背景令牌直接代表客户端应用本身。在Spring Security 6授权服务器中客户端凭证模式支持的配置相对简单核心是给客户端定义好authorizedGrantTypes并声明scope范围。我通常会额外加一层措施限制这个模式获取的令牌只能访问指定的端点不让它默认拥有全量权限再用IP白名单限定调用来源避免纯靠client_secret作为唯一防线。5.3 两种模式的取舍对照表对比维度密码模式客户端凭证模式是否需要用户参与需要不需要使用的客户端类型第一方App、可信客户端后端服务、定时任务主要风险密码集中泄露客户端凭证泄露访问令牌代表身份代表授权用户代表客户端应用本身能否签刷新令牌通常可以不建议签发推荐度低需严格限制高服务间调用首选我见过很多项目把这两个模式搞反。比如让定时任务去用密码模式这会导致任务需要维护一个“服务账号的密码”一旦密码过期系统就静默失败又如把客户端凭证模式拿到的令牌当成“用户令牌”去访问用户专属数据这就是越权访问了。认清“令牌代表谁”这个问题是做OAuth2集成时最根本的判断标准。6. 四种授权模式的横向对比与选型判断方法把四种模式放在一张表里看差别一目了然授权模式客户端类型是否需要用户交互是否需要client_secret令牌获取路径刷新令牌典型场景授权码模式Web应用、移动App是后端需要授权端点令牌端点支持用户登录、第三方授权隐式模式纯前端SPA已不推荐是否授权端点直接返回令牌不支持老旧SPA密码模式第一方可信客户端是是令牌端点支持官方App登录客户端凭证模式后端服务否是令牌端点通常不签服务间API调用选型的逻辑顺序其实很简单先问这个系统里有没有“用户”参与。有用户参与就选授权码模式现代首选用户参与但客户端极端受限、且无法安全保存任何凭证可以考虑授权码PKCE有用户参与但客户端是第一方且信任度极高才考虑密码模式。没有用户参与直接走客户端凭证模式。隐式模式除非有历史包袱否则不应该出现在新项目选型里。在Spring Security 6中注册一个OAuth2客户端时可以通过声明authorizedGrantTypes来决定该客户端允许使用的授权模式。官方推荐的默认配置通常只开放授权码模式和客户端凭证模式。如果某个客户端类型没有必要开放全部模式我一般只会按需打开一个或两个入口尽量不打包开放。7. Spring Security 6的OAuth2落地授权服务器与客户端的配置实战这些年Spring生态对OAuth2的支持经历了比较明显的演进。Spring Security 6开始授权服务器支持已经单独拆出为一个独立组件不再和老的Spring Security OAuth旧模块混在一起。Spring Security 6的OAuth2使用方式比Security 5时代有比较大的变化初次接触的人很容易被各种版本差异绕晕。7.1 授权服务器搭建的整体配置思路在Spring Security 6中搭建OAuth2授权服务器核心依赖是spring-security-oauth2-authorization-server。整体上需要做的事有这么几件配置授权服务器的基本信息、注册客户端信息、定义授权模式、指定令牌的密钥和格式。一个基础配置的长相大致如下Configuration EnableAuthorizationServer public class AuthorizationServerConfig { Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient client RegisteredClient.withId(coupon-service) .clientId(coupon-client) .clientSecret({noop}secret) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .redirectUri(http://localhost:8080/login/oauth2/code/coupon) .scope(read) .scope(write) .build(); return new InMemoryRegisteredClientRepository(client); } Bean public JWKSourceSecurityContext jwkSource() { // 生成RSA密钥对并包装为JWKSet RSAKey rsaKey generateRsaKey(); JWKSet jwkSet new JWKSet(rsaKey); return (jwkSelector, securityContext) - jwkSelector.select(jwkSet); } }注意这里有一个版本变化Spring Security 6中部分配置类注解和类名与老版本不一样了以前用的EnableAuthorizationServer来自旧模块新版本虽然还保留类似注解但实际实现已经完全不同项目依赖也需要用新坐标。如果从老项目直接升级最少要重新梳理依赖和配置项。7.2 客户端接入配置的常见写法客户端接入授权服务器时在Spring Security 6里主要通过spring-security-oauth2-client实现。在application.yml中常见配置如下spring: security: oauth2: client: registration: coupon-login: client-id: coupon-client client-secret: secret authorization-grant-type: authorization_code redirect-uri: {baseUrl}/login/oauth2/code/{registrationId} scope: - read - write provider: coupon-login: authorization-uri: http://authorization-server:9000/oauth2/authorize token-uri: http://authorization-server:9000/oauth2/token jwk-set-uri: http://authorization-server:9000/oauth2/jwks配置里最容易出错的地方是redirect-uri。它必须和授权服务器上注册的重定向地址保持一致否则授权服务器会直接忽略回调。另一个常见问题是jwk-set-uri配置错误导致JWT签名无法解析资源服务器一旦启动就抛异常WS报错日志还很隐蔽。7.3 资源服务器端如何校验令牌资源服务器只需要配置一个JWT解析器声明jwk-set-uri地址即可spring: security: oauth2: resourceserver: jwt: jwk-set-uri: http://authorization-server:9000/oauth2/jwks这里有个坑点jwk-set-uri指向的地址必须是你授权服务器真正暴露的JWK集合端点不能写到/oauth2/token这类令牌端点。如果同时存在多个资源服务器它们可以从同一个授权服务器上获取JWK公钥信息来验签这样令牌就实现了“一处签发、多处通用”的效果。但你也要知道这种去中心验签方式只有在令牌都是JWT格式时才能用如果令牌是不透明字符串资源服务器就只能调用授权服务器的校验接口来验证。7.4 实际项目中我常用的安全加固项把流程跑通不是终点真正让我觉得“稳定能上线”的是下面这些细节强制HTTPS明文传输授权码和令牌等于把钥匙直接放在门上。生产环境必须全链路加密。令牌有效期按场景区分用户点击“记住我”可以签一个较长的刷新令牌普通操作则用短有效期的访问令牌。不要所有令牌一刀切。client_secret不要硬编码在代码库用配置中心或环境变量管理且支持轮换。授权码一次性使用令牌端点必须校验授权码是否已被消费过重复消费直接拒掉。日志脱敏不要打印完整令牌、授权码和用户密码相关的信息。Scope最小化不要把read、write、admin一股脑全给每个客户端只要自己需要的范围。8. 集成过程中的高频坑点与排查思路OAuth2集成不像普通接口联调那么直观大量问题出在“配置对了但流程没走通”的模糊地带。我把自己踩过和帮朋友排查过的几个典型问题列出来很有参考价值。8.1 授权码回调后令牌端点报invalid_grant这个错误在授权码模式下出现频率最高。原因通常是这几个方向授权码已过期。授权码生命周期通常很短如果你在测试时拿着URL等了很久才复制到Postman里请求令牌过期是必然的。授权码已被消费。有些人习惯把同一条授权URL粘贴多次请求第二次必然失败。redirect_uri前后不一致。发起授权时带的redirect_uri和换取令牌时带的redirect_uri必须完全相同差一个字符都不行。排查思路是从请求头开始逐项比对重点看回调URL是否有额外的路径影响、是否有大小写差异、是否缺少协议端口等等。大多数情况下我通过在客户端请求日志里完整打印授权请求和令牌请求参数就能快速定位。8.2 JWT令牌验签失败在资源服务器端看到“Invalid signature”之类的异常大概率是jwk-set-uri配置不对或授权服务器切换了密钥。密钥轮换后授权服务器会继续在JWK集合里保留旧密钥一段时间资源服务器可以同时持有多个公钥候选因此令牌大多能顺利验签但如果授权服务器没有保留旧密钥就直接轮换所有在途令牌就会立刻失效。我在生产环境处理这个问题的方式是密钥用单独密钥管理服务生成和轮换JWK集合端点尽量通过配置中心动态下发避免密钥变更时需要手动重启整个资源服务器集群。8.3 CORS导致的preflight请求失败前端SPA调用资源服务器时如果遇到跨域拦截要先查看预检请求是否成功。OAuth2本身不解决网络跨域问题这是Web基础设施的另一层范畴。不要把CORS失败归结为“令牌不对”或“授权配置有问题”先看请求是否真的到了后端再说。常见做法是在资源服务器上显式允许指定来源、允许指定的Authorization请求头并在OPTIONS预检请求阶段直接放行不要进入鉴权链。8.4 刷新令牌被拒绝后日志缺失排查很费劲这是一个容易被忽视的运维问题。很多授权服务器在刷新令牌失效时只返回一个invalid_grant并不记录太多上下文。实际项目里我建议自己为令牌端点补充审计日志把客户端ID、是否命中过期令牌、刷新令牌最后使用时间等信息记录下来。这对日后排查“为什么用户突然掉线”帮助非常大。9. 关于OAuth2的常见认知误区和我的简单建议网上对OAuth2的讨论非常多但很多理解是片面的甚至是错的。最后澄清几个我经常纠正的误区也算是对整套内容的一次总结性拔高。9.1 OAuth2不是“单点登录协议”本身很多人把OAuth2和SSO划等号实际上OAuth2解决的是授权问题不是认证问题。虽然它在实现中经常承担认证职责但协议设计了严格的能力边界。真正做企业级单点登录时更完整的设计是在OAuth2之上再定义ID Token去承载用户身份信息或者在授权流程中增加认证语义扩展。只靠OAuth2却想覆盖“用户身份认证会话管理多端登出”等需求会显得左右支绌。9.2 JWT不是必须要用的有些团队一说到OAuth2就默认“访问令牌一定是JWT”。实际上OAuth2完全不关心令牌格式它只关心令牌能不能被资源服务器正确校验。JWT的好处是自带信息、去中心化验签坏处是难以远程撤销。如果业务上有强制撤销、短期失效的需求反而可以考虑不透明令牌加上后端校验。选型要看业务诉求不是为了用而用。9.3 授权码模式不等于绝对安全再安全的协议如果实现得粗糙也会出问题。我见过有人用授权码模式却不在回调里校验state或者在客户端保存client_secret用于前端调用。这些做法直接让授权码模式的保护效果大打折扣。安全永远是一个组合过程协议只提高信任基数不负责帮你兜底所有实现失误。9.4 从实际项目出发的一点个人体会我在实际项目中用过这四种模式前期也犯过不少错误比如把客户端凭证模式用在用户交互场景又比如在纯前端应用里默认使用隐式模式导致令牌泄露。踩过几次坑之后我的习惯已经固定下来新项目一律优先考虑授权码模式PKCE服务间通信统一用客户端凭证模式密码模式除非是自研第一方客户端否则一票否决隐式模式只在兼容老系统时才用。OAuth2的设计看似有四种模式其实核心只有一句话在客户端与资源服务器之间利用第三方授权服务器保障令牌委托过程中的信任边界。把这四种模式的适用边界、交互流程和典型部署方式都掌握好再遇到各类系统集成需求时就会少很多既想通又难通的头疼事。