Spring Security 5:没有为“null”映射PasswordEncoder。

117

我正在从Spring Boot 1.4.9迁移到Spring Boot 2.0,并升级到Spring Security 5,尝试通过OAuth 2进行身份验证。但是遇到以下错误:

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

根据Spring Security 5的文档,密码存储格式已更改。

在我的当前代码中,我已创建了密码编码器bean:

@Bean
public BCryptPasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

然而我遇到了以下错误:

加密后的密码看起来不像是BCrypt格式

因此,我按照Spring Security 5文档更新了编码器:

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

现在,如果我可以在数据库中看到密码,它会被存储为:

{bcrypt}$2a$10$LoV/3z36G86x6Gn101aekuz3q9d7yfBp3jFn7dzNN/AL5630FyUQ

解决了第一个错误之后,现在我尝试进行身份验证时,遇到以下错误:

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null

为了解决这个问题,我尝试了Stackoverflow上所有以下问题的解答:

以下是类似于我的问题,但没有答案的问题:

注意:我已经将加密密码存储在数据库中,因此不需要在UserDetailsService中再次编码。

Spring security 5文档中,他们建议使用以下方式处理此异常:

DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)

如果这是解决方案,那么我应该把它放在哪里?我已经尝试将其放置在PasswordEncoder bean中,但它没有起作用:

DelegatingPasswordEncoder def = new DelegatingPasswordEncoder(idForEncode, encoders);
def.setDefaultPasswordEncoderForMatches(passwordEncoder);

MyWebSecurity类

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {

        web
                .ignoring()
                .antMatchers(HttpMethod.OPTIONS)
                .antMatchers("/api/user/add");
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

MyOauth2配置

@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;


    @Bean
    public TokenEnhancer tokenEnhancer() {
        return new CustomTokenEnhancer();
    }

    @Bean
    public DefaultAccessTokenConverter accessTokenConverter() {
        return new DefaultAccessTokenConverter();
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints)
            throws Exception {
        endpoints
                .tokenStore(tokenStore())
                .tokenEnhancer(tokenEnhancer())
                .accessTokenConverter(accessTokenConverter())
                .authenticationManager(authenticationManager);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
                .inMemory()
                .withClient("test")
                .scopes("read", "write")
                .authorities(Roles.ADMIN.name(), Roles.USER.name())
                .authorizedGrantTypes("password", "refresh_token")
                .secret("secret")
                .accessTokenValiditySeconds(1800);
    }
}
请指导我解决这个问题。我已经花了几个小时来修复,但是无法解决。

我对问题进行了更详细的描述。当我从Spring Security 4转移到5时,我首先遇到了一个错误,然后通过更改我的密码生成器来解决它,但是它开始给我第二个错误。而且错误消息是不同的。1)编码的密码看起来不像BCrypt,2)java.lang.IllegalArgumentException:没有为ID“null”映射PasswordEncoder。这是我目前遇到的第二个问题。 - Jimmy
3
我认为问题出在客户端(而不是个人用户账户)上。就我个人而言,我已经对用户详细信息进行了编码,但没有对客户端进行编码。现在,OAuth2的用户需要编码客户端密码(以及用户密码)。具体来说,请在ClientDetailsServiceConfigurer上设置 passwordEncoder或在密码前加上{noop}前缀。希望这有助于解决问题。 - KellyM
这个问题通过运行 mvn clean package 命令已经被解决了。可能是缓存的问题。 - Janac Meena
13个回答

135

当您配置ClientDetailsServiceConfigurer时,您还必须将新的密码存储格式应用于客户端秘钥。

.secret("{noop}secret")

请参阅Spring Security 5.0.0.RC1发布说明中的密码匹配密码存储格式,了解更多信息。 - olivmir
1
还可以使用默认密码编码器添加 secret(passwordEncoder.encode("secret")) - Troy Young
3
如果我不使用秘密,会发生什么? - f.khantsis
4
也可以这样写:auth.inMemoryAuthentication()。withUser("admin").roles("ADMIN").password("{noop}password");该代码段的意思是在内存中认证用户,用户名为"admin",角色为"ADMIN",密码为"{noop}password"。 - bzhu
@bzhu 如果我不想硬编码将来可能注册的用户怎么办? - Sebi

53

.password("{noop}password")添加到安全配置文件中。

例如:

auth.inMemoryAuthentication()
        .withUser("admin").roles("ADMIN").password("{noop}password");

4
我只想使用纯文本密码!就像不对其进行任何操作一样!我这么认为!;) - Loki

22

如果有人遇到同样的问题,不需要安全解决方案 - 主要用于测试和调试 - 可以配置内存用户。

这只是用来玩耍的 - 没有真实场景。

下面使用的方法已经被弃用了。

这里是我得到它的地方:


在您的WebSecurityConfigurerAdapter中添加以下内容:

@SuppressWarnings("deprecation")
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}

显然,在这里密码被哈希了,但仍然可以在内存中使用。


当然,你也可以使用真正的PasswordEncoder,如BCryptPasswordEncoder,并在密码前加上正确的ID:

// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

