之所以想写这一系列,是因为之前工作过程中使用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
上一章,我们将来客户端前四种的认证方式,这一章来讲到none的认证方式,这种认证方式其实和OAuth2.1协议有关,因此放到本章。本章会先讲关于OAuth2.1有什么新的特性,之后再说none认证与PKCE。
Spring Authrization Server其实是极具前瞻性,他们不仅实现了OAuth2.1(该协议还只是个草稿,目前出了13个版本了),同时还新增OIDC1.0协议。下面我们就这些新的特性一样讲解,这样让我们能更好使用Spring Authrization Server
1 OAuth2.1新的特性
虽然OAuth2.1只是一个草案阶段,但是Spring Authrization Server还是对其做了实现。我们主要来看看相对于OAuth2.0,OAuth2.1做了一下关键的改变:
- 1)废除OAuth2.0中的密码模式和简化模式。我们从《系列之一 - 开篇入门》中知道,这两种模式很不安全,因此废除。(因此在本次系列中只采用授权码模式和客户端模型进行演示)
- 2)对授权码模式增加了PKCE扩展,PKCE的官方文档RFC 7636,这是本章重点,后面会讲
- 3)增加了设备授权码模式,参考RFC 8628,主要为了支持智能电视、游戏机、IoT 设备等无法输入凭证的一种模式(这个下一章讲)
- 4)增加了刷新token模式,之前虽然2.0有说到刷新token,但是2.1给出完整流程规范。这部分我们在《系列之十 - 授权服务器–刷新token》中已经讲过token的刷新
2 PKCE扩展
2.1 为什么需要PKCE扩展
我们先回顾一下《系列之一 - 开篇入门》中讲到的授权码模式:
- A:你(User-Agent)打开第三方分析应用(Client客户端),会跳转到淘宝(Authorization Server授权服务器)的授权界面
- B:你(User-Agent)点击确认授权
- C:淘宝(Authorization Server授权服务器)会返回一个授权码Code(注意:该Code并非令牌,而是一个临时授权码Code,为了后面能获得令牌),第三方分析应用(Client客户端)得到这个授权码Code。
- D:第三方分析应用(Client客户端)使用授权码Code,再次访问淘宝(Authorization Server授权服务器)去获得令牌token
- E:淘宝(Authorization Server授权服务器)验证授权码code无误后,返回令牌token。
我们看到在步骤C是先返回一个授权码Code,这个授权码可以去请求token,这时候如果有黑客拦击到这个授权码Code,也是可以获得token,这就比较危险。为了减轻这种攻击,就引入PKCE。
PKCE:全称Proof Key for Code Exchange by OAuth Public Clients。下面我们从官方文档摘取流程,说明一下PKCE如何加强这方面的安全:
- 1)A:客户端发送获取授权码Code时,会新增两个参数,一个是加密算法t_m,一个是将一个值coder_verfier进行加密后t(coder_verfier)。
- 2)B:授权服务器返回授权Code,同时会记录下加密算法t_m和加密后的coder_verfier值
- 3)C:客户端去获取token时,带上未加密的coder_verfier,也就是原始值。授权服务器会验证coder_verfier是否正确(通过步骤B保存的加密算法t_m和加密后的coder_verfier值)
可以看出,PKCE是对授权码模式的改进,因此就是一个增强版的授权码模式
2.2 PKCE的底层原理
参数转换:PublicClientAuthenticationConverter
认证处理:PublicClientAuthenticationProvider
1)none认证方式首先在我们请求授权码code时,对于其参数解析的OAuth2AuthorizationCodeRequestAuthenticationConverter会去判断code_challenge和code_challenge_method参数
2)在授权码code的Provider中OAuth2AuthorizationCodeRequestAuthenticationProvider,会对是否存在code_challenge和code_challenge_method参数进行判断,其中配置了requireProofKey=true,则是必需使用PKCE方式
3)因为请求授权码时,会将信息先存储在oauth2_authorization,其实code_challenge和code_challenge_method参数都保存在attributes字段中
4)再次请求token时,会使用PublicClientAuthenticationConverter中判断是否存在code_verifier参数
5)如果参数没问题,那么在PublicClientAuthenticationProvider中进行认证
6)从上图我们知道使用一个CodeVerifierAuthenticator进行验证,如下图会获取之前保存的信息,在使用code_verifier参数进行验证
7)我们再看看codeVerifierValid方法,其使用的是SHA-256进行验证,而且codeChallengeMethod参数是S256。
至此,我们就了解了其工作原理。下面我们通过一个示例,进行演示一遍
2.3 代码实现
代码参考lesson10子模块
1)新建lesson10子模块,其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: 9000
logging:
level:
org.springframework.security: trace
spring:
security:
# 使用security配置授权服务器的登录用户和密码
user:
name: user
password: 1234
3)在config包下,创建配置类SecurityConfig,对客户端信息、授权服务器以及Spring Security进行配置
@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());
// 异常处理
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("/jwt").permitAll()
.anyRequest().authenticated()).formLogin(withDefaults());
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient3 = RegisteredClient.withId(UUID.randomUUID().toString())
// 客户端id
.clientId("oidc-client")
// 客户端密码
.clientSecret("{noop}secret")
// 客户端认证方式
.clientAuthenticationMethods(methods ->{
methods.add(ClientAuthenticationMethod.NONE);
})
// 客户端配置requireProofKey=true
.clientSettings(ClientSettings.builder()
.requireProofKey(true)
.requireAuthorizationConsent(true)
.build())
// 授权码模式
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
// 回调地址
.redirectUri("http://localhost:8080/login/oauth2/code/oidc-client")
.postLogoutRedirectUri("http://localhost:8080/")
// 授权范围
.scopes(scopes->{
scopes.add(OidcScopes.OPENID);
scopes.add(OidcScopes.PROFILE);
})
.build();
return new InMemoryRegisteredClientRepository(registeredClient3 );
}
}
4)在utils包下,新建PkceTest类,用于生成加密的参数code_challenge
public class PkceTest {
public static void main(String[] args) throws Exception{
// 明文
String code_verifier = "linmoo";
// 摘要算法
String code_challenge_method = "SHA-256";
// 密文
byte[] bytes = code_verifier.getBytes(StandardCharsets.US_ASCII);
MessageDigest md = MessageDigest.getInstance(code_challenge_method);
byte[] digest = md.digest(bytes);
String code_challenge = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
System.out.println(code_challenge);
}
}
5)创建启动类Oauth2Lesson10ServerApplication,并启动项目
@SpringBootApplication
public class Oauth2Lesson10ServerApplication {
public static void main(String[] args) {
SpringApplication.run(Oauth2Lesson10ServerApplication.class, args);
}
}
6)测试效果
运行PkceTest类的main函数,生成一个加密的数据
请求授权码code
登录
获取授权码code(记得设置postman不自动跳转)
获取token
结语:本章我们了解了none情况下的认证模式,可以通过PKCE进行增强。其实PKCE不止是none认证方式,所有认证方式下都可以加入PKCE,但是PKCE只能用于授权码模式,不能用于客户端模式。下一章,我们将继续OAuth2.1未讲解的一个关键更新:设备授权码模式