基于Spring Security 6的OAuth2 系列之九 - 授权服务器--token的获取
之所以想写这一系列,是因为之前工作过程中使用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 OAuth2TokenEndpointFilter原理
- 1.1 使用Postman获取token
- 1.2 三种token的作用及内容
- 1.3 OAuth2TokenEndpointFilter原理
- 1.4 OAuth2TokenGenerator(token生成器)
- 1.5 Opaque Token 和 JWT Token
- 2 自定义JWT加密方式
前面我们对Spring Authrization Server的授权码模式都做了比较详细的解析,也知道通过/oauth2/token接口,可以获取到token,其处理是OAuth2TokenEndpointFilter过滤器,本章我们来更详细说一下OAuth2TokenEndpointFilter过滤器以及token。
1 OAuth2TokenEndpointFilter原理
1.1 使用Postman获取token
之前我们都是使用代码方式的客户端访问授权服务器,现在我们使用非代码方式,一步步看看授权码模式下如何获得token。
1)启动lesson04子模块的授权服务器
2)GET方式访问授权码:http://localhost:9000/oauth2/authorize?client_id=oidc-client&redirect_uri=http://localhost:8080/login/oauth2/code/oidc-client&response_type=code&scope=openid profile 如下图
注意:这里会跳转到登录界面。里面以一个_csrf的value,拷贝下来,登录时需要。
3)登录授权服务器,把步骤2)中的_csrf复制到此,作为请求body中的一个参数,POST请求:http://localhost:9000/login
注意:这里跳转到授权页面,有一个state返回值,拷贝下来,下一步获取授权code需要传入
4)获得授权码之前,先将Postman设置为不自动跳转,如下图
5)获得授权码,将步骤3)中的state拷贝过来作为入参。POST请求访问:http://localhost:9000/oauth2/authorize
注意:这里会获得授权码code,在返回的header的Location属性中,将code拷贝下来,请求token需要
6)获得token,将步骤5)中的授权码code拷贝过来,访问如下两个图。POST请求:http://localhost:9000/oauth2/token
注意:
- 设置Body参数,分别是grant_type、code、client_id、redirect_uri
- 设置Authorization为Basic Auth,因为我们配置的是client_secret_basic;
设置Authorization的Basic Auth
7)这时候得到access_token,就是我们需要的最终认证token,我们使用jwt在线解析,就可以看到Header、Payload以及私钥。
关于JWT的相关信息,可以参考《Spring Security 6 系列之集成JWT》
1.2 三种token的作用及内容
我们从上面步骤6中可以看到返回由三种token:
- access_token:这个就是我们拥有从资源服务器获取资源需要的token
- refresh_token:这个是用于刷新access_token使用的token,因为我们默认access_token只有5分钟有效期,需要刷新token才行
- id_token:这个是基于OIDC1.0协议身份验证的一个token,后面会详细讲OIDC1.0,它是使用JWT格式的。
其中我们来说一下access_token的内容
字段 | 说明 |
---|---|
iss (Issuer Identifier) | 提供认证信息者的唯一标识。一般是一个https的URL |
aud (Audience) | 标识 id_token 的受众。必须包含OAuth2的client_id |
nbf | token在该时间之前无效,一般设置签发的时间 |
scope | 授权范围 |
exp (Expiration time) | 过期时间,超过此时间的id_token会作废不再被验证通过 |
iat (Issued At Time) | token使用JWT发布的时间 |
auth_time (AuthenticationTime) | 用户完成认证的时间。如果客户端发送AuthN请求的时候携带max_age的参数,则此Claim是必须的 |
nonce | 客户端发送请求的时候提供的随机字符串,用来减缓重放攻击,也可以来关联ID Token和RP本身的Session信息 |
acr (Authentication Context Class Reference) | 身份验证上下文类参考 |
amr (Authentication Methods References) | 身份验证上下文类参考 |
azp (Authorized party) | 被授权方 - 向其颁发 id_token 的一方 |
1.3 OAuth2TokenEndpointFilter原理
从上面流程知道获取token需要访问/oauth2/token接口,从《系列之八-授权服务器–Spring Authrization Server的基本原理》中得知OAuth2TokenEndpointFilter过滤器是用于拦截/oauth2/token接口的。
1)那么我们先从OAuth2TokenEndpointFilter的doFilter入手,看到是通过authenticationManager,那就是熟悉使用ProviderManager中代理了OAuth2AuthorizationCodeAuthenticationProvider
2)从OAuth2AuthorizationCodeAuthenticationProvider中,我们看到如下图代码,就是使用tokenGenerator生成token
3)tokenGenertor也是一个代理DelegatingOAuth2TokenGenerator,里面有多个tokenGenertor。这里默认使用的是JwtGenerator
4)JwtGenerator中使用jwtEncoder进行生成真正JWT的token
5)从上面整个流程可以看出,主要是使用tokenGenertor进行生成
1.4 OAuth2TokenGenerator(token生成器)
从上面流程可以知道,最终token都是使用OAuth2TokenGenerator生成器生成,以下是授权服务器常见的生成器
下面介绍一下不同的token生成器:
-
DelegatingOAuth2TokenGenerator:token生成器代理,用于循环选择适合的token生成器,默认情况下,自动加载3个tokenGenerator生成器,如下图:
-
JwtGenerator:生成JWT格式的access_token和id_token,如果客户端配置中oauth2_registered_client表中字段token_settings配置为self-contained时,access_token和id_token由JwtGenerator生成,也就是会生成JWT格式的token
-
OAuth2AccessTokenGenerator:生成BASE64的access_token,长度为128,如果客户端配置中oauth2_registered_client表中字段token_settings配置为reference时,access_token和id_token由OAuth2AccessTokenGenerator生成,也就生成BASE64格式的token
-
OAuth2RefreshTokenGenerator:生成刷新refresh_token,也是基于BASE64,长度为128
1.5 Opaque Token 和 JWT Token
我们从上面知道,access_token是可以根据token_settings的配置决定生成的方式,当值为self-contained时,使用JWT Token,当值为reference时,使用BASE64格式。而这种BASE64也称为Opaque Token。大家可以尝试一下,把token_settings配置为reference,token就会是一个无意义的字符串,而JWT Token则是可以解析其中存储的信息。这Opaque Token其实是为了安全,不暴露用户信息。因此如果只是内部系统之间使用,推荐使用JWT Token,如果是共用的,推荐Opaque Token。
2 自定义JWT加密方式
我们从源码OAuth2AuthorizationServerJwtAutoConfiguration可以看到默认情况下是自动生成一个RSA非对称的加密,如下图:
但是如果使用默认的情况会有2个问题:
- 安全问题:我们知道密钥是经常需要轮换的,如果使用默认我们就无法定时轮换,当然重新启动就能切换
- 集群问题:如果我们的授权服务器是一个集群,那么每个服务器的密钥都是不一样,无法实现集群效果
因此我们只需要自定义jwkSource,就可以自己使用自己生成的RSA密钥。
代码参考lesson05子模块
1)生成自己的RSA密钥对,这个可以使用keytool
生成jks,在命令行模式下,执行下面语句
keytool -genkeypair -alias demo -keyalg RSA -keypass linmoo -storepass linmoo -keysize 2048 -keystore demo.jks
这时候会在目录下生成一个demo.jks文件,该文件包括私钥和公钥 。该文件拷贝到项目resources文件下面,供签名使用
2)新建lesson05子模块,其代码拷贝lesson04子模块的src和resources目录,以及其pom依赖
3)拷贝过来之后,注意:yaml文件中的这2处需要修改,不然mybatis-plus会加载不了entity和handler
4)SecurityConfig配置jwkSource、jwtDecoder以及读取密钥对的方法
@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())
;
// 资源服务器默认jwt配置
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
.requestMatchers("/demo", "/test").permitAll()
.anyRequest().authenticated()).formLogin(withDefaults());
return http.build();
}
/**
* 访问令牌签名
*/
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
/**
* 其 key 在启动时生成,用于创建上述 JWKSource
*/
private static KeyPair generateRsaKey() {
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("demo.jks"), "linmoo".toCharArray());
KeyPair keyPair = factory.getKeyPair("demo", "linmoo".toCharArray());
return keyPair;
}
}
5)当然,这个部分我们还可以改进,就是把密钥对放到nacos或者redis上面,这样我们可以手动更新。
结语:本章我们讲解了OAuth2的token以及Spring Security如何实现的底层原理,并自定义jwkSource来实现使用自己的RSA的密钥对。其中还有一个点没有讲,就是token的刷新。下一章我们将详细讲一下如何刷新token以及其代码原理。