3
你好,实际上我们不能使用NoOpPasswordEncoder,因为它在新的spring-security版本中已经被弃用。根据spring security文档(https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-encoding),即使我们使用NoOpPasswordEncoder,我们也必须将{noop}作为密码和密钥的标识添加。我认为这并不能解决我的问题。所有解决方案的主要问题是它们没有提到你需要为你的密钥添加一个标识符。 - Jimmy
1
是的,它已经被弃用了。我的源代码也是这么说的。如果你只使用 NoOpPasswordEncoder 而不是 BCryptPasswordEncoder,它可以工作。我使用的是 Spring Boot 2.0.1.RELEASE 版本。 - rocksteady
但是,正如我在答案中提到的那样,这根本不是生产场景。 - rocksteady
我不明白为什么我们即使是为了测试目的,也要使用一个已经过时的类? - Jimmy
好的,你是完全正确的。我添加了这个答案是为了那些只是玩弄和开始使用过时教程深入此事的人。这就是为什么我还提到了BCryptPasswordEncoder。我更新了我的答案。 - rocksteady
@Jimmy,"这是一个过时的实现方式,使用它被认为是不安全的"。 - undefined

5

在从Spring Security 4升级到5时,会出现java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"错误消息。请参考Baeldung文章以获取完整的解释和可能的解决方案。


4

不知道这是否对任何人有所帮助。以下是我的WebSecurityConfigurer和OAuth2Config代码:

OAuth2Config文件:

package com.crown.AuthenticationServer.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;

@Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
            .withClient("crown")
            .secret("{noop}thisissecret")
            .authorizedGrantTypes("refresh_token", "password", "client_credentials")
            .scopes("webclient", "mobileclient");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
            .authenticationManager(authenticationManager)
            .userDetailsService(userDetailsService);
    }
}

WebSecurityConfigurer:
package com.crown.AuthenticationServer.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;


@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {

        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

        final User.UserBuilder userBuilder = User.builder().passwordEncoder(encoder::encode);
        UserDetails user = userBuilder
            .username("john.carnell")
            .password("password")
            .roles("USER")
            .build();

        UserDetails admin = userBuilder
            .username("william.woodward")
            .password("password")
            .roles("USER","ADMIN")
            .build();

        return new InMemoryUserDetailsManager(user, admin);
    }

}

这是该项目的链接: springboot-authorization-server-oauth2


4

每当Spring存储密码时,它会在像bcrypt、scrypt、pbkdf2等编码密码的前缀中添加encoder的前缀,以便在解码密码时使用适当的编码器进行解码。如果编码密码中没有前缀,则使用defaultPasswordEncoderForMatches。您可以查看DelegatingPasswordEncoder.class的matches方法以了解其工作原理。因此,基本上我们需要通过以下行设置defaultPasswordEncoderForMatches。

@Bean(name="myPasswordEncoder")
public PasswordEncoder getPasswordEncoder() {
        DelegatingPasswordEncoder delPasswordEncoder=  (DelegatingPasswordEncoder)PasswordEncoderFactories.createDelegatingPasswordEncoder();
        BCryptPasswordEncoder bcryptPasswordEncoder =new BCryptPasswordEncoder();
    delPasswordEncoder.setDefaultPasswordEncoderForMatches(bcryptPasswordEncoder);
    return delPasswordEncoder;      
}

现在,您可能还需要向身份验证提供程序提供DefaultPasswordEncoderForMatches编码器。我在我的配置类中使用以下行执行了此操作。
@Bean
    @Autowired  
    public DaoAuthenticationProvider getDaoAuthenticationProvider(@Qualifier("myPasswordEncoder") PasswordEncoder passwordEncoder, UserDetailsService userDetailsServiceJDBC) {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        daoAuthenticationProvider.setUserDetailsService(userDetailsServiceJDBC);
        return daoAuthenticationProvider;
    }

3
您可以在官方的Spring Security文档中阅读DelegatingPasswordEncoder,对于DelegatingPasswordEncoder,密码的一般格式为:{id}encodedPassword

其中,id是用于查找应使用哪个PasswordEncoder的标识符,encodedPassword是所选PasswordEncoder的原始编码密码。id必须位于密码的开头,以{开始,以}结束。如果找不到id,则id将为null。例如,以下可能是使用不同id编码的密码列表。所有原始密码都是“password”。

id示例:

bcrypt:$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 密码:{noop}password pbkdf2:5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc scrypt:$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= sha256:97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

这个问题是因为我们需要在客户端密钥中也加入加密类型。目前,密码已经将加密类型附加到加密密码上了。 - Jimmy
正确的链接到文档 - Pierre C

3

如果您正在从数据库中获取用户名和密码,您可以使用以下代码添加NoOpPassword实例。

protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   auth.userDetailsService(adm).passwordEncoder(NoOpPasswordEncoder.getInstance());
}

其中adm是我项目中的自定义用户对象,具有getPassword()和getUsername()方法。

还要记住,要创建一个自定义的User POJO,您必须实现UserDetails接口并实现其所有方法。

希望这可以帮到您。


1

用于XML配置。

<authentication-manager>
    <authentication-provider>
        <user-service>
            <user name="admin" password="{noop}1234" authorities="ROLE_ADMIN"/>
        </user-service>
    </authentication-provider>
</authentication-manager>

0
@Configuration
@EnableWebSecurity
public class AppSecurityConfig extends WebSecurityConfigurerAdapter{

    @Autowired
    private UserDetailsService userDetailsService;
    
    
    @Bean
    public AuthenticationProvider authProvider() {
        
        DaoAuthenticationProvider provider =  new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(NoOpPasswordEncoder.getInstance());
        
        return provider;
    }

}

添加以下两个注释可以解决该问题:
@Configuration @EnableWebSecurity

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接