Where does spring security default user name and password come from and why write UserDetails

Posted by RobM on Sun, 21 Nov 2021 01:32:05 +0100

1. Create a normal Spring boot project

After creating the project, start it directly, and the password will be printed on the console:

Enter in the browser http://localhost:8080 , it will jump to the login page:

The default user name is user, and the password is printed on the console.

This indicates that spring security is effective!

2. Custom user name and password

First, we need to understand why there is a default user name and password, which indicates that there must be an automatic configuration class.

In idea, double-click shift and enter UserDetailsServiceAutoConfiguration. We will find:

@Bean
@ConditionalOnMissingBean(
    type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
@Lazy
// Inmemoryuserdetails manager indicates that the user information is saved in memory at this time, and the next startup password will change, but our focus is not on this
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
                                                             ObjectProvider<PasswordEncoder> passwordEncoder) {
    // There is a User in SecurityProperties. In the next code explanation, let's see what the User is
    SecurityProperties.User user = properties.getUser();
    List<String> roles = user.getRoles();
    return new InMemoryUserDetailsManager(
        User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
        .roles(StringUtils.toStringArray(roles)).build());
}

private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
    String password = user.getPassword();
    if (user.isPasswordGenerated()) {
        // Remember the password printed on the console?
        // Using generated security password: 8e45224d-58c8-4776-ba43-d3808def675e
        logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
    }
    if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
        return password;
    }
    return NOOP_PASSWORD_PREFIX + password;
}

Let's click User to see why the User is sacred:

// It is a static inner class of SecurityProperties
public static class User {

    /**
	 * Default user name.
	*/
    private String name = "user"; // Default information

    /**
	* Password for the default user name.
	*/
    private String password = UUID.randomUUID().toString();  // Default password UUID

   ......

}

At present, we know how the default account and password come from, but how to modify them?

Let's configure it first, because we know that there is a SecurityProperties configuration class, which can certainly be configured through the configuration file

In application.yml:

spring:
  security:
    user:
      name: butcher
      password: bb123

After restarting the project, we found that the console had not printed the password

Revisit http://localhost:8080

The login is successful, but this is not the result we want. We hope that the user name and password are set dynamically, not written in the configuration file.

Return to UserDetailsServiceAutoConfiguration with such an annotation on the class name

@ConditionalOnMissingBean(
    value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class },
    type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
            "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector" })
// Note: if we customize the UserDetailsService.class class and put it into the IOC container, the default configuration will fail. Of course, other classes will also fail

So let's see what UserDetailsService is?

public interface UserDetailsService {

	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

}
// Omit the annotation: the general content of the annotation is to find out the complete user information in the database through username, so the complete user information should be UserDetails

We create an implementation class of UserDetailsService in our service layer.

/**
 * If this interface is implemented, the default user name and password automatic configuration will be invalid!
 */
@Service
public class MyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return null;
    }
}

At this time, our problem arises again. What should the complete user information contain?

Let's open the UserDetails class

// First, it is an interface
// The general meaning of the annotation on the class is:
// For security purposes, Spring Security does not use the implementation directly. They only store user information, which is then encapsulated in the Authentication object.
// This allows non security related user information such as e-mail addresses, phone numbers, etc. to be stored in a convenient location.
public interface UserDetails extends Serializable {

	/**
	 * Returns the permission collection granted to the user, cannot return null
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * User's password
	 */
	String getPassword();

	/**
	 * Returns the user name, and the user name cannot be empty
	 */
	String getUsername();

	/**
	 * Whether the user has expired. If it has not expired, return true
	 */
	boolean isAccountNonExpired();

	/**
	 * Whether the user is locked, and the lock returns true.
	 */
	boolean isAccountNonLocked();

	/**
	 * Whether the user credentials are available, return true
	 */
	boolean isCredentialsNonExpired();

	/**
	 * Whether the user is enabled. If enabled, return true
	 */
	boolean isEnabled();

}

