03 Authentication System How to Deeply Understand the Spring Security Authentication Mechanism

03 Authentication System How to Deeply Understand the Spring Security Authentication Mechanism #

In the previous lesson, we introduced the system approach to implementing user authentication in Spring Security. It is evident that the whole implementation process is relatively simple, as developers only need to use some configuration methods to complete complex processing logic. This simplicity is due to the refinement and abstraction of the user authentication process in Spring Security. Today, we will discuss this topic and further explore the users and authentication objects in Spring Security, as well as how to customize user authentication schemes based on these objects.

Users and Authentication in Spring Security #

The authentication process in Spring Security is made up of a group of core objects, which can be roughly divided into two categories: user objects and authentication objects. Let’s take a closer look at each category.

User Objects in Spring Security #

User objects in Spring Security are used to describe users and manage user information. They involve four core objects: UserDetails, GrantedAuthority, UserDetailsService, and UserDetailsManager.

  • UserDetails: Describes users in Spring Security.
  • GrantedAuthority: Defines user’s operation permissions.
  • UserDetailsService: Defines the querying operation for UserDetails.
  • UserDetailsManager: Extends UserDetailsService and adds functions like creating users and changing user passwords.

The association between these four objects is shown in the following diagram. It is evident that a user described by the UserDetails object should have one or more GrantedAuthority objects that they can perform:

Drawing 0.png

Four core user objects in Spring Security

Let’s start with the UserDetails interface, which carries detailed user information:

public interface UserDetails extends Serializable {
    
    // Get the authorities of this user
    Collection<? extends GrantedAuthority> getAuthorities();
    
    // Get the password
    String getPassword();
    
    // Get the username
    String getUsername();
    
    // Check if the account has expired
    boolean isAccountNonExpired();
    
    // Check if the account is locked
    boolean isAccountNonLocked();
    
    // Check if the credentials of the account have expired
    boolean isCredentialsNonExpired();
    
    // Check if the user is enabled
    boolean isEnabled();
}

Through UserDetails, we can obtain basic user information and check their current status. At the same time, we can see that UserDetails contains a set of GrantedAuthority objects. The GrantedAuthority specifies a method to obtain authority information:

public interface GrantedAuthority extends Serializable {
    
    // Get the authority
    String getAuthority();
}

UserDetails has a sub-interface called MutableUserDetails, from its name, it’s not difficult to see that the latter is a mutable UserDetails, and the mutable content is the password. The definition of the MutableUserDetails interface is as follows:

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

If we want to create a UserDetails object in our application, we can use the following method chaining syntax:

UserDetails user = User.withUsername("jianxiang")
  .password("123456")
  .authorities("read", "write")
  .accountExpired(false)
  .disabled(true)
  .build();

Spring Security also provides a UserBuilder object specifically for constructing UserDetails. The usage is similar:

User.UserBuilder builder = User.withUsername("jianxiang");

UserDetails user = builder
  .password("12345")
  .authorities("read", "write")
  .accountExpired(false)
  .disabled(true)
  .build();

In Spring Security, there is a UserDetailsService specifically for UserDetails management. The interface is defined as follows:

public interface UserDetailsService {
    
    // Get user information based on the username
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetailsManager extends UserDetailsService and provides a set of operations interface specifically for UserDetails, as shown below:

public interface UserDetailsManager extends UserDetailsService {
    
    // Create a user
    void createUser(UserDetails user);
    
    // Update a user
    void updateUser(UserDetails user);
    
    // Delete a user
public interface Authentication {
  
    //安全主体具有的权限
    Collection<? extends GrantedAuthority> getAuthorities();

    //证明主体有效性的凭证
    Object getCredentials();

    //认证请求的明细信息
    Object getDetails();

    //主体的标识信息
    Object getPrincipal();

    //认证是否通过
    boolean isAuthenticated();

