A variety of encryption schemes coexist in Spring Security, which is a powerful tool for the integration of old and dilapidated systems!

Posted by FlipinMonkeyPie on Thu, 18 Jun 2020 04:58:42 +0200

About the problem of password encryption, brother song has talked with you before. For reference:

In this article, song introduced two kinds of encryption schemes, but both of them are used independently! Can multiple password encryption schemes exist in the same project at the same time? The answer is yes!

Today, song Ge will come to talk with you about how to make a variety of different password encryption schemes coexist in Spring Security.

This is the 31st article in the Spring Security series. Reading the previous articles will help you better understand this article:

  1. Dig a big hole and start Spring Security!
  2. Song Ge takes you to Spring Security by hand. Don't ask how to decrypt the password again
  3. How to customize form login in Spring Security
  4. Spring Security does front and back separation, let's not do page Jump! All JSON interactions
  5. The authorization operation in Spring Security is so simple
  6. How does Spring Security store user data in the database?
  7. Spring Security+Spring Data Jpa join hands, security management is only simpler!
  8. Spring Boot + Spring Security for automatic login
  9. How to control security risks when Spring Boot automatically logs in?
  10. In micro service projects, what is the advantage of Spring Security over Shiro?
  11. Two ways to customize the authentication logic of spring security (advanced play method)
  12. How to quickly view the IP address and other information of the login user in Spring Security?
  13. Spring Security automatically kicks out the previous login user, one configuration is done!
  14. Spring Boot + Vue front and back end separation project, how to kick off the logged in users?
  15. Spring Security comes with its own firewall! You don't know how secure your system is!
  16. What is session fixation attack? How to prevent session fixation attack in Spring Boot?
  17. How does Spring Security handle session sharing in a clustered deployment?
  18. SongGe teaches you how to defend against CSRF attacks in SpringBoot! so easy!
  19. To learn thoroughly! Analysis of CSRF defense source code in Spring Security
  20. Two ways of password encryption in Spring Boot!
  21. How to learn Spring Security? Why do we have to learn systematically?
  22. Spring Security two resource release strategies, don't use them wrong!
  23. SongGe teaches you how to start Spring Boot + CAS single sign on
  24. Spring Boot implements the third solution of single sign on!
  25. Spring Boot+CAS single sign on, how to connect the database?
  26. Spring Boot+CAS default login page is too ugly. What should I do?
  27. How to carry Token in request header when Swagger is used to test interface?
  28. Summary of three cross domain scenarios in Spring Boot
  29. How to implement HTTP authentication in Spring Boot?
  30. Four ways of authority control in Spring Security

Why encryption? I won't go over the common encryption algorithm and other problems. You can refer to the previous: Two ways of password encryption in Spring Boot! Let's go straight to today's text.

1.PasswordEncoder

In Spring Security, everything related to password encryption / verification is dominated by PasswordEncoder, which has many implementation classes:

Some of these implementation classes are out of date and some are useless. For us, BCryptPasswordEncoder is the most commonly used one.

PasswordEncoder itself is an interface with only three methods:

public interface PasswordEncoder {
	String encode(CharSequence rawPassword);
	boolean matches(CharSequence rawPassword, String encodedPassword);
	default boolean upgradeEncoding(String encodedPassword) {
		return false;
	}
}
  • The encode method is used to encrypt the password.
  • matches method is used to compare passwords.
  • Upgrade encoding indicates whether the password needs to be re encrypted to make it more secure. The default value is false.

The implementation class of PasswordEncoder implements these methods.

2. Where does passwordencoder work

For our developers, we usually configure an instance of PasswordEncoder in SecurityConfig, similar to the following:

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

The rest is called by the system. Today we are going to unveil the mystery of system call! Let's see how the system is called!

First of all, song Ge mentioned in the previous article that in Spring Security, if the user name / password is used to log in, the password is verified in the DaoAuthenticationProvider. You can refer to: Two ways to customize the authentication logic of spring security (advanced play method).

