Spring Security OAuth2 完全解析 (流程/原理/实战定制) —— Client / ResourceServer 篇
一、前言
本文假设读者对 Spring Security 本身原理有一定程度的了解,假设对 OAuth2 规范流程、Jwt 有基础了解,以此来对 SpringSecurity 整合 OAuth2 有个快速全面的认识。
(关于总体流程,若对SS实在不熟悉可以简单理解为:Filter构造Authentication-> Provider认证并填充-> 设置到SecurityContext -> 而后用于Filter/AOP鉴权)
要了解一个SpringSecurity模块,就是了解它如何身份认证、如何自定义、(至于如何鉴权是SpringSecurity通用部分),对应到代码就是:背后的相关 Configurer、Filter、Authentication、Provider。
本文以最新Spring Boot版本2.6.2 按此逻辑梳理;涉及源码太多,就不放源码对照了,可以自行fork查看;斜体表示可配置自定义替换的部分
- 第一部分:先演示默认配置下 spring-boot-starter-oauth2-client 所带来的流程和效果,建立大概认知。对应代码 thirdpart-login 项目
- 第二部分:全面解析 oauth2login、oauth2client 原理。
- 第三部分:常见业务下我们自己用户系统也有token分发需求,因此也解读下提供JWT服务的 oauth2ResourceServer 模块
- 第四部分:综上所述,实战定制SpringSecurity OAuth2。对应代码 thirdpart-login-custom
二、默认 spring-boot-starter-oauth2-client 效果预览
由 OAuth2ClientAutoConfiguration自动配置类引入的默认配置,可由代码 thirdpart-login 复现。
- 后端在 .yml 配置中做好相关配置

- 访问受限资源"/user",后端鉴权异常后,由
LoginUrlAuthenticationEntryPoint重定向到登录页。

- 登录页则由
DefaultLoginPageGeneratingFilter根据相关配置自动构造页面String返回。

- 页面点击
<a "href"="/oauth2/authorization/gitee">发出授权请求。
后端OAuth2AuthorizationRequestRedirectFilter匹配响应该模板路径,返回实际授权码请求的重定向响应,转入三方授权页面:

- 同意授权后,Gitee会向游览器返回重定向响应。
游览器向 "redirect-uri" 发起访问,此时被后端OAuth2LoginAuthenticationFilter匹配处理,其会用请求携带的 code 向配置的 "token-uri、user-info-uri" 发起一系列请求,最后构造出认证后的身份放入SecurityContext,以SESSION持久化等。再将先前保存在SESSION中的的受限资源访问请求拿出,重定向重新访问。

三、oauth2Login、oauth2Client 解析
1. 两者区别
这两者都是在SpringSecurity中整合OAuth2的入口方法(例http.oauth2Login()),对应OAuth2LoginConfigurer、OAuth2ClientConfigurer,只是引入Filter有所异同。简而言之:
oauth2Login会在授权请求时进行认证(即设置安全上下文SecurityContext),背后会连续访问acc_token&user-info-url 将获取的用户信息构造填充 Authentication。- 而
oauth2Client也会对授权请求进行处理,但只是获取到access_token后用repository存起来(要怎么使用自行处理),不会认证,这也意味着需要自行实现认证逻辑。
2. 从 OAuth2 请求的维度概览
- 对 授权码code 的请求:由
OAuth2AuthorizationRequestRedirectFilter响应"授权请求"向客户端返回重定向响应,定向到实际 "authorization-uri" - 对 access_token 和 user-info-uri 实际请求:
OAuth2LoginAuthenticationFilter会对回调地址(携带了code和state)进行处理,调用AuthemticationManager进行认证。背后OAuth2LoginAuthenticationProvider会进行连续 token-uri、user-info-uri 请求,最后返回完全填充的OAuth2LoginAuthenticationToken。
3. 必须要配置的属性:
太长就不贴了 参考Github项目代码,CommonOAuth2Provider也内建了一些常见OAuth2提供方,在之内少配几个字段也没关系。
- Client属性:
OAuth2ClientRegistrationRepositoryConfiguration用以处理application.yml中的相关属性,并构建代表OAuth2方的一个个ClientRegistration。根据不同模式,对必须属性有不同要求。

