07 Case Practice Using Spring Security Basic Functions to Protect Web Applications

07 Case Practice Using Spring Security Basic Functions to Protect Web Applications #

In the previous lectures, we systematically introduced the authentication and authorization features of Spring Security. These are the most fundamental and commonly used security features provided by the framework. As a summary of this phase, today we will integrate the content of the previous lectures and protect a web application based on Spring Security’s authentication and authorization features.

Case Study Design and Initialization #

In today’s case study, we will build a simple but complete small web application. After a valid user successfully logs in to the system, the browser will be redirected to a system homepage and display some personal health record data.

Case Study Design #

This web application will adopt the classic three-tier architecture, namely Web layer, Service layer, and Data Access layer. Therefore, we will have HealthRecordController, HealthRecordService, and HealthRecordRepository, which form an independent code flow to complete the system’s business logic processing.

On the other hand, the core feature of this case study is to implement a custom user authentication process. Therefore, we need to build an independent UserDetailsService and AuthenticationProvider, which form another independent code flow. In this code flow, components such as User and UserRepository will also be required.

By integrating these two code flows, we can obtain the overall design blueprint of the case study, as shown in the following diagram:

Drawing 0.png

Business code flow and user authentication process in the case study

System Initialization #

To achieve the effect shown in the above diagram, we need to initialize the system first. This part of the work involves defining domain objects, organizing database initialization scripts, and introducing relevant dependency components.

Regarding domain objects, let’s focus on the following User class definition:

@Entity
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    private String username;
    private String password;
    
    @Enumerated(EnumType.STRING)
    private PasswordEncoderType passwordEncoderType;
    
    @OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
    private List<Authority> authorities;
  
    // ...
}

As we can see, besides specifying the primary key id, the username, and the password, there is also an enumeration value EncryptionAlgorithm for specifying the encryption algorithm. In this case, we will provide two available password encoders: BCryptPasswordEncoder and SCryptPasswordEncoder, which can be set using this enumeration value.

In the User class, we also find a list of Authority. Obviously, this list is used to specify the authority information possessed by the User. The definition of the Authority class is as follows:

@Entity
public class Authority {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    private String name;
    
    @JoinColumn(name = "user")
    @ManyToOne
    private User user;
  
    // ...
}

From the definition, it is easy to see that User and Authority have a one-to-many relationship, which is consistent with the built-in user authority model in Spring Security. We also notice the use of a series of annotations from the Java Persistence API (JPA) specification to define the association between domain objects. You can learn more about the usage of these annotations by referring to the “Spring Data JPA Principles and Practice” column on Lagou Education.

Based on the User and Authority domain objects, we also provide the SQL definitions for creating database tables, as shown below:

CREATE TABLE IF NOT EXISTS `spring_security`.`user` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(45) NOT NULL,
    `password` TEXT NOT NULL,
    `password_encoder_type` VARCHAR(45) NOT NULL,
    PRIMARY KEY (`id`)
);

CREATE TABLE IF NOT EXISTS `spring_security`.`authority` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(45) NOT NULL,
    `user` INT NOT NULL,
    PRIMARY KEY (`id`)
);

Before running the system, we also need to initialize the data. The corresponding script is as follows:

INSERT IGNORE INTO `spring_security`.`user` (`id`, `username`, `password`, `password_encoder_type`) VALUES ('1', 'jianxiang', '$2a$10$xn3LI/AjqicFYZFruSwve.681477XaVNaUQbr1gioaWPn4t1KsnmG', 'BCRYPT');

INSERT IGNORE INTO `spring_security`.`authority` (`id`, `name`, `user`) VALUES ('1', 'READ', '1');
INSERT IGNORE INTO `spring_security`.`authority` (`id`, `name`, `user`) VALUES ('2', 'WRITE', '1');

INSERT IGNORE INTO `spring_security`.`health_record` (`id`, `username`, `name`, `value`) VALUES ('1', 'jianxiang', 'weight', '70');
INSERT IGNORE INTO `spring_security`.`health_record` (`id`, `username`, `name`, `value`) VALUES ('2', 'jianxiang', 'height', '177');
INSERT IGNORE INTO `spring_security`.`health_record` (`id`, `username`, `name`, `value`) VALUES ('3', 'jianxiang', 'bloodpressure', '70');
INSERT IGNORE INTO `spring_security`.`health_record` (`id`, `username`, `name`, `value`) VALUES ('4', 'jianxiang', 'pulse', '80');

Please note that here we initialized a user with the username “jianxiang” and the password “12345” with the encryption algorithm set to “BCRYPT”.

Now that the initialization work for the domain objects and data layer has been completed, we need to add the following Maven dependencies to the pom file of the code project:

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

This completes the initialization of the system. Next, we will focus on the implementation of the case study’s features. spring-boot-starter-thymeleaf

org.springframework.boot

spring-boot-starter-web

mysql

mysql-connector-java

runtime

org.springframework.security

spring-security-test

test

These dependencies are very common, and I believe you can understand the functions of each dependency from their package names.

Reference links for dependencies: spring-boot-starter-data-jpa: https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa spring-boot-starter-security: https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security spring-boot-starter-thymeleaf: https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf spring-boot-starter-web: https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web mysql-connector-java: https://mvnrepository.com/artifact/mysql/mysql-connector-java spring-security-test: https://mvnrepository.com/artifact/org.springframework.security/spring-security-test

Implementing Custom User Authentication #

Implementing custom user authentication typically involves two main parts: customizing user management using the User and Authority objects, and integrating this customized user management into the entire user authentication process. Below, we will analyze these two parts in detail.

