18 User Authentication How to Build User Authentication System Based on Spring Security

18 User Authentication - How to Build User Authentication System based on Spring Security #

In Lecture 17, we discussed the security requirements of web applications and introduced Spring Security, a development framework specifically designed for handling security requirements in the Spring family. We also clarified that authentication and authorization are the core functions of a security framework.

In this lecture, we will first discuss topics related to authentication and provide the authentication mechanism and its usage in Spring Security. Since Spring Security is a basic component in daily development, we will also explore the process of implementing data encryption and decryption.

Integrating the Spring Security framework into Spring Boot is very simple. We just need to add the spring-boot-starter-security dependency in the pom.xml file. Unlike the previous development process, which required many configurations to integrate with Spring Security, the following code shows how to do it in a simplified way:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Note that once we add the above dependency to our code project, all HTTP endpoints included in that project will be protected.

For example, in the account-service of the SpringCSS case, we know that there is an AccountController exposing the “accounts/ /{accountId}” endpoint. Now, if we start the account-service and access the above endpoint, we will see the following interface:

Drawing 0.png

Login interface automatically added after adding Spring Security

At the same time, in the console logs when the system starts, we found the following new log message:

Using generated security password: 17bbf7c4-456a-48f5-a12e-a680066c8f80

Here, we can see that Spring Security has automatically generated a password for us. We can use the “user” account and the above password to log in to this interface. You can also try it when you have time.

If we use Postman as a visual HTTP request tool, we can set the authentication type to “Basic Auth” and enter the corresponding username and password to access the HTTP endpoint. The settings interface is shown in the following image:

Drawing 1.png

Using Postman to set authentication information

In fact, after adding the spring-boot-starter-security dependency, Spring Security will create a default account with the username “user”. Obviously, each time the application starts, the password generated by Spring Security will change, so it is not suitable for a formal application method.

If we want to set the login account and password, the simplest way is to use a configuration file. For example, we can add the following configuration in the application.yml file of the account-service:

spring:
    security:
        user:
            name: springcss
            password: springcss_password

After restarting the account-service, we can log in using the above username and password.

Although the configuration file-based user information storage scheme is simple and direct, it is obviously lacking in flexibility. Therefore, Spring Security provides us with multiple schemes for storing and managing user authentication information. Let’s take a look.

Configuring Spring Security #

In Spring Security, the configuration class that initializes user information is the WebSecurityConfigurer interface. This interface is actually an empty interface that extends the more basic SecurityConfigurer interface.

In daily development, we usually don’t need to implement this interface ourselves, but use the WebSecurityConfigurerAdapter class to simplify the usage of this configuration class. For example, we can configure it by inheriting from the WebSecurityConfigurerAdapter class and overriding its configure(AuthenticationManagerBuilder auth) method.

Regarding the WebSecurityConfigurer configuration class, first we need to clarify the content of the configuration. Actually, the user information for initialization is very simple, we just need to specify three pieces of data: username, password, and role.

In the WebSecurityConfigurer class, we can easily implement verification based on memory, LADP, and JDBC by using the AuthenticationManagerBuilder class to create an AuthenticationManager.

Next, we will implement multiple user information storage schemes using the functionality provided by the AuthenticationManagerBuilder.

Using an In-Memory User Information Storage Scheme #

Let’s first see how to use the AuthenticationManagerBuilder to implement an in-memory user information storage scheme.

The implementation method is to call the inMemoryAuthentication() method of AuthenticationManagerBuilder, as shown in the following example:

@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    builder.inMemoryAuthentication()
           .withUser("springcss_user").password("password1")
           .roles("USER")
           .and()
           .withUser("springcss_admin").password("password2")
           .roles("USER", "ADMIN");
}

From the above code, we can see that there are two users in the system: “springcss_user” and “springcss_admin”, with passwords “password1” and “password2” respectively. They represent the roles “USER” and “ADMIN” for ordinary users and administrators.

In the AuthenticationManagerBuilder, the implementation process of the inMemoryAuthentication() method mentioned above is shown in the following code:

public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication()
            throws Exception {
    return apply(new InMemoryUserDetailsManagerConfigurer<>());
}

Here, the InMemoryUserDetailsManagerConfigurer internally uses the InMemoryUserDetailsManager object. By delving into this class, we can access a large number of core objects related to user authentication in Spring Security, as shown in the following diagram:

图片3

Class structure diagram of user authentication-related classes in Spring Security

Firstly, let’s look at the UserDetails interface, which represents user details, as shown in the following code:

public interface UserDetails extends Serializable {
    // Get the user's authorities
    Collection<? extends GrantedAuthority> getAuthorities();
    
    // Get the password
    String getPassword();
    
    // Get the username
    String getUsername();
    
    // Check if the account is expired
    boolean isAccountNonExpired();
    
