• 【OAuth2】二十、OAuth2扩展协议 PKCE


    一、什么是PKCE

    PKCE 全称是 Proof Key for Code Exchange(代码交换证明密钥), 在2015年发布, 它是 OAuth 2.0 核心的一个扩展协议, 所以可以和现有的授权模式结合使用,比如 Authorization Code + PKCE, 这也是最佳实践,PKCE 最初是为移动设备应用和本地应用创建的, 主要是为了减少公共客户端的授权码拦截攻击。

    PKCE其实主要是通过在授权的过程中增加了code_challenge和code_verifier两个元素来对整个流程进行验证,防止code被第三方截取的情况。 实际上它的原理是客户端提供一个自创建的证明给授权服务器, 授权服务器通过它来验证客户端,把访问令牌(access_token) 颁发给真实的客户端而不是伪造的。
    参考
    参考2

    1、回顾OAuth2.0授权码模式流程:

    https://www.rfc-editor.org/rfc/rfc6749
    在这里插入图片描述

    (A)客户端请求资源所有者授权。 可以直接向资源所有者发出授权请求 (如所示),或者最好通过授权间接地 服务器作为中介。

    (B)客户端收到授权授予,这是a 表示资源所有者授权的凭据, 使用本例中定义的四种授权类型之一表示 说明或使用延期授权类型方法使用的方法决定了授权授予类型
    客户机请求授权和支持的类型 授权服务器。

    ©客户端通过认证请求访问令牌 授权服务器,并给出授权授权。

    (D)授权服务器对客户端进行认证和验证 授权授予,如果有效,将发出访问令牌。

    2、 授权码模式

    在这里插入图片描述

    • 1、客户端携带 client_id, scope, redirect_uri, state 等信息引导用户请求授权服务器的授权端点下发 code。
    • 2、授权服务器验证客户端身份,验证通过则询问用户是否同意授权(此时会跳转到用户能够直观看到的授权页面,等待用户点击确认授权)。
    • 3、假设用户同意授权,此时授权服务器会将 code 和 state(如果客户端传递了该参数)拼接在 redirect_uri 后 面,以302(重定向)形式下发 code。
    • 4、客户端携带 code, redirect_uri, 以及 client_secret 请求授权服务器的令牌端点下发 access_token。
    • 5、授权服务器验证客户端身份,同时验证 code,以及 redirect_uri 是否与请求 code 时相同,验证通过后下发 access_token,并选择性下发 refresh_token,支持令牌的刷新。
      参考

    3、PKCE的流程

    PKCE主要是通过在授权的过程中增加了code_challenge和code_verifier两个元素来对整个流程进行验证,防止code被第三方截取的情况。具体流程如下:
    在这里插入图片描述
    也就是在原来请求的token中添加了code_challenge和code_verifier参数

    • code_verifier
      一个Client端生成的随机字符串(由字母,数字,- ,. , ,~ 组成)。
      调用应用程序(SPA)创建的密钥,该密钥可由授权服务器验证密钥被称为 Code Verifier(代码验证器)
    • code_challenge
      调用应用程序为 Code Verifier 创建一个转换值,称为 Code Challenge,HTTPS 发送该值(Code Challenge)去检索 Authorization Code.
      这样就提高了从认证服务器获取 token的安全性。

    4、 PKCE的流程说明

    • 1、 OAuth2客户端生成 code_verifier,并使用 code_challenge_method 计算 code_challenge,具体的算法稍后会详细讲解。
    • 2、OAuth2客户端发起/oauth2/authorize授权请求,携带 code_challenge 和 code_challenge_method 这两个参数。
    • 3、OAuth2授权服务器对OAuth2客户端/oauth2/authorize的授权请求进行验证。
    • 4、 OAuth2授权服务器检查 code_challenge 和 code_challenge_method 是否存在。
    • 5、 如果步骤4存在这两个参数,授权服务器会持久化code_challenge 和 code_challenge_method这两个参数。
    • 6、然后OAuth2授权服务器对授权码请求进行响应。
    • 7、 OAuth2客户端收到授权码响应开始调用/oauth2/token请求访问令牌,该请求需要额外附加初始请求中生成的code_verifier参数。
    • 8、 OAuth2授权服务器收到访问令牌请求,用该请求中携带的code_verifier和步骤5持久化的code_challenge_method进行摘要计算生成一个校验串,该校验串必须和步骤5持久化的code_challenge进行匹配校验。
    • 29、如果步骤8中的匹配校验成功,则发放访问令牌access_token,否则该请求被拒绝。

    5、PKCE请求演示

    5.1 没有PKCE的授权请求:

    http://localhost:9000/oauth2/authorize?response_type=code&client_id=felord&scope=message.read message.write&state=46ge_TeI-dHuAnyv67nVmCcAmFgCVSZAqjTi9Om-1aA=&redirect_uri=http://127.0.0.1:8082/test/bar
    
    • 1

    5.2 有PKCE的授权请求:

    http://localhost:9000/oauth2/authorize?response_type=code&client_id=felord&scope=message.read%20message.write&state=NAqBLbmooEhMPGELwleACOHvybODP_hctqV-PuNjuxo%3D&redirect_uri=http://127.0.0.1:8082/test/bar&code_challenge=mQ4xjFrCXg-1P4iURSXcSCmGVc-dloG0b0sdGpICrN0&code_challenge_method=S256
    
    • 1

    6、Spring Security PKCE的实现

    pkce 分支

    6.1、 OAuth2 客户端配置

    OAuth2客户端要生成code_verifier,并使用 code_challenge_method 计算 code_challenge,并封装到授权请求参数,需要改造OAuth2AuthorizationRequestResolver接口。

       @Bean
        SecurityFilterChain customSecurityFilterChain(HttpSecurity http,  ClientRegistrationRepository clientRegistrationRepository) throws Exception {
    
    
            OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient = accessTokenResponseClient();
            //TODO OAuth2客户端测要生成code_verifier,并使用 code_challenge_method 计算 code_challenge,并封装到授权请求参数,需要改造OAuth2AuthorizationRequestResolver接口。
            DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
    
            authorizationRequestResolver.setAuthorizationRequestCustomizer(builder -> builder.attributes(attributes -> {
                if (!attributes.containsKey(PkceParameterNames.CODE_VERIFIER)) {
                    String codeVerifier = this.secureKeyGenerator.generateKey();
                    attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
    
                    builder.additionalParameters(additionalParameters -> {
                        try {
                            MessageDigest md = MessageDigest.getInstance("SHA-256");
                            byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
                            String codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
                            additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
                            additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
                        } catch (NoSuchAlgorithmException ex) {
                            //  plain  方式  这种方式几乎作废了
                            additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
                        }
                    });
    
                }
            }));
            http.authorizeRequests((requests) -> requests
                            .antMatchers("/test/bar", "/oauth2/jwks")
                            .hasAnyAuthority("ROLE_ANONYMOUS", "SCOPE_userinfo")
                            .anyRequest().authenticated())
                 //然后把改造好的OAuth2AuthorizationRequestResolver配置到HttpSecurity:   .oauth2Login().authorizationEndpoint().authorizationRequestResolver(authorizationRequestResolver)
                    .and()
                    // 获取token端点配置  比如根据code 获取 token
                    .tokenEndpoint().accessTokenResponseClient(accessTokenResponseClient);
    
    
            http.oauth2Client()
                    .authorizationCodeGrant().authorizationRequestResolver(authorizationRequestResolver)
                    .accessTokenResponseClient(accessTokenResponseClient);
            return http.build();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    6.1、 Spring Authorization Server配置

    客户端配置要添加 .requireProofKey(true)

        private RegisteredClient createJwtRegisteredClient(final String id) {
            return RegisteredClient.withId(id)
    //               客户端ID和密码
                    .clientId("testid")
    //                名称 可不定义
                    .clientName("test")
                    // jwt 断言必备
                    .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
                    .clientSettings(ClientSettings.builder()
                            .tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256)
                           // 开启PKCE
                            .requireProofKey(true)
                            // private key jwt
                            .jwkSetUrl("http://localhost:8082/oauth2/jwks")
                            .build())
                    .build();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
  • 相关阅读:
    论文阅读-FCD-Net: 学习检测多类型同源深度伪造人脸图像
    高项 成本管理论文
    Qt消除警告
    shell实现日期加减
    简述for in 和 for of 的区别
    代码随想录算法训练营第五十天| 309.最佳买卖股票时机含冷冻期 714.买卖股票的最佳时机含手续费
    国王游戏NOIP
    不要在问了!工作六年总结的Java面试题与经验
    基本的计算机结构知识----基础向
    【Vue】Element开发笔记
  • 原文地址:https://blog.csdn.net/weixin_43333483/article/details/126256938