Spring 安全 5:没有为 id "null" 映射的 PasswordEncoder

Spring Security 5 : There is no PasswordEncoder mapped for the id "null"

我正在从 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();
}

但是它给我以下错误:

Encoded password does not look like BCrypt

所以我根据 Spring Security 5 文档将编码器更新为:

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

现在如果我能在数据库中看到密码,它存储为

{bcrypt}a$LoV/3z36G86x6Gn101aekuz3q9d7yfBp3jFn7dzNN/AL5630FyUQ

第一个错误消失了,现在当我尝试进行身份验证时,出现以下错误:

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

为了解决这个问题,我尝试了 Whosebug 中的以下所有问题:

这是一个与我相似但未回答的问题:

注意:我已经将加密密码存储在数据库中,因此无需在 UserDetailsService.

中再次编码

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

DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)

如果这是解决方法,那么我应该把它放在哪里?我试过像下面那样把它放在 PasswordEncoder bean 中,但它没有用:

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

MyWebSecurity class

@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);
    }
}

请指导我解决这个问题。我花了几个小时来解决这个问题,但无法解决。

配置 ClientDetailsServiceConfigurer 时,还必须将新的 password storage format 应用到客户端密码。

.secret("{noop}secret")

对于面临相同问题且不需要安全解决方案的任何人 - 主要用于测试和调试 - 内存用户仍然可以配置。

这只是为了玩玩 - 没有真实世界的场景。

下面使用的方法已弃用。

这是我从哪里得到的:


在您的 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));

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

例如:

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

关于

Encoded password does not look like BCrypt

在我的例子中,默认构造函数 (10) 使用的 BCryptPasswordEncoder 强度不匹配,因为 pwd 哈希是用强度 4 生成的。所以我设置了强度显式。

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

还有我的 Spring 安全版本是 5.1.6,它与 BCryptPasswordEncoder

完美配合

每当Spring存储密码时,它都会在编码后的密码中加上编码器的前缀,如bcrypt、scrypt、pbkdf2等,以便在需要解码密码时,它可以使用合适的编码器来解码。如果编码密码中没有前缀,则使用 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;
    }

不知道这是否对任何人有帮助。我的工作 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);
    }

}

这是项目的 link: springboot-authorization-server-oauth2

您可以阅读 in the official Spring Security Documentation DelegatingPasswordEncoder 密码的一般格式为:{id}encodedPassword

Such that id is an identifier used to look up which PasswordEncoder should be used and encodedPassword is the original encoded password for the selected PasswordEncoder. The id must be at the beginning of the password, start with { and end with }. If the id cannot be found, the id will be null. For example, the following might be a list of passwords encoded using different id. All of the original passwords are "password".

ID 示例是:

{bcrypt}a$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG {noop}password {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc {scrypt}$e0801bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

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

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

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

另请记住,要制作自定义用户 POJO,您必须实现 UserDetails 接口并实现它的所有方法。

希望对您有所帮助。

从 Spring 安全 4 升级到 5 时出现 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" 错误消息。请参阅此 Baeldung article 了解完整的解释和可能的解决方案。

Spring Boot official documentation 已经为此提供了解决方案

The easiest way to resolve the error is to switch to explicitly providing the PasswordEncoder that your passwords are encoded with. The easiest way to resolve it is to figure out how your passwords are currently being stored and explicitly provide the correct PasswordEncoder.

If you are migrating from Spring Security 4.2.x you can revert to the previous behavior by exposing a NoOpPasswordEncoder bean.

Alternatively, you can prefix all of your passwords with the correct id and continue to use DelegatingPasswordEncoder. For example, if you are using BCrypt, you would migrate ... more