【问题标题】:Spring Security OAuth 2.0 - client secret always required for authorization code grantSpring Security OAuth 2.0 - 授权代码授予始终需要客户端密码
【发布时间】:2026-02-23 03:15:01
【问题描述】:

根据规范,只要client_id 包含在请求中 client_id 相同,就不需要对使用授权码授予的令牌请求进行身份验证一个用于生成代码。但是,使用 Spring Security OAuth 2.0 实现,即使从未为客户端分配秘密,/oauth/token 端点上似乎也始终需要基本身份验证。

由于ClientDetails 接口中的isSecretRequired() 方法,似乎支持允许没有秘密的客户端。我需要做什么才能使没有秘密的客户端能够在/oauth/token URL 上进行身份验证?

4.1.3。访问令牌请求

客户端通过发送
以下参数使用“application/x-www-form-urlencoded”
HTTP 中字符编码为 UTF-8 的附录 B 格式
请求实体主体:

grant_type 必需的。值必须设置为“authorization_code”。

代码 必需的。收到的授权码 授权服务器。

redirect_uri 必需,如果“redirect_uri”参数包含在 第 4.1.1 节中描述的授权请求,及其 值必须相同。

client_id 必需的,如果客户端没有与 授权服务器,如第 3.2.1 节所述。

如果客户类型是保密的或客户是发给客户的 凭据(或分配的其他身份验证要求),
客户端必须按照所述向授权服务器进行身份验证
在第 3.2.1 节中。

【问题讨论】:

    标签: spring security oauth-2.0 spring-security-oauth2


    【解决方案1】:

    我使用 Spring boot 2.5.0,不需要allowFormAuthenticationForClients()。我的 PasswordEncoder 如下所示:

        @Bean
        public PasswordEncoder passwordEncoder() {
            return new PasswordEncoder() {
                @Override
                public String encode(CharSequence charSequence) {
                    return BCrypt.hashpw(charSequence.toString(), BCrypt.gensalt());
                }
    
                @Override
                public boolean matches(CharSequence charSequence, String s) {
                    return BCrypt.checkpw(charSequence.toString(), s);
                }
            };
        }
    

    我通过这个website将秘密生成为空字符串

    只需单击“Bcrypt”按钮并获取您将插入数据库的哈希值。

    【讨论】:

      【解决方案2】:

      这对我有用

          @Override
          public void configure(AuthorizationServerSecurityConfigurer cfg) throws Exception {
              cfg
                  .tokenKeyAccess("permitAll()")
                  .checkTokenAccess("isAuthenticated()")
                  .allowFormAuthenticationForClients()
                  .passwordEncoder(clientPasswordEncoder());
          }
      
      
          @Bean("clientPasswordEncoder")
          PasswordEncoder clientPasswordEncoder() {
              return new BCryptPasswordEncoder(4);
          }
      

      测试 1:

      测试 2:

      【讨论】:

        【解决方案3】:

        为了解决问题,请参见类的方法loadUserByUsernameorg.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService:

        if (clientSecret == null || clientSecret.trim().length() == 0) {
           clientSecret = this.emptyPassword;
        }
        

        在您的情况下,emptyPassword 可能尚未由您的密码编码器使用空编码密码进行初始化。 在AuthorizationServerConfigurerAdapter中设置缺少的密码编码器:

        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
            oauthServer.passwordEncoder(passwordEncoder);
        }
        

        【讨论】:

          【解决方案4】:

          最初我有一个与接受的答案类似的设置,这绝对是完成这项工作的先决条件。但是缺少的是您不能简单地将密码设置为空。您必须将其设置为空密码,例如:

          String secret = PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("");
          clientDetails.setClientSecret(secret);
          

          如果你不这样做,你仍然会得到 401!

          【讨论】:

            【解决方案5】:

            像这样:

            @Override
            public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
                clients.inMemory()
                        .withClient("client").secret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("")) //empty
                        .authorizedGrantTypes("authorization_code", "refresh_token")
                        .redirectUris("http://www.dev.com")
                        .scopes("all")
                        .autoApprove(true);
            } @Override
            public void configure(AuthorizationServerSecurityConfigurer oauthServer)
                    throws Exception {
                oauthServer.tokenKeyAccess("permitAll()")
                        .checkTokenAccess("isAuthenticated()")
                        .allowFormAuthenticationForClients();
            }
            

            没关系。

            【讨论】:

            • 添加更多解释为什么这个解决方案是最好的解决方案。仅代码答案无济于事。
            【解决方案6】:

            Spring 允许您使用空密钥定义 OAuth2 客户端。 这些可以被认为是“公共”客户端,或者无法保密的客户端。想想 Javascript 应用程序、移动应用程序......您通常不希望在其中存储客户端机密。

            正如您所指出的,根据 OAuth2 规范,令牌端点可以选择不需要这些公共客户端的密钥。

            所以在 Spring 中,只需定义一个带有空密钥的 OAuth2 客户端,并将其配置为一组有限的授权类型(authorization_code 和 refresh_token)

            Spring Security 实现令牌 url 将接受没有特定 OAuth2 客户端的客户端密钥的令牌交换。

            【讨论】:

            • 我也是这么想的,但我一直收到 401。EnableAuthorizationServer 的文档还说:“但是令牌端点 (/oauth/token) 将使用客户端的 HTTP Basic * 身份验证自动保护凭据”
            • @lilalinux 你知道你的问题是什么吗?我也有同样的情况。
            • @SebastiaanvandenBroek 在客户端定义中尝试 .secret("{noop}")。那应该接受一个空密码。
            • @lilalinux 感谢您的回复,我使用标准密码编码器对空密码进行了类似的成功编码,请在此处查看我的答案。我想这归结为同一件事。
            【解决方案7】:

            使用allowFormAuthenticationForClients() 方法启用使用表单参数而不是基本身份验证对客户端进行身份验证,如下面的代码示例所示。

            class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
            
                @Override
                void configure(AuthorizationServerSecurityConfigurer security) {
                    security
                            .tokenKeyAccess("permitAll()")
                            .checkTokenAccess("isAuthenticated()")
                            .allowFormAuthenticationForClients()
                }
            }
            

            allowFormAuthenticationForClients() 方法触发添加 ClientCredentialsTokenEndpointFilter,它允许通过表单参数进行身份验证。

            【讨论】:

            • 仍然对我不起作用。当我省略 Authorization 标头时,我确实得到了 401 代码。
            • @ECostello 你知道为什么吗?我和你的情况一样。
            • @SebastiaanvandenBroek,这对你来说仍然是个问题吗?如果是,请尝试将秘密设置为空。这意味着任何秘密或非秘密都将被接受。
            • 我的情况也很相似。但是在我的情况下,我有多个客户端,其中一些使用基本身份验证,而其他使用表单参数。每当我添加 allowFormAuthenticationForClients() 时,使用基本身份验证的客户端都无法访问。是否可以同时推广这两种方法?