PKCE3-PKCE实现(SpringBoot3.0)

在 Spring Boot 3.0 + JDK 17 的环境下,实现 PKCE 认证的核心步骤包括:

1)引入依赖:使用 Spring Security OAuth 2.0 客户端进行授权码流程。

2)配置 OAuth 2.0 客户端:在 Spring Boot 中配置 OAuth 2.0 客户端,包括设置 code_verifier 和 code_challenge。

3)实现 PKCE 流程:在授权请求中生成 code_challenge,并在换取访问令牌时使用 code_verifier。

1.实现步骤

下面是一个完整的 Spring Boot 3.0 应用实现 PKCE 授权码流程的步骤。

1.1.引入依赖

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>

  <groupId>com.me.mengyu.auth.net</groupId>

  <artifactId>mengyu-love</artifactId>

  <version>0.0.1-SNAPSHOT</version>

  <packaging>war</packaging>

  <description>Auth</description>

  <dependencies>

  <!-- JWT认证利用 -->

  <dependency>

    <groupId>io.jsonwebtoken</groupId>

    <artifactId>jjwt-api</artifactId>

    <version>0.11.5</version>

    </dependency>

    <dependency>

      <groupId>io.jsonwebtoken</groupId>

      <artifactId>jjwt-impl</artifactId>

      <version>0.11.5</version>

      <scope>runtime</scope>

    </dependency>

    <dependency>

      <groupId>io.jsonwebtoken</groupId>

      <artifactId>jjwt-jackson</artifactId>

      <version>0.11.5</version>

  </dependency>

  

  <!-- OIDC认证利用 -->

  <dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-oauth2-client</artifactId>

    <version>3.0.0</version>

  </dependency>

  <dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-web</artifactId>

    <version>3.0.0</version>

  </dependency>

  <dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-thymeleaf</artifactId>

    <version>3.0.0</version>

  </dependency>

  <dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-security</artifactId>

    <version>3.0.0</version>

  </dependency>

  <!-- Spring Security OAuth2 Resource Server -->

  <dependency>

      <groupId>org.springframework.boot</groupId>

      <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>

      <version>3.0.0</version>

    </dependency>

</dependencies>

<build>

    <plugins>

      <plugin>

        <groupId>org.apache.maven.plugins</groupId>

        <artifactId>maven-war-plugin</artifactId>

        <version>3.3.2</version>

        <configuration>

            <failOnMissingWebXml>false</failOnMissingWebXml>

        </configuration>

      </plugin>

    </plugins>

  </build>

</project>

1.2.配置 OAuth2 客户端

server:

  port: 8181

spring:

  #不同的身份提供者有不同的配置

  security:

    oauth2:

      client:

        registration:

          my-client:

            #client-id, client-secret需要去QQ开发中心获取

            client-id: your-client-id

            client-secret: your-client-secret

            scope: openid, profile, email

            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"

            authorization-grant-type: authorization_code

            provider: my-provider  # 指定身份提供者的 ID

        provider:

          my-provider:

            #authorization-uri: https://your-authorization-server.com/auth

            #token-uri: https://your-authorization-server.com/token

            #user-info-uri: https://your-authorization-server.com/userinfo

            #authorization-uri: https://accounts.google.com/o/oauth2/auth

            #token-uri: https://oauth2.googleapis.com/token

            #user-info-uri: https://openidconnect.googleapis.com/v1/userinfo

            #authorization-uri: https://graph.qq.com/oauth2.0/authorize

            #token-uri: https://graph.qq.com/oauth2.0/token

            #user-info-uri: https://graph.qq.com/oauth2.0/me

            #user-info-auth-method: query

            # 模拟认证服务器 mengyu-sim-oauth-userserver

            authorization-uri: https://graph.qq.com/oauth2.0/authorize

            token-uri: https://graph.qq.com/oauth2.0/token

            user-info-uri: https://graph.qq.com/oauth2.0/me

          #github:

            #authorization-uri: https://github.com/login/oauth/authorize

            #token-uri: https://github.com/login/oauth/access_token

            #user-info-uri: https://api.github.com/user

         

1.3.PKCE的实现

Spring Security 5.5 及以上版本已经内置了 PKCE 支持,Spring Boot 3.0 采用了 Spring Security 的最新版本,因此我们不需要手动生成 code_verifier 和 code_challenge,而是通过配置和默认行为来完成 PKCE 验证。

1.3.1.配置 Security Filter

在 Spring Boot 3.0 中,默认情况下,OAuth2 客户端支持 PKCE。因此,当客户端是公共客户端时,Spring Security 将自动处理 PKCE 流程。

你需要在 SecurityConfig.java 中配置 Spring Security,使应用程序处理 OAuth 2.0 登录流程:

  @Bean

  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

      http

          .authorizeRequests()

              .requestMatchers("/", "/login", "/error").permitAll() // 允许所有用户访问的页面

              .anyRequest().authenticated() // 其余请求需要认证

          .and()

          .oauth2Login()

              .loginPage("/login") // 自定义登录页

              .defaultSuccessUrl("/home", true) // 登录成功后的默认跳转页

              .failureUrl("/login?error=true") // 登录失败后的跳转页

          .and()

          .exceptionHandling()

              .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")); // 未认证用户访问的处理

      

      return http.build(); // 返回构建的 HttpSecurity

  }

