之所以想写这一系列,是因为之前工作过程中使用Spring Security OAuth2搭建了网关和授权服务器,但当时基于spring-boot 2.3.x,其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0,结果一看Spring Security也升级为6.3.0。无论是Spring Security的风格和以及OAuth2都做了较大改动,里面甚至将授权服务器模块都移除了,导致在配置同样功能时,花费了些时间研究新版本的底层原理,这里将一些学习经验分享给大家。
注意:由于框架不同版本改造会有些使用的不同,因此本次系列中使用基本框架是 spring-boo-3.3.0(默认引入的Spring Security是6.3.0),JDK版本使用的是19,本系列OAuth2的代码采用Spring Security6.3.0框架,所有代码都在oauth2-study项目上:https://github.com/forever1986/oauth2-study.git
目录
- 1 什么是令牌交换(Token Exchange)
- 1.1 令牌交换(Token Exchange)的定义
- 1.2 令牌交换(Token Exchange)的场景
- 1.3 令牌交换(Token Exchange)的流程
- 2 代码示例
- 3 底层原理
上两章我们讲了OIDC协议的定义、作用到实现以及代码原理。这一章我们来学习另外一个高级特性Token Exchange(令牌交换)。
1 什么是令牌交换(Token Exchange)
1.1 令牌交换(Token Exchange)的定义
令牌交换(Token Exchange)来自于RFC8693协议。OAuth 2.0 Token Exchange 是标准 OAuth 2.0 协议的扩展。它使客户端应用程序能够从充当安全令牌服务(Security Token Service STS) 的授权服务器请求和获取安全令牌(例如访问令牌)。
STS 是一项服务,负责验证提供给它的令牌并在响应中颁发新令牌,这使客户端应用程序能够为分布式环境中的资源获取适当的安全令牌。换句话说,客户端应用程序可以使用来自第三方授权服务器的访问令牌请求访问资源,将其令牌交换为目标授权服务器提供的访问令牌,然后使用此令牌对请求进行身份验证。
1.2 令牌交换(Token Exchange)的场景
上面的定义可能让你觉得很难懂,我们举一个具体场景,你可能就会很好的理解令牌交换(Token Exchange)。假如有这样一个场景,你的客户端使用API A服务,这时候你的客户端先去授权服务器授权并获得访问API A服务的权限的access_token。但是如果你客户端调用API A服务的接口里面需要调用另外一个 API B服务(这在现在微服务分布式架构中非常常见),那么你会如何做?下面有两种方式可供参考:
- 方式一:扩大你从授权服务器获取到的token的权限,比如受众aud或者范围scopes,然后让你的API B服务也对该token的认可。这种方式虽然可以,但是会导致API B权限控制有些失控,本来你是访问API A的,但是却要同时申请API B。
- 方式二:重新获取token,按照授权服务器的方式,你API A重新去获取授权服务器对API B的授权。这个也是可以的,但是也有一个问题,这时候token的内容sub对象就是API A,对于API B来说它就无法知道其实是你的客户端在访问它。
那么有没有更好的方案,下面是OAuth2.0给出来的方案:令牌交换(Token Exchange)RFC8693协议
1.3 令牌交换(Token Exchange)的流程
从前面的定义和场景描述,我们可以梳理的访问流程如下:
初步看这个流程和前面提倡的方式二没什么区别,API A还是需要去授权服务器获取新的token,但是这个交换token和申请新token就有很大的不一样,其中最大的不一样是sub主题还是客户端,并非API A。这就是和重新申请新token最大的区别。下面我们先通过代码来展示一下Spring Security的Spring Authrization Server应该如何实现令牌交换。
2 代码示例
代码参考lesson14子模块
1)新建lesson14子模块,其pom引入如下:
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-authorization-server</artifactId></dependency>
</dependencies>
2)在resources目录下,新建yaml配置Spring Security的认证信息
server:port: 9000logging:level:org.springframework.security: tracespring:security:# 使用security配置授权服务器的登录用户和密码user:name: userpassword: 1234
3)在config包下,新建SecurityConfig配置
@Configuration
public class SecurityConfig {// 自定义授权服务器的Filter链@Bean@Order(Ordered.HIGHEST_PRECEDENCE)SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)// oidc配置.oidc(withDefaults());// 同时作为资源服务器,使用/userinfo接口http.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults()));// 异常处理http.exceptionHandling((exceptions) -> exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")));return http.build();}// 自定义Spring Security的链路。如果自定义授权服务器的Filter链,则原先自动化配置将会失效,因此也要配置Spring Security@Bean@Order(SecurityProperties.BASIC_AUTH_ORDER)SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()).formLogin(withDefaults());return http.build();}@Beanpublic RegisteredClientRepository registeredClientRepository() {// 客户端访问API A的授权RegisteredClient registeredClient1 = RegisteredClient.withId(UUID.randomUUID().toString())// 客户端id.clientId("api-a")// 客户端密码.clientSecret("{noop}secret1")// 客户端认证方式.clientAuthenticationMethods(methods ->{methods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);})// 配置授权码模式.authorizationGrantTypes(grantTypes -> {grantTypes.add(AuthorizationGrantType.AUTHORIZATION_CODE);})// 需要授权确认.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())// 回调地址.redirectUri("http://localhost:8080/login/oauth2/code/oidc-client").postLogoutRedirectUri("http://localhost:8080/")// 授权范围.scopes(scopes->{scopes.add(OidcScopes.OPENID);scopes.add(OidcScopes.PROFILE);}).build();// API A访问API B的授权RegisteredClient registeredClient2 = RegisteredClient.withId(UUID.randomUUID().toString())// 客户端id.clientId("api-b")// 客户端密码.clientSecret("{noop}secret2")// 客户端认证方式:因为是API A做令牌交换,因此没有用户参与,使用客户端模式.clientAuthenticationMethods(methods ->{methods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);})// 配置令牌交换.authorizationGrantTypes(grantTypes -> {grantTypes.add(AuthorizationGrantType.TOKEN_EXCHANGE);})// 回调地址.redirectUri("http://localhost:8080/login/oauth2/code/oidc-client").postLogoutRedirectUri("http://localhost:8080/")// 授权范围.scopes(scopes->{scopes.add("clientB");}).build();return new InMemoryRegisteredClientRepository(registeredClient1, registeredClient2);}
}
4)新建启动类Oauth2Lesson14STSServerApplication,并启动项目
5)演示:使用postman登录并获取授权码code
6)演示:使用postman获取能访问api-a的access_token
7)演示:通过交换令牌,获取可以访问api-b的access_token
8)我们通过在线解析JWT的工具,可以看到,重新交换的access_token,其sub主体依旧是user。
3 底层原理
关于令牌交换(Token Exchange)的原理,在Spring Security的Spring Authrization Server中,它将其当作一种扩展授权模式方式来实现,因此只需要看3个类即可,OAuth2TokenExchangeAuthenticationToken、OAuth2TokenExchangeAuthenticationConverter和OAuth2TokenExchangeAuthenticationProvider,这也是Spring Security的一种常见的实现规范,很多自定义方式也是通过重写这3个实现类就能够完成。
1)OAuth2TokenExchangeAuthenticationToken:主要用于保存上下文信息,比如令牌交换(Token Exchange)就扩展了一些自己需要的内容
2)OAuth2TokenExchangeAuthenticationConverter:主要看convert方法,其中要求必须有subject_token和subject_token_type两个参数
3)OAuth2TokenExchangeAuthenticationProvider:provider主要看其authenticate方法,这个和获取token的OAuth2AuthorizationCodeAuthenticationProvider区别不大,唯一区别就是会获取subjecttoken,去查询上一次token信息,并将sub主题放入新的token
4)因此,我们大概知道其实现原理,就是通过实现一种新的授权方式,这种授权方式在返回的access_token做了一些不一样的处理。
结语:本章我们详解讲解了令牌交换(Token Exchange)的场景和实现,还包括底层源码解析。我们可以看到Spring Authrization Server中是将其当作一种扩展授权模式方式来实现。那么下一章我们也来实现自己自定义授权模式方式。