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.