这里配置了 OAuth 2.0 登录流程,并允许匿名访问根目录和登录页面。所有其他请求都需要经过认证。

1.3.2.使用DefaultOAuth2AuthorizationRequestResolver 进行 PKCE

默认情况下,Spring Security 使用 DefaultOAuth2AuthorizationRequestResolver 来自动生成 code_challenge 并将其包含在 OAuth2 授权请求中。对于公共客户端(例如浏览器中的单页应用),它会自动应用 PKCE 流程。

1.4.自定义授权请求(可选)

如果需要自定义 PKCE 相关的内容(例如指定 code_challenge_method),可以通过 OAuth2AuthorizationRequestCustomizer 来定制授权请求。

以下是一个自定义 Authorization Request Resolver 的示例,手动配置 code_challenge 和 code_challenge_method:

package com.me.mengyu.love.resolver;

import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;

import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;

import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;

import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;

import jakarta.servlet.http.HttpServletRequest;

import java.nio.charset.StandardCharsets;

import java.security.MessageDigest;

import java.security.NoSuchAlgorithmException;

import java.security.SecureRandom;

import java.util.Base64;

import java.util.HashMap;

import java.util.Map;

public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {

    private final DefaultOAuth2AuthorizationRequestResolver defaultResolver;

    // 使用新构造函数,authorizationRequestBaseUri为OAuth2授权端点的基础URI

    public CustomOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {

        this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, "/oauth2/authorization");

    }

    //@Override

    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {

        OAuth2AuthorizationRequest authorizationRequest = defaultResolver.resolve(request);

        return customizeAuthorizationRequest(authorizationRequest);

    }

    

    

    // 覆盖 resolve(HttpServletRequest, String)

    //@Override

    public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {

        OAuth2AuthorizationRequest authorizationRequest = defaultResolver.resolve(request, clientRegistrationId);

        return customizeAuthorizationRequest(authorizationRequest);

    }

    

    // Customize the authorization request to include PKCE parameters

    private OAuth2AuthorizationRequest customizeAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest) {

        if (authorizationRequest == null) {

            return null;

        }

        // Create a mutable map for additional parameters

        Map<String, Object> additionalParameters = new HashMap<String, Object>(authorizationRequest.getAdditionalParameters());

        // Generate code_verifier and code_challenge

        String codeVerifier = generateCodeVerifier();

        String codeChallenge = generateCodeChallenge(codeVerifier);

        // Add PKCE parameters to the request

        additionalParameters.put("code_challenge", codeChallenge);

        additionalParameters.put("code_challenge_method", "S256");

        // Create a new authorization request with the PKCE parameters

        return OAuth2AuthorizationRequest.from(authorizationRequest)

                .additionalParameters(additionalParameters)

                .build();

    }

    // Generate a high-entropy code_verifier

    private String generateCodeVerifier() {

        SecureRandom secureRandom = new SecureRandom();

        byte[] codeVerifierBytes = new byte[32];

        secureRandom.nextBytes(codeVerifierBytes);

        return Base64.getUrlEncoder().withoutPadding().encodeToString(codeVerifierBytes);

    }

    // Generate code_challenge from code_verifier using SHA-256

    private String generateCodeChallenge(String codeVerifier) {

        try {

            MessageDigest digest = MessageDigest.getInstance("SHA-256");

            byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8));

            return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);

        } catch (NoSuchAlgorithmException e) {

            throw new RuntimeException("SHA-256 algorithm not found");

        }

    }

}

然后在 SecurityConfig 中将这个 CustomOAuth2AuthorizationRequestResolver 注册为授权请求解析器:

  public SecurityFilterChain securityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception {

      OAuth2AuthorizationRequestResolver customAuthorizationRequestResolver = new CustomOAuth2AuthorizationRequestResolver(clientRegistrationRepository);

      http

          .authorizeRequests()

              .requestMatchers("/", "/login", "/error").permitAll() // 允许所有用户访问的页面

              .anyRequest().authenticated() // 其余请求需要认证

          .and()

          .oauth2Login()

              .loginPage("/login") // 自定义登录页

              .defaultSuccessUrl("/home", true) // 登录成功后的默认跳转页

              .failureUrl("/login?error=true") // 登录失败后的跳转页

          .and()

          .exceptionHandling()

              .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")); // 未认证用户访问的处理

      

      return http.build(); // 返回构建的 HttpSecurity

  }

1.5.启动应用

1)启动 Spring Boot 应用。

2)访问 http://localhost:8080,系统将会重定向到 OAuth 2.0 提供的登录页面。

3)完成 OAuth 2.0 登录后,将会使用 PKCE 完成整个授权流程。

1.6.总结

•  PKCE 自动化:Spring Security 5.5 及以上版本自动支持 PKCE,因此在大多数情况下不需要手动生成 code_verifier 和 code_challenge。

•  定制化支持:如果需要自定义 PKCE 处理,可以通过 OAuth2AuthorizationRequestResolver 来实现。

上一篇:unity3D雨雪等粒子特效不穿透房屋效果实现


下一篇:基于HTML、CSS和JavaScript的滚动数字显示效果,类似于老式计数器或电子表上的数字滚动效果