Spring Security: user UserDetails source code analysis

The object of Spring Security authentication and authorization is the user. The user mentioned here can be defined in the configuration file, stored in the database table, or automatically created by Spring Security (Spring Security will automatically create the user when there is no user or user source related configuration), Spring Security uses the UserDetails interface to abstract users.

Applied POM xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.kaven</groupId>
    <artifactId>security</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

UserDetails

UserDetails interface source code (user abstraction):

package org.springframework.security.core.userdetails;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

import java.io.Serializable;
import java.util.Collection;

/**
 * Provide user information
 */
public interface UserDetails extends Serializable {
	/**
	 * Returns the permissions granted to the user
	 * Cannot return null
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * Return password
	 */
	String getPassword();

	/**
	 * Returns the user name used to authenticate the user
	 * Cannot return null
	 */
	String getUsername();

	/**
	 * Indicates whether the user account has expired
	 * Expired users cannot be authenticated
	 */
	boolean isAccountNonExpired();

	/**
	 * Indicates whether the user account is locked
	 * The locked user cannot be authenticated
	 */
	boolean isAccountNonLocked();

	/**
	 * Indicates whether the user's credentials (password) have expired
	 * Expired credentials prevent authentication
	 */
	boolean isCredentialsNonExpired();

	/**
	 * Indicates whether the user account is disabled
	 * Unable to authenticate disabled users
	 */
	boolean isEnabled();
}

The inheritance and implementation relationship of UserDetails interface is shown in the following figure:

MutableUserDetails

MutableUserDetails interface source code (abstract of variable user, inheriting UserDetails interface):

package org.springframework.security.provisioning;
import org.springframework.security.core.userdetails.UserDetails;

interface MutableUserDetails extends UserDetails {
    // Set password
	void setPassword(String password);
}

MutableUser

MutableUser class source code (implementation of variable user and MutableUserDetails interface):

package org.springframework.security.provisioning;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.userdetails.UserDetails;

class MutableUser implements MutableUserDetails {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // password
	private String password;
	// Delegate the method implementation to another UserDetails instance
	private final UserDetails delegate;

	MutableUser(UserDetails user) {
		this.delegate = user;
		this.password = user.getPassword();
	}

	public String getPassword() {
		return password;
	}

    // Set password
	public void setPassword(String password) {
		this.password = password;
	}
	
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return delegate.getAuthorities();
	}

	public String getUsername() {
		return delegate.getUsername();
	}

	public boolean isAccountNonExpired() {
		return delegate.isAccountNonExpired();
	}

	public boolean isAccountNonLocked() {
		return delegate.isAccountNonLocked();
	}

	public boolean isCredentialsNonExpired() {
		return delegate.isCredentialsNonExpired();
	}

	public boolean isEnabled() {
		return delegate.isEnabled();
	}
}

MutableUser class only provides password acquisition and setting, and the implementation of other methods is delegated to another UserDetails instance.

User

User class source code (implements UserDetails and CredentialsContainer interfaces, and deletes some template code)

package org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Function;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert;

public class User implements UserDetails, CredentialsContainer {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private static final Log logger = LogFactory.getLog(User.class);

    // password
	private String password;
	// user name
	private final String username;
	// Authorization collection for users
	private final Set<GrantedAuthority> authorities;
	// Whether the user account has expired
	private final boolean accountNonExpired;
    // Is the user account locked
	private final boolean accountNonLocked;
	// Whether the user's credentials have expired
	private final boolean credentialsNonExpired;
	// Is the user's account disabled
	private final boolean enabled;
	
	/**
	 * Call the more complex constructor and set all Boolean parameters to true
	 */
	public User(String username, String password,
			Collection<? extends GrantedAuthority> authorities) {
		this(username, password, true, true, true, true, authorities);
	}

