基于Spring Security 6的OAuth2 系列之十六 - 高级特性--PKCE

之所以想写这一系列,是因为之前工作过程中使用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未讲解的一个关键更新:设备授权码模式

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

linmoo1986

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值