SpringSecurityOAuth2完全解析(流程原理实战定制)——
ClientRes。。。
⼀、前⾔
假设对 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 所带来的流程和效果,建⽴⼤概认知。对应代码项⽬
第⼆部分:全⾯解析 oauth2login、oauth2client 原理。
第三部分:常见业务下我们⾃⼰⽤户系统也有token分发需求,因此也解读下提供JWT服务的 oauth2ResourceServer 模块
第四部分:综上所述,实战定制SpringSecurity OAuth2。对应代码
⼆、默认 spring-boot-starter-oauth2-client 效果预览
由OAuth2ClientAutoConfiguration⾃动配置类引⼊的默认配置,可由代码复现。
1. 后端在 .yml 配置中做好相关配置
2. 访问受限资源"/user",后端鉴权异常后,由LoginUrlAuthenticationEntryPoint重定向到登录页。
3. 登录页则由DefaultLoginPageGeneratingFilter根据相关配置⾃动构造页⾯String返回。
4. 页⾯点击<a "href"="/oauth2/authorization/gitee">发出授权请求。
后端OAuth2AuthorizationRequestRedirectFilter匹配响应该模板路径,返回实际授权码请求的重定向响应,转⼊三⽅授权页⾯:
5. 同意授权后,Gitee会向游览器返回重定向响应。
游览器向 "redirect-uri" 发起访问,此时被后端OAuth2LoginAuthenticationFilter匹配处理,其会⽤请求携带的 code 向配置的 "token-uri、user-info-uri" 发起⼀系列请求,最后构造出认证后的⾝份放⼊Secu
rityContext,以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. 必须要配置的属性:
太长就不贴了参考,CommonOAuth2Provider也内建了⼀些常见OAuth2提供⽅,在之内少配⼏个字段也没关系。
Client属性:OAuth2ClientRegistrationRepositoryConfiguration⽤以处理l中的相关属性,并构建代表OAuth2⽅的⼀个
个ClientRegistration。根据不同模式,对必须属性有不同要求。
provider属性:除了上图授权码模式下必须校验的 "authorization-uri、token-uri" 外,"user-info-uri"、userNameAttribute也是必须的,在后续OAuth2LoginAuthenticationProvider调⽤的DefaultOAuth2UserService内,必须需要这俩属性才能尝试访问 user-info-uri 并包装
为DefaultOAuth2User。
4. 相关 Authentication
1. OAuth2LoginAuthenticationToken
⽤以给Provider认证过渡⽤,最初仅含code,最终包含access_token、user等。
2. OAuth2AuthorizationCodeAuthenticationToken
⽤以给Provider认证过渡⽤,未填充时仅含code,经填充后包含access_token等。
3. OAuth2AuthenticationToken
authenticated=true认证后安全上下⽂实际保存的OAuth2⽤户认证,由convert将填充后的OAuth2LoginAuthenticationToken转换⽽来。
5. 相关 Filter
1. OAuth2AuthorizationRequestRedirectFilter
1. 通过调⽤OAuth2AuthorizationRequestResolver⽤于判断是否为授权请求(默认为 "/oauth2/authorization/{registrationId}",可通
过.oauth2Login().authorizationEndpoint().baseUri()配置) ,并且请求包装为OAuth2AuthorizationRequest后由authorizationRequestRepository(默认基于SESSION实现)将授权请求保存(后有他⽤)
2. 随后重定向到追加了参数(client-id、response_type)的真实授权码请求。
2. OAuth2LoginAuthenticationFilter
继承⾃AbstractAuthenticationProcessingFilter,即负责⾝份认证的Filter。
1. 当是loginProcessingUrl(默认为/login/oauth2/code/*)请求且带了code和state时,尝试以这俩参数构
建OAuth2LoginAuthenticationToken且调⽤AuthenticationManager去进⾏认证。
2. 认证通过后,调⽤authenticationResultConverter将认证后完全填充的OAuth2LoginAuthenticationToken转为authenticated=true
的OAuth2AuthenticationToken,⽤以代表认证后的⾝份。(该converter默认就是直接提取填充后的"principal、authorities、
clientid"直接new)
3. 将先前得到 "token、refreshToken" 等信息包装为OAuth2AuthorizedClient调⽤OAuth2AuthorizedClientRepository#saveAuthorizedClient保存起
来(默认是基于内存实现的ClientId和Principal为key的Map)
3. 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
1. OAuth2LoginAuthenticationProvider
1. 对OAuth2LoginAuthenticationToken尝试认证,其内会进⼀步构造OAuth2AuthorizationCodeAuthenticationToken,然后调⽤
OAuth2AuthorizationCodeAuthenticationProvider 对其进⾏认证。
2. 经过上述认证后拿到填充了 "access_token" 的OAuth2AuthorizationCodeAuthenticationToken,会构造成OAuth2UserRequest后传
给OAuth2UserService负责进⾏实际的 "user-info-uri" 请求,并将结果包装成DefaultOAuth2User返回。(该User拥有两类authorities,⼀个是ROLE_USER(Spring在经过oauth2UserService时⼿动添加的),⼀类是Token中的SCOPE_{sopces})
2. OAuth2AuthorizationCodeAuthenticationProvider
对OAuth2AuthorizationCodeAuthenticationToken尝试认证,内部会构造对"token-uri"的实际请求,并调
⽤DefaultAuthorizationCodeTokenResponseClient进⾏请求返回,并根据返回结果OAuth2AccessTokenResponse(内含
access_token/refreash_token),新new⼀个填充了"access_token"的OAuth2AuthorizationCodeAuthenticationToken返回。
四、oauth2ResourceServer(JWT)
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配置)处理。springboot aop
4. 相关 Authentication
1. BearerTokenAuthenticationToken
代表原始token的⼀个过渡⾝份。
2. JwtAuthenticationToken
其authenticated=true,进⾏实际系统访问的⾝份。由BearerTokenAuthenticationToken认证后,通过JwtAuthenticationConverter转换⽽来。
5. 相关 Provider
JwtAuthenticationProvider
对 BearerTokenAuthenticationToken(带access_token)进⾏认证。
1. 内部会调⽤JwtDecoder::decode(可通过.bearerTokenResolver().jwt().decode配置)对 "token" 进⾏解析&验证为Jwt对象。
2. 调⽤JwtAuthenticationConverter(可通过.bearerTokenResolver().jwt().jwtAuthenticationConverter配置)尝试对Jwt进⼀步转换为进⾏实际系统
访问的(authenticated=true)JwtAuthenticationToken返回。(默认converter内部会调⽤jwtGrantedAuthoritiesConverter解析Jwt填充
authorities(将"scpoe"/"scp"声明中空格分隔的字串转为SimpleGrantedAuthority);将"sub"字段作为 principal)
五、前后端分离实战定制
实际情况中,除了OAuth2登录,我们系统⾃⾝也有完整的⽤户体系,也有按⾃⼰业务定制的token构建分发服务。
三⽅登录仅作为绑定⼿段,⽽且在初次三⽅登录时往往还需补全信息注册到我们⾃⼰的⽤户体系。
最终实现代码以及效果展⽰都放在Github上了:
登录流程,⼤致API流程:
六、结语
从上⽂也能看出,不得不提⽤SpringSecurity很多时候宁愿迁出去⾃⼰写套Configuer/Filter/Provider…,官⽅虽提供了很多服务,⽽且也能看出在尽可能定制化,但背后还是强制引⼊了太多逻辑,很难与实际业务契合,即便稍有不同在它基础上定制也都需要付出很⼤代价。这代价不仅指新增代码⾏数,为了定制或运⾏稳定你⾸先就得彻底清楚它原本引⼊了哪些逻辑(这点官⽹⽂档⼜做得不好),这就需要源码阅读和上⼿成本。
本⽂仍存在些许问题,特别是OAuth2AuthorizationCodeGrantFilter的重定向问题,还有与⽆状态相悖的oauth2AuthorizedClientRepository涉及较少,也没⼀张清晰流程图,时间关系暂且就这样了,有什么问题还希望指正讨论。
关于 Spring Security 对 OAuth2 认证服务org.springframework.security:spring-security-oauth2-authorization-server的实现,以及前⾔提到的SpringSecurity原理、JWT等等,后⾯有时间的话也会慢慢更。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论