Let's see how the password in DaoAuthenticationProvider is verified:

protected void additionalAuthenticationChecks(UserDetails userDetails,
		UsernamePasswordAuthenticationToken authentication)
		throws AuthenticationException {
	if (authentication.getCredentials() == null) {
		throw new BadCredentialsException(messages.getMessage(
				"AbstractUserDetailsAuthenticationProvider.badCredentials",
				"Bad credentials"));
	}
	String presentedPassword = authentication.getCredentials().toString();
	if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
		throw new BadCredentialsException(messages.getMessage(
				"AbstractUserDetailsAuthenticationProvider.badCredentials",
				"Bad credentials"));
	}
}

As you can see, password verification is passed passwordEncoder.matches Method.

So where does passwordEncoder come from in DaoAuthenticationProvider? Is it the Bean we configured in SecurityConfig?

Let's take a look at the definition of passwordEncoder in DaoAuthenticationProvider, as follows:

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
	private PasswordEncoder passwordEncoder;
	public DaoAuthenticationProvider() {
		setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
	}
	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
		this.passwordEncoder = passwordEncoder;
		this.userNotFoundEncodedPassword = null;
	}

	protected PasswordEncoder getPasswordEncoder() {
		return passwordEncoder;
	}
}

As you can see from this code, PasswordEncoder was specified when DaoAuthenticationProvider was created. It seems that the Bean we configured at the beginning is not used? It's not! When the DaoAuthenticationProvider is created, a default PasswordEncoder will be developed. If we do not configure any PasswordEncoder, this default PasswordEncoder will be used. If we customize the PasswordEncoder instance, then our customized PasswordEncoder instance will be used!

Where do you know?

Let's take a look at how the DaoAuthenticationProvider is initialized.

The initialization of DaoAuthenticationProvider is completed in the initializeuserdetailsmanagerconfigurer ා configure method. Let's take a look at the definition of this method:

public void configure(AuthenticationManagerBuilder auth) throws Exception {
	if (auth.isConfigured()) {
		return;
	}
	UserDetailsService userDetailsService = getBeanOrNull(
			UserDetailsService.class);
	if (userDetailsService == null) {
		return;
	}
	PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
	UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
	DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
	provider.setUserDetailsService(userDetailsService);
	if (passwordEncoder != null) {
		provider.setPasswordEncoder(passwordEncoder);
	}
	if (passwordManager != null) {
		provider.setUserDetailsPasswordService(passwordManager);
	}
	provider.afterPropertiesSet();
	auth.authenticationProvider(provider);
}

From this code, we can see:

  1. First, call the getbeanor null method to get a PasswordEncoder instance. The getbeanor null method is actually to find objects in the Spring container.
  2. Next, you can directly new a DaoAuthenticationProvider object. As you know, in the process of new, the default PasswordEncoder in DaoAuthenticationProvider has been created.
  3. If the PasswordEncoder instance is obtained from the Spring container at the beginning, it will be assigned to the DaoAuthenticationProvider instance. Otherwise, it will be the PasswordEncoder created by DaoAuthenticationProvider itself by default.

So far, the truth is clear. The PasswordEncoder instance we configured is indeed used.

3. What is the default?

At the same time, you can see that if we do not make any configuration, the default PasswordEncoder will also be provided. What is the default PasswordEncoder? From this point of view:

public DaoAuthenticationProvider() {
	setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}

continue:

public class PasswordEncoderFactories {
	public static PasswordEncoder createDelegatingPasswordEncoder() {
		String encodingId = "bcrypt";
		Map<string, passwordencoder> encoders = new HashMap&lt;&gt;();
		encoders.put(encodingId, new BCryptPasswordEncoder());
		encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
		encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
		encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
		encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
		encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
		encoders.put("scrypt", new SCryptPasswordEncoder());
		encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
		encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
		encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
		encoders.put("argon2", new Argon2PasswordEncoder());

		return new DelegatingPasswordEncoder(encodingId, encoders);
	}