	public User(String username, String password, boolean enabled,
			boolean accountNonExpired, boolean credentialsNonExpired,
			boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {

		if (((username == null) || "".equals(username)) || (password == null)) {
			throw new IllegalArgumentException(
					"Cannot pass null or empty values to constructor");
		}

		this.username = username;
		this.password = password;
		this.enabled = enabled;
		this.accountNonExpired = accountNonExpired;
		this.credentialsNonExpired = credentialsNonExpired;
		this.accountNonLocked = accountNonLocked;
		this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
	}
	
    // Delete credentials, which is defined by the CredentialsContainer interface
	public void eraseCredentials() {
		password = null;
	}

	private static SortedSet<GrantedAuthority> sortAuthorities(
			Collection<? extends GrantedAuthority> authorities) {
		Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
		// Ensure that the iterative order of the user's authorization set is predictable
		SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet<>(
				new AuthorityComparator());

		for (GrantedAuthority grantedAuthority : authorities) {
			Assert.notNull(grantedAuthority,
					"GrantedAuthority list cannot contain any null elements");
			sortedAuthorities.add(grantedAuthority);
		}

		return sortedAuthorities;
	}

	private static class AuthorityComparator implements Comparator<GrantedAuthority>,
			Serializable {
		private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

		public int compare(GrantedAuthority g1, GrantedAuthority g2) {
			// Neither should be empty, because each entry is checked for emptiness before it is added to the collection
			// If the permission is blank, it is a custom permission and should take precedence over other permissions
			if (g2.getAuthority() == null) {
				return -1;
			}

			if (g1.getAuthority() == null) {
				return 1;
			}

			return g1.getAuthority().compareTo(g2.getAuthority());
		}
	}

	/**
	 * Returns true if the supplied object is a User instance with the same username value
	 * If objects have the same username value and represent the same principal, they are equal
	 */
	@Override
	public boolean equals(Object rhs) {
		if (rhs instanceof User) {
			return username.equals(((User) rhs).username);
		}
		return false;
	}

	/**
	 * Returns the hash code of username
	 */
	@Override
	public int hashCode() {
		return username.hashCode();
	}


	/**
	 * Creates a UserBuilder with the specified user name
	 */
	public static UserBuilder withUsername(String username) {
		return builder().username(username);
	}

	/**
	 * Create UserBuilder
	 */
	public static UserBuilder builder() {
		return new UserBuilder();
	}

    /**
	 * Create a UserBuilder based on UserDetails
	 */
    public static UserBuilder withUserDetails(UserDetails userDetails) {
		return withUsername(userDetails.getUsername())
			.password(userDetails.getPassword())
			.accountExpired(!userDetails.isAccountNonExpired())
			.accountLocked(!userDetails.isAccountNonLocked())
			.authorities(userDetails.getAuthorities())
			.credentialsExpired(!userDetails.isCredentialsNonExpired())
			.disabled(!userDetails.isEnabled());
	}

	/**
	 * Build users to add
	 * As a minimum, user name, password and permissions should be provided
	 * The remaining properties have reasonable default values
	 */
	public static class UserBuilder {
		private String username;
		private String password;
		private List<GrantedAuthority> authorities;
		private boolean accountExpired;
		private boolean accountLocked;
		private boolean credentialsExpired;
		private boolean disabled;
		private Function<String, String> passwordEncoder = password -> password;

		/**
		 * Fill role
		 * This method calls authorities(String...) Shortcut to, but "ROLE_" is automatically added for each entry prefix
		 * This means the following: Builder roles("USER","ADMIN")
		 * Equivalent to builder authorities("ROLE_USER","ROLE_ADMIN")
		 * This property is required, but you can also use authorities(String...) fill
		 */
		public UserBuilder roles(String... roles) {
			List<GrantedAuthority> authorities = new ArrayList<>(
					roles.length);
			for (String role : roles) {
				Assert.isTrue(!role.startsWith("ROLE_"), () -> role
						+ " cannot start with ROLE_ (it is automatically added)");
				authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
			}
			return authorities(authorities);
		}

        public UserBuilder authorities(GrantedAuthority... authorities) {
			return authorities(Arrays.asList(authorities));
		}

		public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) {
			this.authorities = new ArrayList<>(authorities);
			return this;
		}
		
		public UserBuilder authorities(String... authorities) {
			return authorities(AuthorityUtils.createAuthorityList(authorities));
		}

		public UserDetails build() {
			String encodedPassword = this.passwordEncoder.apply(password);
			return new User(username, encodedPassword, !disabled, !accountExpired,
					!credentialsExpired, !accountLocked, authorities);
		}
	}
}

The profile specifies the user

The configuration file is as follows:

spring:
  security:
    user:
      name: kaven
      password: itkaven
      roles:
        - USER
        - ADMIN

When Debug starts the application, the constructor of User class will be called, as shown in the following figure:

The reason why the password is {noop}itkaven instead of itkaven (itkaven is still required for authentication) is that the password has been modified (prefixed with {noop}) in the getOrDeducePassword method of UserDetailsServiceAutoConfiguration class before creating the User instance.

    private String getOrDeducePassword(User user, PasswordEncoder encoder) {
        String password = user.getPassword();
        if (user.isPasswordGenerated()) {
            logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
        }

        return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password;
    }

Moreover, the permissions granted to the user are consistent with the configuration file, except that the name of the ROLE has been modified (with the ROLE_prefix). It is obvious that the roles method of the UserBuilder class has been called (called in the inMemoryUserDetailsManager method of the UserDetailsServiceAutoConfiguration class).

    public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) {
        User user = properties.getUser();
        List<String> roles = user.getRoles();
        return new InMemoryUserDetailsManager(new UserDetails[]{org.springframework.security.core.userdetails.User.withUsername(user.getName()).password(this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build()});
    }

Automatically create users

Spring Security will automatically create a user when there is no user or user source related configuration. The user name is user and the password is automatically generated (it will also be prefixed with {noop}).

Configure user source

Add database dependency:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

Add database configuration:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ITkaven@666.com
    url: jdbc:mysql://192.168.31.150:3306/user?useSSL=false&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // Configure custom user services
        // Configure password encoder (a password encoder that does nothing for testing)
        auth.userDetailsService(new UserDetailsServiceImpl()).passwordEncoder(NoOpPasswordEncoder.getInstance());
    }

    // Custom user services
    public static class UserDetailsServiceImpl implements UserDetailsService {

        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // Simulate finding users in the database
            // Suppose the USER exists, the password is itkaven, and the role list is USER and ADMIN
            UserDetails userDetails = User.withUsername(username).password("itkaven").roles("USER", "ADMIN").build();
            return userDetails;
        }
    }
}

When the user source is configured, Spring Security will not create users at startup (the first two methods will create users at startup), because Spring Security cannot create all users in the user source at startup (hungry man), which is unrealistic, so it needs to customize the user service, The user service is to load the specified user from the user source at an appropriate time (for example, during login authentication) (through the user name, the user source may not have the user). The content of UserDetailsService will be described in detail later. That's all for the user UserDetails source code analysis. If the blogger has something wrong or you have different opinions, you are welcome to comment and supplement.

Keywords: Java Spring Back-end Spring Security

Added by paggard on Wed, 05 Jan 2022 10:09:24 +0200