    //设置认证结果
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
if (currentUser == null) {

    throw new AccessDeniedException(

        "Can't change password as no Authentication object found in context "

            + "for current user.");

}

String username = currentUser.getName();

if (authenticationManager != null) {

    authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(

        username, oldPassword));

}

else {

    

}

MutableUserDetails user = users.get(username);

if (user == null) {

    throw new IllegalStateException("Current user doesn't exist in database.");

}

user.setPassword(newPassword);

As can be seen, the AuthenticationManager is used here instead of the authenticate() method in the AuthenticationProvider to perform authentication. At the same time, we also note the appearance of the UsernamePasswordAuthenticationToken class, which is a concrete implementation class of the Authentication interface and is used to store the username and password information required for user authentication.

As a summary, we also outline a large number of core classes related to the authentication object in Spring Security, and their relationships are shown in the following figure:

Drawing 2.png

Structure diagram of classes related to authentication objects in Spring Security

Implementing Customized User Authentication Solutions #

Through the previous analysis, we understand that the implementation process of user information storage can actually be customized. What Spring Security does is embed common implementation methods that meet general business scenarios into the framework. If there is a special scenario, developers can completely implement a custom user information storage solution.

Now that we know that the UserDetails interface represents user detailed information and is responsible for various operations on UserDetails, the implementation of a customized user authentication solution mainly involves implementing these two interfaces, UserDetails and UserDetailsService.

Extending UserDetails #

The method of extending UserDetails is to directly implement this interface. For example, we can create a SpringUser class as follows:

public class SpringUser implements UserDetails {

    private static final long serialVersionUID = 1L;

    private Long id;  

    private final String username;

    private final String password;

    private final String phoneNumber;

    // omit getter/setter

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

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}

Obviously, the simplest method is used here to satisfy the implementation requirements of various interfaces in UserDetails. Once we have built such a SpringUser class, we can create a table structure to store the fields defined in the storage class. At the same time, we can also create a custom repository based on Spring Data JPA, as shown below:

public interface SpringUserRepository extends CrudRepository<SpringUser, Long> {

    SpringUser findByUsername(String username);

}

SpringUserRepository extends the CrudRepository interface in Spring Data and provides a method naming query findByUsername.

Extending UserDetailsService #

Next, let’s implement the UserDetailsService interface:

@Service
public class SpringUserDetailsService implements UserDetailsService {

    @Autowired
    private SpringUserRepository repository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SpringUser user = repository.findByUsername(username);
        if (user != null) {
            return user;
        }
        throw new UsernameNotFoundException("SpringUser '" + username + "' not found");
    }

}

We know that the UserDetailsService interface only has one loadUserByUsername method that needs to be implemented. Therefore, based on the findByUsername method of SpringUserRepository, we can query the data from the database based on the username.

Extending AuthenticationProvider #

The process of extending AuthenticationProvider is to provide a custom implementation class of AuthenticationProvider. Here, let’s take the most common username and password authentication as an example in order to clarify the steps required to implement custom authentication. The implementation process of custom AuthenticationProvider is shown in the following diagram:

Drawing 3.png

Diagram of the implementation process of custom AuthenticationProvider

The flow chart in the figure above is not complicated. First, we need to obtain a UserDetails object through UserDetailsService, and then match the password in this object with the password in the authentication request. If they match, the authentication is successful; otherwise, a BadCredentialsException is thrown. The sample code is as follows:

@Component
public class SpringAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        UserDetails user = userDetailsService.loadUserByUsername(username);
        if (passwordEncoder.matches(password, user.getPassword())) {
            return new UsernamePasswordAuthenticationToken(username, password, user.getAuthorities());
        } else {
            throw new BadCredentialsException("The username or password is wrong!");
        }
    }

    @Override
    public boolean supports(Class<?> authenticationType) {
        return authenticationType.equals(UsernamePasswordAuthenticationToken.class);
    }

}

Here, we also use UsernamePasswordAuthenticationToken to pass the username and password, and use a PasswordEncoder object to validate the password.

Integrating Customized Configuration #

Finally, we create a SpringSecurityConfig class that inherits from the WebSecurityConfigurerAdapter configuration class. This time, we will use the custom SpringUserDetailsService to complete the storage and query of user information, and some adjustments need to be made to the original configuration strategy. The complete SpringSecurityConfig class after adjustment is as follows:

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService springUserDetailsService;

    @Autowired
    private AuthenticationProvider springAuthenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(springUserDetailsService)
            .authenticationProvider(springAuthenticationProvider);
    }

}

Here, we inject SpringUserDetailsService and SpringAuthenticationProvider into AuthenticationManagerBuilder, so AuthenticationManagerBuilder will use the custom SpringUserDetailsService to create and manage UserDetails, and use the custom SpringAuthenticationProvider to complete user authentication.

Summary and Outlook #

In this lesson, we analyzed the implementation process behind Spring Security’s user authentication functionality. Our starting point is to analyze the various core classes related to users and authentication, and clarify their interaction processes. On the other hand, we also implemented a customized user authentication solution by extending the UserDetailsService and AuthenticationProvider interfaces.

The summary of this lesson is as follows:

Drawing 4.png

Finally, I leave you with a question: How can you implement a customized user authentication solution based on the username and password using Spring Security? You are welcome to share your thoughts in the comments section.