	private PasswordEncoderFactories() {}
}

You can see:

  1. In passwordencoderfactors, we first build an encoder, then give a name to all the coding methods, and then make the name key, and the coding method value, and store them in the encoders.
  2. Finally, a DelegatingPasswordEncoder instance is returned. At the same time, the default encodingId is bcrypt and the encoders instance. DelegatingPasswordEncoder sees that the name should be a proxy object.

Let's look at the definition of DelegatingPasswordEncoder:

public class DelegatingPasswordEncoder implements PasswordEncoder {
	private static final String PREFIX = "{";
	private static final String SUFFIX = "}";
	private final String idForEncode;
	private final PasswordEncoder passwordEncoderForEncode;
	private final Map<string, passwordencoder> idToPasswordEncoder;
	private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
	public DelegatingPasswordEncoder(String idForEncode,
		Map<string, passwordencoder> idToPasswordEncoder) {
		if (idForEncode == null) {
			throw new IllegalArgumentException("idForEncode cannot be null");
		}
		if (!idToPasswordEncoder.containsKey(idForEncode)) {
			throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
		}
		for (String id : idToPasswordEncoder.keySet()) {
			if (id == null) {
				continue;
			}
			if (id.contains(PREFIX)) {
				throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);
			}
			if (id.contains(SUFFIX)) {
				throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);
			}
		}
		this.idForEncode = idForEncode;
		this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
		this.idToPasswordEncoder = new HashMap&lt;&gt;(idToPasswordEncoder);
	}
	public void setDefaultPasswordEncoderForMatches(
		PasswordEncoder defaultPasswordEncoderForMatches) {
		if (defaultPasswordEncoderForMatches == null) {
			throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");
		}
		this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;
	}

	@Override
	public String encode(CharSequence rawPassword) {
		return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
	}

	@Override
	public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
		if (rawPassword == null &amp;&amp; prefixEncodedPassword == null) {
			return true;
		}
		String id = extractId(prefixEncodedPassword);
		PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
		if (delegate == null) {
			return this.defaultPasswordEncoderForMatches
				.matches(rawPassword, prefixEncodedPassword);
		}
		String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
		return delegate.matches(rawPassword, encodedPassword);
	}

	private String extractId(String prefixEncodedPassword) {
		if (prefixEncodedPassword == null) {
			return null;
		}
		int start = prefixEncodedPassword.indexOf(PREFIX);
		if (start != 0) {
			return null;
		}
		int end = prefixEncodedPassword.indexOf(SUFFIX, start);
		if (end &lt; 0) {
			return null;
		}
		return prefixEncodedPassword.substring(start + 1, end);
	}

	@Override
	public boolean upgradeEncoding(String prefixEncodedPassword) {
		String id = extractId(prefixEncodedPassword);
		if (!this.idForEncode.equalsIgnoreCase(id)) {
			return true;
		}
		else {
			String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
			return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);
		}
	}

	private String extractEncodedPassword(String prefixEncodedPassword) {
		int start = prefixEncodedPassword.indexOf(SUFFIX);
		return prefixEncodedPassword.substring(start + 1);
	}
	private class UnmappedIdPasswordEncoder implements PasswordEncoder {

		@Override
		public String encode(CharSequence rawPassword) {
			throw new UnsupportedOperationException("encode is not supported");
		}

		@Override
		public boolean matches(CharSequence rawPassword,
			String prefixEncodedPassword) {
			String id = extractId(prefixEncodedPassword);
			throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
		}
	}
}