    // ...
}
```java
    private DataSource dataSource;

    @Autowired
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

在以上代码中,我们使用 @Autowired 注解将 DataSource 对象注入到 dataSource 字段中。然后,我们可以使用该对象来查询用户数据。

接下来,我们将创建一个 JdbcUserDetailsManager 对象并设置它的数据源,如下代码所示:

    JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();
    userDetailsManager.setDataSource(dataSource);

然后,我们可以通过以下代码来创建用户并设置其角色和密码:

    UserDetails user = User.withUsername("springcss_user")
            .passwordEncoder(passwordEncoder::encode)
            .roles("USER")
            .build();
userDetailsManager.createUser(user);

通过以上代码,我们使用 User 对象来创建一个用户,并使用密码编码器将密码进行编码。我们还为该用户设置了一个角色。然后,我们使用 userDetailsManager 对象的 createUser 方法将用户保存到数据库中。

除了创建用户,还可以使用 userDetailsManager 对象来更新用户、删除用户以及更改用户密码。

总结一下,我们可以使用 InMemoryUserDetailsManagerJdbcUserDetailsManager 来存储用户信息,前者将用户信息存储在内存中,后者将用户信息存储在数据库中。根据实际情况选择合适的方案。

DataSource dataSource;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.jdbcAuthentication().dataSource(dataSource)
        .usersByUsernameQuery("select username, password, enabled from Users where username=?")
        .authoritiesByUsernameQuery("select username, authority from UserAuthorities where username=?")
        .passwordEncoder(new BCryptPasswordEncoder());
}

Here, the jdbcAuthentication method of AuthenticationManagerBuilder is used to configure the database authentication method, and internally, the JdbcUserDetailsManager utility class is used.

The class structure of the entire code chain around JdbcUserDetailsManager is very similar to InMemoryUserDetailsManager, and in this class, various SQL statements for user database queries are defined, as well as the specific implementation methods for accessing the database using JdbcTemplate. We will not go into detail here, you can analyze it by comparing it with the class structure diagram of InMemoryUserDetailsManager provided earlier.


Note that when verifying user information through the jdbcAuthentication method in the above method, we must integrate an encryption mechanism by embedding an implementation class of the PasswordEncoder interface using the passwordEncoder method.

In Spring Security, the PasswordEncoder interface represents a password encoder, defined as follows:

public interface PasswordEncoder {
    // Encodes the raw password
    String encode(CharSequence rawPassword);

    // Compares the raw password submitted with the encrypted password stored in the database
    boolean matches(CharSequence rawPassword, String encodedPassword);

    // Returns whether the encrypted password needs to be encrypted again. By default, it returns false.
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

Spring Security provides a large number of implementation classes for the PasswordEncoder interface, as shown in the following figure:

Drawing 3.png

Implementation classes of PasswordEncoder in Spring Security

In the above figure, commonly used algorithms include the StandardPasswordEncoder for the SHA-256 algorithm, the BCryptPasswordEncoder for the bcrypt strong hash algorithm, etc. In the actual case, we use BCryptPasswordEncoder, and its encode method is shown in the following code:

public String encode(CharSequence rawPassword) {
    String salt;
    if (random != null) {
        salt = BCrypt.gensalt(version.getVersion(), strength, random);
    } else {
        salt = BCrypt.gensalt(version.getVersion(), strength);
    }
    return BCrypt.hashpw(rawPassword.toString(), salt);
}

As you can see, the encode method above performs two steps: the first step is to generate a salt, and the second step is to generate the final encrypted password based on the salt and the plaintext password.

Implementing a Customized User Authentication Solution #

From the previous analysis, we can see that the implementation process of storing user information is completely customizable, and Spring Security only embeds commonly used and generally applicable implementation methods into the framework. If there are special scenarios, developers can completely implement them by customizing the user information storage solution.

In the previous content, we introduced that the UserDetails interface represents user details, and the UserDetailsService interface is responsible for various operations on UserDetails. Therefore, the key to implementing a customized user authentication solution is to implement these two interfaces, UserDetails and UserDetailsService.

Extending UserDetails #

The essence of extending the methods of UserDetails is to directly implement the interface. For example, we can build a SpringCssUser class as follows:

public class SpringCssUser implements UserDetails {
    private static final long serialVersionUID = 1L;
    private Long id;
    private final String username;
    private final String password;
    private final String phoneNumber;
    // getter/setter omitted
    @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;
    }
}

Clearly, we used a simpler method to meet the implementation requirements of the various interfaces in UserDetails. Once we have built a SpringCssUser class, we can create a corresponding table structure storage class to define the fields. At the same time, we can also create a custom repository based on Spring Data JPA, as shown in the following code:

public interface SpringCssUserRepository extends CrudRepository<SpringCssUser, Long> {
    SpringCssUser findByUsername(String username);
}

SpringCssUserRepository extends the CrudRepository interface and provides a method name-derived query, findByUsername.

For more information on how to use Spring Data JPA, you can review “ORM Integration: How to Use Spring Data JPA to Access Relational Databases?”.

Extending UserDetailsService #

Next, let’s implement the UserDetailsService interface, as shown in the following code:

@Service
public class SpringCssUserDetailsService implements UserDetailsService {
    @Autowired
    private SpringCssUserRepository repository;

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

In the UserDetailsService interface, we only need to implement the loadUserByUsername method. Therefore, we can use the findByUsername method of SpringCssUserRepository to query data from the database based on the username.

Integrating Customized Configuration #

Finally, let’s go back to the SpringCssSecurityConfig class.

This time, we will use the custom SpringCssUserDetailsService to store and query user information. Now we just need to make some adjustments to the configuration policy. The complete SpringCssSecurityConfig class after adjustments is as follows:

@Configuration
public class SpringCssSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    SpringCssUserDetailsService springCssUserDetailsService;

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

Here, we injected SpringCssUserDetailsService and added it to AuthenticationManagerBuilder. This way, AuthenticationManagerBuilder will create and manage UserDetails based on the custom SpringCssUserDetailsService.

Summary and Next Steps #

In this lesson, we detailed how to use Spring Security to build a user authentication system.

On the one hand, we can store user information using the built-in memory and database solutions, both of which are provided by Spring Security. On the other hand, we can implement a customized authentication solution by extending the UserDetails interface. At the same time, for your convenience in understanding and mastering this part of the content, we have also organized the core classes related to user authentication.

After introducing user authentication information, in Lesson 19, we will explain how to ensure secure access to web requests based on Spring Security.