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

我正在从 SpringBoot1.4.9迁移到 SpringBoot2.0,也迁移到 SpringSecurity5,我正在尝试通过 OAuth2进行身份验证。但我得到了这个错误:

IllegalArgumentException: 没有为 id“ null”映射的 PasswordEncoder

春季安全5的文档中,我得知 更改密码的存储格式。

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

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

然而,它给了我以下错误:

编码密码看起来不像 BCrypt

因此,我根据 春季安全5文档将编码器更新为:

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

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

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

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

IllegalArgumentException: 没有为 id“ null”映射的 PasswordEncoder

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

这里有一个类似于我的问题,但没有答案:

注意: 我已经在数据库中存储了加密的密码,所以不需要在 UserDetailsService中再次编码。

春季安全5文档中,他们建议您可以使用以下方法处理这个异常:

SetDefaultPasswordEncoderForMatches (PasswordEncoder)

如果这是修复,那么我应该把它放在哪里?我已经尝试把它放在 PasswordEncoder豆像下面这样,但它没有工作:

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

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

141682 次浏览

When you are configuring the ClientDetailsServiceConfigurer, you have to also apply the new password storage format to the client secret.

.secret("{noop}secret")

For anyone facing the same issue and not in need of a secure solution - for testing and debugging mainly - in memory users can still be configured.

This is just for playing around - no real world scenario.

The approach used below is deprecated.

This is where I got it from:


Within your WebSecurityConfigurerAdapter add the following:

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

Here, obviously, passwords are hashed, but still are available in memory.


Of course, you could also use a real PasswordEncoder like BCryptPasswordEncoder and prefix the password with the correct id:

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

Add .password("{noop}password") to Security config file.

For example :

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

Regarding

Encoded password does not look like BCrypt

In my case there was a mismatch in BCryptPasswordEncoder strength used by default constructor (10) as pwd hash was generated with strength 4. So I've set strength explicit.

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

also my Spring Security version is 5.1.6 and it perfectly works with BCryptPasswordEncoder

Whenever Spring stores the password, it puts a prefix of encoder in the encoded passwords like bcrypt, scrypt, pbkdf2 etc. so that when it is time to decode the password, it can use appropriate encoder to decode. if there is no prefix in the encoded password it uses defaultPasswordEncoderForMatches. You can view DelegatingPasswordEncoder.class's matches method to see how it works. so basically we need to set defaultPasswordEncoderForMatches by the following lines.

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

Now, you might also have to provide this encoder with DefaultPasswordEncoderForMatches to your authentication provider also. I did that with below lines in my config classes.

@Bean
@Autowired
public DaoAuthenticationProvider getDaoAuthenticationProvider(@Qualifier("myPasswordEncoder") PasswordEncoder passwordEncoder, UserDetailsService userDetailsServiceJDBC) {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
daoAuthenticationProvider.setUserDetailsService(userDetailsServiceJDBC);
return daoAuthenticationProvider;
}

Don't know if this will help anyone. My working WebSecurityConfigurer and OAuth2Config code as below:

OAuth2Config File:

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


}

Here is the link to the project: springboot-authorization-server-oauth2

You can read in the official Spring Security Documentation that for the DelegatingPasswordEncoder the general format for a password is: {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 examples are:

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

If you are fetching the username and password from the database, you can use below code to add NoOpPassword instance.

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

Where adm is a custom user object for my project which has getPassword() and getUsername() methods.

Also remember, to make a custom User POJO, you'll have to implement UserDetails interface and implements all of it's methods.

Hope this helps.

The java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" error message arises when upgrading from Spring Security 4 to 5. Please refer to this Baeldung article as for the complete explanation and possible solutions.

Spring Boot official documentation has provided a solution for this

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

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


}

adding the below two annotation is fixed that issue
@Configuration @EnableWebSecurity

juste add this bean to an annoted @configuration class.

@Configuration
public class BootConfiguration {


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


}

next go to your WebSecurity configuration class and call the password bean add it to authentication provider dont forgot set in your auth manager bean the authentication provider bean and final set the authentication manager bean in the SecurityFilterChain use the example below

    @EnableWebSecurity
@Configuration
public class DefaultSecurityConfig {
    

@Autowire
private PasswordEncoder passwordEncoder
        

    

@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userManager);
authenticationProvider.setPasswordEncoder(passwordEncoder);
return authenticationProvider;
}


@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.authenticationProvider(authenticationProvider();


return authenticationManagerBuilder.build();
}




@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
          

.anyRequest().authenticated());
http.authenticationManager(authManager(http));
http.exceptionHandling(exceptions->exceptions
.accessDeniedHandler(accessDeniedHandler));
http.formLogin(formLogin-> formLogin.loginPage("/login")
.permitAll());
http.logout(logout-> logout
.permitAll()
return http.build();
}




}