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:
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:
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:
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:
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.