- provider属性:除了上图授权码模式下必须校验的 "authorization-uri、token-uri" 外,"user-info-uri"、
userNameAttribute也是必须的,在后续OAuth2LoginAuthenticationProvider调用的DefaultOAuth2UserService内,必须需要这俩属性才能尝试访问 user-info-uri 并包装为DefaultOAuth2User。
4. 相关 Authentication
- OAuth2LoginAuthenticationToken
用以给Provider认证过渡用,最初仅含code,最终包含access_token、user等。 - OAuth2AuthorizationCodeAuthenticationToken
用以给Provider认证过渡用,未填充时仅含code,经填充后包含access_token等。 - OAuth2AuthenticationToken
authenticated=true认证后安全上下文实际保存的OAuth2用户认证,由convert将填充后的OAuth2LoginAuthenticationToken转换而来。
5. 相关 Filter

OAuth2AuthorizationRequestRedirectFilter
- 通过调用
OAuth2AuthorizationRequestResolver用于判断是否为授权请求(默认为 "/oauth2/authorization/{registrationId}",可通过.oauth2Login().authorizationEndpoint().baseUri()配置) ,并且请求包装为OAuth2AuthorizationRequest后由authorizationRequestRepository(默认基于SESSION实现)将授权请求保存(后有他用) - 随后重定向到追加了参数(client-id、response_type)的真实授权码请求。
- 通过调用
OAuth2LoginAuthenticationFilter
继承自AbstractAuthenticationProcessingFilter,即负责身份认证的Filter。- 当是
loginProcessingUrl(默认为/login/oauth2/code/*)请求且带了code和state时,尝试以这俩参数构建OAuth2LoginAuthenticationToken且调用AuthenticationManager去进行认证。 - 认证通过后,调用
authenticationResultConverter将认证后完全填充的OAuth2LoginAuthenticationToken转为authenticated=true的OAuth2AuthenticationToken,用以代表认证后的身份。(该converter默认就是直接提取填充后的"principal、authorities、clientid"直接new) - 将先前得到 "token、refreshToken" 等信息包装为
OAuth2AuthorizedClient调用OAuth2AuthorizedClientRepository#saveAuthorizedClient保存起来(默认是基于内存实现的ClientId和Principal为key的Map)
- 当是
OAuth2AuthorizationCodeGrantFilter
(该Filter,在oauth2Login()下会永远被跳过,因为该请求已被OAuth2LoginAuthenticationFilter处理后通过successHandler重定向)
匹配带code与state的请求(表示回调请求)且满足authorizationRequestRepository.loadAuthorizationRequest不为空时(表示经过了RedirectFilter,是先前授权请求发起的),会构造OAuth2AuthorizationCodeAuthenticationToken交由AuthenticationManager(背后交由OAuth2AuthorizationCodeAuthenticationProvider)进行认证,并将结果构造为OAuth2AuthorizedClient交由authorizedClientRepository保存,然后去除参数再将请求重定向到 "savedRequest 或者 redirect-url"。
【注:不是很能理解该Filter这里为什么要重定向,这个重定向真的很恼火。如果API自身需要code,这重定向把参数清除了会报错;而即便API不要code了依附于它的逻辑使用authorizedClientRepository,那也是无意义多一次请求。而且其基于SESSION的实现本来没什么问题,但非要重定向请求一次就导致单纯的多实例时会存在问题】
6. 相关 Provider
OAuth2LoginAuthenticationProvider
- 对
OAuth2LoginAuthenticationToken尝试认证,其内会进一步构造OAuth2AuthorizationCodeAuthenticationToken,然后调用 OAuth2AuthorizationCodeAuthenticationProvider 对其进行认证。 - 经过上述认证后拿到填充了 "access_token" 的
OAuth2AuthorizationCodeAuthenticationToken,会构造成OAuth2UserRequest后传给OAuth2UserService负责进行实际的 "user-info-uri" 请求,并将结果包装成DefaultOAuth2User返回。(该User拥有两类authorities,一个是ROLE_USER(Spring在经过oauth2UserService时手动添加的),一类是Token中的SCOPE_{sopces})

- 对
OAuth2AuthorizationCodeAuthenticationProvider
对OAuth2AuthorizationCodeAuthenticationToken尝试认证,内部会构造对"token-uri"的实际请求,并调用DefaultAuthorizationCodeTokenResponseClient进行请求返回,并根据返回结果OAuth2AccessTokenResponse(内含access_token/refreash_token),新new一个填充了"access_token"的OAuth2AuthorizationCodeAuthenticationToken返回。
四、oauth2ResourceServer
1. 概述
由 org.springframework.boot:spring-boot-starter-oauth2-resource-server 引入,提供对请求中携带token校验解析、身份认证的服务。
2. 必须的配置
- JwtDecoder:在
oauth2ResourceServer的Configurer::init时,会构建JwtAuthenticationProvider,它就需要decorder以提供对"token"校验解析。 - JwtEncoder:虽然不是必须的,但我们自己系统登录有令牌分发的需要。
3. 相关 Filter
- BearerTokenAuthenticationFilter
(虽没继承AbstractAuthenticationProcessingFilter但却干着认证的事)
首先通过DefaultBearerTokenResolver::resolve判断是否含"token",然后构建BearerTokenAuthenticationToken并调用AuthenticationManager尝试认证。
将认证后的结果JwtAuthenticationToken设置到安全上下文中。如果中途出现了异常,则以该filter的authenticationEntryPoint(可通过.oauth2ResourceServer().authenticationEntryPoint配置) 处理。
4. 相关 Authentication
- BearerTokenAuthenticationToken
代表原始token的一个过渡身份。 - JwtAuthenticationToken
其authenticated=true,进行实际系统访问的身份。由BearerTokenAuthenticationToken认证后,通过JwtAuthenticationConverter转换而来。
5. 相关 Provider
- JwtAuthenticationProvider
对 BearerTokenAuthenticationToken(带access_token)进行认证。- 内部会调用
JwtDecoder::decode(可通过.bearerTokenResolver().jwt().decode配置)对 "token" 进行解析&验证为Jwt对象。 - 调用
JwtAuthenticationConverter(可通过.bearerTokenResolver().jwt().jwtAuthenticationConverter配置)尝试对Jwt进一步转换为进行实际系统访问的(authenticated=true)JwtAuthenticationToken返回。(默认converter内部会调用jwtGrantedAuthoritiesConverter解析Jwt填充 authorities(将"scpoe"/"scp"声明中空格分隔的字串转为SimpleGrantedAuthority);将"sub"字段作为 principal)
- 内部会调用
五、前后端分离实战定制
实际情况中,除了OAuth2登录,我们系统自身也有完整的用户体系,也有按自己业务定制的token构建分发服务。
三方登录仅作为绑定手段,而且在初次三方登录时 往往还需补全信息注册到我们自己的用户体系。
最终实现代码以及效果展示都放在Github上了:spring-security-oauth2-sample
登录流程,大致API流程:

六、后记
从上文也能看出,不得不提 笔者实际用SpringSecurity很多时候宁愿迁出去自己写套Configuer/Filter/Provider…,官方虽提供了很多服务,而且也能看出在尽可能定制化。但背后还是强制耦合引入了太多逻辑,很难与实际业务契合,即便稍有不同在它基础上定制也都需要付出很大代价。这代价不仅指新增代码行数,为了运行稳定 你首先就得彻底清楚它原本引入了哪些逻辑,这就需要大量上手成本。
本文仍存在些许问题,特别是OAuth2AuthorizationCodeGrantFilter的重定向问题,还有与无状态相悖的oauth2AuthorizedClientRepository涉及较少,也没一张清晰流程图,时间关系暂且就这样了。即便要用好也得知其然 知其所以然,笔者撰文也只是尽量往上靠,有什么问题还希望指正讨论。
关于 Spring Security 对 OAuth2 认证服务org.springframework.security:spring-security-oauth2-authorization-server的实现,以及前言提到的 SpringSecurity原理、JWT等等,后面有时间的话也会慢慢更。
Spring Security OAuth2 完全解析 (流程/原理/实战定制) —— Client / ResourceServer 篇的更多相关文章
- Spring Security OAuth2 微服务认证中心自定义授权模式扩展以及常见登录认证场景下的应用实战
一. 前言 [APP 移动端]Spring Security OAuth2 手机短信验证码模式 [微信小程序]Spring Security OAuth2 微信授权模式 [管理系统]Spring Se ...
- 【SpringSecurityOAuth2】源码分析@EnableOAuth2Sso在Spring Security OAuth2 SSO单点登录场景下的作用
目录 一.从Spring Security OAuth2官方文档了解@EnableOAuth2Sso作用 二.源码分析@EnableOAuth2Sso作用 @EnableOAuth2Client OA ...
- Spring Security OAuth2 单点登录
1. OAuth 2.0 OAuth(Open Authorization)为用户资源的授权提供了一个安全的.开放而又简易的标准.最简单的理解,我们可以看一下微信OAuth2.0授权登录流程: 通过O ...
- Spring Security Oauth2系列(一)
前言: 关于oauth2,其实是一个规范,本文重点讲解spring对他进行的实现,如果你还不清楚授权服务器,资源服务器,认证授权等基础概念,可以移步理解OAuth 2.0 - 阮一峰,这是一篇对于oa ...
- Re:从零开始的Spring Security Oauth2(二)
本文开始从源码的层面,讲解一些Spring Security Oauth2的认证流程.本文较长,适合在空余时间段观看.且涉及了较多的源码,非关键性代码以…代替. 准备工作 首先开启debug信息: l ...
- Spring Security OAuth2 实现登录互踢
背景说明 一个账号只能一处登录,类似的业务需求在现有后管类系统是非常常见的. 但在原有的 spring security oauth2 令牌方法流程(所谓的登录)无法满足类似的需求. 我们先来看 To ...
- [Spring Cloud实战 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT实现微服务统一认证授权
一. 前言 本篇实战案例基于 youlai-mall 项目.项目使用的是当前主流和最新版本的技术和解决方案,自己不会太多华丽的言辞去描述,只希望能勾起大家对编程的一点喜欢.所以有兴趣的朋友可以进入 g ...
- 【Spring Cloud & Alibaba 实战 | 总结篇】Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权
一. 前言 hi,大家好~ 好久没更文了,期间主要致力于项目的功能升级和问题修复中,经过一年时间的打磨,[有来]终于迎来v2.0版本,相较于v1.x版本主要完善了OAuth2认证授权.鉴权的逻辑,结合 ...
- Spring Security 解析(五) —— Spring Security Oauth2 开发
Spring Security 解析(五) -- Spring Security Oauth2 开发 在学习Spring Cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因此决 ...
随机推荐
- MySQL如何使用coalesce函数
coalesce(a,b,c); 参数说明:如果a==null,则选择b:如果b==null,则选择c:如果a!=null,则选择a:如果a b c 都为null ,则返回为null(没意义)
- Memcached 状态机分析
worker线程拿到了这个连接之后,就应该是分配给这个连接一个结构体,包括这个连接所有的状态,都写buf等,这个结构体就是conn,然后这个worker线程会在它自己的event_base加入对这个新 ...
- 突出显示(Project)
<Project2016 企业项目管理实践>张会斌 董方好 编著 当一个大的项目文件做好以后,查看全部内容,肉眼多少会有点吃不消,这时就需要"划重点".在Porect里 ...
- CF1490D Permutation Transformation 题解
Content 给定一个排列 \(a\),按照以下方法构造一棵树: 选择当前排列中的最大数作为根的编号. 最大数左边的所有数按照上述方法建左子树,若没有数则该节点没有左儿子. 最大数右边的所有数按照上 ...
- LuoguP7106 双生独白 题解
Content 给定一个 十六进制颜色码(一个长度为 \(7\) 的字符串,意义详见题面),请输出其反色的十六进制颜色码. 数据范围:颜色的 R,G,B 值保证在 \(255\) 以内. Soluti ...
- JAVA获取当前日期指定天数之后的日期
/** * 获取day天之后的日期 * @param day 天数 * @return */ public static String getDate(int day){ Calendar calen ...
- ubuntu用户、用户组设置命令总结
1.ubuntu创建新用户: sudo adduser username(新建一个用户username) 2.设置用户 username 的密码(设置用户username密码) sudo passwd ...
- 【九度OJ】题目1474:矩阵幂 解题报告
[九度OJ]题目1474:矩阵幂 解题报告 标签(空格分隔): 九度OJ http://ac.jobdu.com/problem.php?pid=1474 题目描述: 给定一个n*n的矩阵,求该矩阵的 ...
- 【LeetCode】724. Find Pivot Index 解题报告(Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 先求和,再遍历 日期 题目地址:https://le ...
- 【汇编语言】李忠《x86汇编语言——从实模式到保护模式》
该书配套资料网址已经失效 配套资料和章节答案下载 查看最新作者网址:http://www.lizhongc.com/ 勘误表:https://wenku.baidu.com/view/9213288b ...