Since it is related to our security, we create the implementation class of UserDetails in our security package.

public class MyUserDetails implements UserDetails {

    // Add some of your own properties to set values externally
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> Authorities;
    // The default value is true. We can change it after it expires. At the same time, it is also convenient for testing
    private boolean isAccountNonExpired = true;
    private boolean isAccountNonLocked = true;
    private boolean isCredentialsNonExpired = true;
    private boolean isEnabled = true;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.Authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return this.isAccountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return this.isAccountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return this.isCredentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return this.isEnabled;
    }

	Omitted setter Method, please generate it manually, or use lombok generate
}

Then we can use it in MyUserDetailsService!

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // Suppose we found the MyUserDetails from the database
    MyUserDetails myUserDetails = new MyUserDetails();
    myUserDetails.setUsername("tanxi");
    myUserDetails.setPassword("tx1234");
    return myUserDetails;
}

Delete the user name and password previously configured in our configuration file!

Run the project again!

An exception will be reported: There is no PasswordEncoder mapped for the id "null"

sercurity requires that our password must be encrypted, so we need to encrypt the password.

We need a PasswordEncoder class. Double click shift to find:

public interface PasswordEncoder {

	/**
	 * Encode the original password. Generally, a good coding algorithm uses a hash value of SHA-1 or greater combined with a randomly generated salt of 8 bytes or greater.
	 * CharSequence is a readable character sequence and an interface. Many classes implement this interface, such as String, CharArray, etc
	 */
	String encode(CharSequence rawPassword);

	/**
	 * Verify that the encoded password obtained from the memory matches the submitted original password after encoding. If the passwords match, return true; Returns false if the passwords do not match.
	 * rawPassword Is the password that needs to be matched, and encodedPassword is the password in the database
	 */
	boolean matches(CharSequence rawPassword, String encodedPassword);

	/**
	 * If the encoded password should be encoded again for better security, return true; otherwise, return false. The default implementation always returns false.
	 */
	default boolean upgradeEncoding(String encodedPassword) {
		return false;
	}

}

The above is an interface. We can customize the encryption method and implement an encryption ourselves!

// It is not necessary to add @ Component annotation, just for convenience
@Component
public class MyPasswordEncoder implements PasswordEncoder {

    // This is the salt recommended 8 bytes or larger, which we know from the source code
    final String salt = "butchersoyoung";

    @Override
    public String encode(CharSequence rawPassword) {

        try {
            // Use MD5 encryption provided with JDk
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            // Only when CharSequence is converted to String can we get the byte array, StandardCharsets.UTF_8 is the standard character set
            byte[] bytes = md5.digest((rawPassword.toString() + salt).getBytes(StandardCharsets.UTF_8));

            return new String(bytes,StandardCharsets.UTF_8);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        // rawPassword is the original password we want to verify. Encoded password is the password in our encrypted database
        // This method does not need to be called manually, but by spring security. We can write the rules~

        // There is no too much verification here, just to show that the password can be encrypted by itself and set its own matching rules
        if (rawPassword != null && encodedPassword != null){
            return encode(rawPassword).equals(encodedPassword);
        }else {
            return false;
        }
    }
}

Note: for the implementation class of PasswordEncoder, Spring recommends that we use BCryptPasswordEncoder, which uses a strong hash algorithm, which is much safer than our custom encryption~

Custom encryption is just for our understanding. That's what happened~

Modify it in MyUserDetailsService to solve the problem of unencrypted password we encountered above.

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    MyPasswordEncoder myPasswordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Suppose we found the MyUserDetails from the database
        MyUserDetails myUserDetails = new MyUserDetails();
        myUserDetails.setUsername("tanxi");
        String encodePassword = myPasswordEncoder.encode("tx1234");
        myUserDetails.setPassword(encodePassword);
        return myUserDetails;
    }
}

At this point, you can test successfully!

Topics: Java Spring Spring Boot