This code is relatively long. Let me explain one by one:

  1. DelegatingPasswordEncoder also implements the PasswordEncoder interface, so there are two core methods in it: encode method is used to code the password, and matches method is used to verify the password.
  2. In the construction method of DelegatingPasswordEncoder, the default encoder is obtained and assigned to passwordEncoderForEncode through the two incoming parameters encodingId and encoders. The default encoder is actually BCryptPasswordEncoder.
  3. In the encode method, the password is encoded, but the encoding method is prefixed with {encoder name}. For example, if you use BCryptPasswordEncoder to encode, the generated password is similar to {bcrypt}.a$oE39aG10kB/rFu2vQeCJTu/V/v4n6DRR0f8WyXRiAYvBpmadoOBE. What's the use of this? After each password is encrypted, a prefix will be added, so that when you see the prefix, you know which encoder is used to generate the ciphertext.
  4. Finally, the logic of the matches method is very clear. First, extract the prefix from the ciphertext, then find the corresponding PasswordEncoder according to the prefix, and then call the matches method of the PasswordEncoder for password comparison.
  5. If the corresponding PasswordEncoder cannot be found according to the extracted prefix, the unmappedidpasswordencoder ා matches method will be called for password comparison. In fact, this method does not perform password comparison, but directly throws an exception.

OK, so far, I believe you all understand the working principle of DelegatingPasswordEncoder.

If we want to use multiple password encryption schemes at the same time, it seems that DelegatingPasswordEncoder can be used, and DelegatingPasswordEncoder does not need to be configured by default.

4. Experience

Next, let's experience the usage of DelegatingPasswordEncoder.

First, we will generate three passwords as test passwords:

@Test
void contextLoads() {
    Map<string, passwordencoder> encoders = new HashMap&lt;&gt;();
    encoders.put("bcrypt", new BCryptPasswordEncoder());
    encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
    encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
    DelegatingPasswordEncoder encoder1 = new DelegatingPasswordEncoder("bcrypt", encoders);
    DelegatingPasswordEncoder encoder2 = new DelegatingPasswordEncoder("MD5", encoders);
    DelegatingPasswordEncoder encoder3 = new DelegatingPasswordEncoder("noop", encoders);
    String e1 = encoder1.encode("123");
    String e2 = encoder2.encode("123");
    String e3 = encoder3.encode("123");
    System.out.println("e1 = " + e1);
    System.out.println("e2 = " + e2);
    System.out.println("e3 = " + e3);
}

The results are as follows:

e1 = {bcrypt}$2a$10$Sb1gAUH4wwazfNiqflKZve4Ubh.spJcxgHG8Cp29DeGya5zsHENqi
e2 = {MD5}{Wucj/L8wMTMzFi3oBKWsETNeXbMFaHZW9vCK9mahMHc=}4d43db282b36d7f0421498fdc693f2a2
e3 = {noop}123

Next, we copy these three passwords to SecurityConfig:

@Configuration("aaa")
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    @Bean
    protected UserDetailsService userDetailsService() {

        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("javaboy").password("{bcrypt}$2a$10$Sb1gAUH4wwazfNiqflKZve4Ubh.spJcxgHG8Cp29DeGya5zsHENqi").roles("admin").build());
        manager.createUser(User.withUsername("sang").password("{noop}123").roles("admin").build());
        manager.createUser(User.withUsername("A little rain in Jiangnan").password("{MD5}{Wucj/L8wMTMzFi3oBKWsETNeXbMFaHZW9vCK9mahMHc=}4d43db282b36d7f0421498fdc693f2a2").roles("user").build());
        return manager;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/user/**").hasRole("user")
                ...
    }
}

Here three users use three different ways to encrypt passwords.

After the configuration is completed, restart the project and log in using javaboy/123, sang/123 and Jiangnan yidianyu / 123 respectively. It is found that the login is successful.

5. What is the significance?

Why do we need it? Want to have multiple password encryption schemes at the same time in the project? In fact, this is mainly for the transformation of old projects. Once the password encryption method is determined, it can't be changed (you can't let users register again anyway), but we want to use the latest framework to do password encryption, so undoubtedly, DelegatingPasswordEncoder is the best choice.

Well, this is the problem of various password encryption schemes shared with my friends today. Please remember to watch and encourage song Ge to use them

Topics: Programming Spring Session Database encoding