Implementing User Management #

We know that in Spring Security, the interface representing user information is UserDetails. We also introduced the specific definition of the UserDetails interface in the lesson “Understanding the User Authentication Mechanism of Spring Security” (Lesson 03). To implement custom user information, you just need to extend this interface. The implementation is as follows:

public class CustomUserDetails implements UserDetails {

    private final User user;

    public CustomUserDetails(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getAuthorities().stream()
                   .map(a -> new SimpleGrantedAuthority(a.getName()))
                   .collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

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

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

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

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

    public final User getUser() {
        return user;
    }
}

The CustomUserDetails class above implements all the methods defined in the UserDetails interface. Note that in the getAuthorities() method, we convert the Authority list in the User object to a SimpleGrantedAuthority list, which represents the user’s permissions in Spring Security.

Of course, all the custom user information and permission information are maintained in the database. Therefore, to retrieve this information, we need to create a data access component, which is the UserRepository. The definition is as follows:

public interface UserRepository extends JpaRepository<User, Integer> {

    Optional<User> findUserByUsername(String username);
}

Here, we simply extend the JpaRepository interface provided by Spring Data JPA and use the method name derived query mechanism to define the findUserByUsername method, which retrieves user information based on the username.

Now that we are able to maintain custom user information in the database and retrieve UserDetails objects based on this information, the next step is to extend UserDetailsService. The implementation of the custom CustomUserDetailsService is as follows:

@Service
    @Override

    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable()

                .authorizeRequests()

                .antMatchers("/admin/**").hasRole("ADMIN")

                .antMatchers("/user/**").hasRole("USER")

                .anyRequest().authenticated()

                .and()

                .formLogin()

                .loginPage("/login")

                .defaultSuccessUrl("/home")

                .failureUrl("/login-error")

                .permitAll()

                .and()

                .logout()

                .logoutUrl("/logout")

                .logoutSuccessUrl("/login")

                .permitAll();

    }

 

    @Override

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.authenticationProvider(authenticationProvider);

    }
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
    return new BCryptPasswordEncoder();
}

@Bean
public SCryptPasswordEncoder sCryptPasswordEncoder() {
    return new SCryptPasswordEncoder();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(authenticationProvider);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin()
        .defaultSuccessUrl("/healthrecord", true);
    http.authorizeRequests().anyRequest().authenticated();
}

Here, the AuthenticationProviderService that has been constructed is injected, and two password encoders, BCryptPasswordEncoder and SCryptPasswordEncoder, are initialized. Finally, we override the configure() method in the WebSecurityConfigurerAdapter configuration adapter class and specify that the user will be redirected to the page specified by the “/main” path after successful login.

Correspondingly, we need to construct the MainController class as shown below, to specify the “/main” path and display the process of retrieving business data:

@Controller
public class HealthRecordController {
    @Autowired
    private HealthRecordService healthRecordService;
    
    @GetMapping("/healthrecord")
    public String main(Authentication a, Model model) {
        String userName = a.getName();
        model.addAttribute("username", userName);
        model.addAttribute("healthRecords", healthRecordService.getHealthRecordsByUsername(userName));
        return "health_record.html";
    }
}

We obtain the authentication user information through the Authentication object and retrieve health record information through the HealthRecordService. The implementation logic of HealthRecordService is not the focus of today’s content, you can refer to the sample code for learning: https://github.com/lagouEdAnna/SpringSecurity-jianxiang/tree/main/SpringSecurityBasicDemo.

Please note that the health_record.html specified here is located in the resources/templates directory. This page is built based on the Thymeleaf template engine, as shown below:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>健康档案</title>
    </head>
    <body>
        <h2 th:text="'登录用户:' + ${username}" />
        <p><a href="/logout">退出登录</a></p>
        <h2>个人健康档案:</h2>
        <table>
            <thead>
                <tr>
                    <th> 健康指标名称 </th>
                    <th> 健康指标值 </th>
                </tr>
            </thead>
            <tbody>
                <tr th:if="${healthRecords.empty}">
                    <td colspan="2"> 无健康指标 </td>
                </tr>
                <tr th:each="healthRecord : ${healthRecords}">
                    <td><span th:text="${healthRecord.name}"> 健康指标名称 </span></td>
                    <td><span th:text="${healthRecord.value}"> 健康指标值 </span></td>
                </tr>
            </tbody>
        </table>
    </body>
</html>

Here, we obtain the authentication user information and health record information from the Model object and render them on the page.

Case Demonstration #

Now, let’s start the Spring Boot application and access the http://localhost:8080 endpoint. Because authentication is required to access any endpoint in the system, Spring Security will automatically redirect to the login page as shown below:

Drawing 1.png

User login page

We enter the username “jianxiang” and password “12345”, and the system will redirect to the Health Record homepage:

Drawing 2.png

Health Record homepage

In this homepage, we correctly obtain the username of the logged-in user and display the personal health record information. This result also confirms the correctness of implementing a custom user authentication system. You can try some experiments based on the sample code.

Summary and Preview #

In this lesson, we practiced “Protecting Web Applications with Basic Spring Security Features.” Combining the core knowledge points from Lesson 2 to Lesson 6, we designed a simple and complete case and explained the process of implementing a custom user authentication mechanism by building a user management and authentication process.

The summary of this lesson is as follows:

Drawing 3.png

Finally, here is a question for you to ponder: What are the development steps required to implement a custom user authentication system in Spring Security?