云计算百科
云计算领域专业知识百科平台

基于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以及其代码原理。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 基于Spring Security 6的OAuth2 系列之九 - 授权服务器--token的获取
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!