Spring Security custom user authentication

Posted by remal on Wed, 13 Nov 2019 06:15:47 +0100

Read more technical articles about Angular, TypeScript, Node.js/Java, Spring, etc. welcome to my blog—— The road of building immortals in the whole stack

stay Spring Boot integrates Spring Security In this article, we introduced how to quickly integrate Spring Security in the Spring Boot project, as well as how to change the user name and password generated by the system by default. Next, this article will introduce how to implement custom user authentication in Spring Security based on the project created in the article of integrating Spring Security with Spring Boot.

I. user defined authentication process

Development environment and main framework version used in the project:

  • java version "1.8.0_144"
  • spring boot 2.2.0.RELEASE
  • spring security 5.2.0.RELEASE

1.0 configuration project pom.xml file

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.semlinker</groupId>
    <artifactId>custom-user-authentication</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>custom-user-authentication</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
      
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- ellipsis spring-boot-starter-test,spring-security-test and spring-boot-devtools -->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

1.1 user defined model

First, create a MyUser class to store the simulated user information (in actual development, the real user information is usually obtained from the database):

// com/semlinker/domain/MyUser.java
@Data
public class MyUser implements Serializable {
    private static final long serialVersionUID = -1090551705063344205L;

    private String userName;
    private String password;
    private boolean accountNonExpired = true; // Indicates whether the account has not expired
    private boolean accountNonLocked = true; // Indicates whether the account is unlocked
    private boolean credentialsNonExpired = true; // Indicates that the user's credentials have not expired, such as the user password
    private boolean enabled = true; // Indicates whether the user is enabled
}

1.2 custom Security configuration class and PasswordEncoder object

Then configure the PasswordEncoder object, as the name implies, for password encryption. This object needs to be used in the following UserDetailsService service, so we need to configure it in advance. PasswordEncoder is a password encryption interface. There are many implementation classes in Spring Security, such as BCryptPasswordEncoder, Pbkdf2PasswordEncoder and LdapShaPasswordEncoder.

Of course, we can also customize PasswordEncoder, but the function of BCryptPasswordEncoder implemented in Spring Security is powerful enough. It can generate different results after encrypting the same password, which greatly improves the security of the system. That is to say, although some users who use the same password in the system accidentally disclose the password, it will not cause other users to disclose the password. Since BCryptPasswordEncoder is so powerful, we must use it directly. The specific configuration is as follows:

// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

1.3 customize UserDetailsService service

To customize the UserDetailsService service, you need to implement the UserDetailsService interface, which only contains a loadUserByUsername method, which is used to load matching users through username. A UsernameNotFoundException exception is thrown when the user corresponding to username cannot be found. The UserDetailsService interface is defined as follows:

// org/springframework/security/core/userdetails/UserDetailsService.java
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

The loadUserByUsername method returns the UserDetails object. The UserDetails here is also an interface. Its definition is as follows:

// org/springframework/security/core/userdetails/UserDetails.java
public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

As the name implies, UserDetails represents detailed user information. This interface covers some necessary user information fields, which are extended by specific implementation classes. The specific functions of the above methods are as follows:

  • getPassword(): used to get the password;
  • getUsername(): used to get the user name;
  • isAccountNonExpired(): used to determine whether the account has not expired;
  • isAccountNonLocked(): used to determine whether the account is unlocked;
  • isCredentialsNonExpired(): used to determine whether the user's credentials have not expired, that is, whether the password has not expired;
  • isEnabled(): used to determine whether the user is available.

After introducing the above content, let's create a MyUserDetailsService class and implement the UserDetailsService interface, as follows:

// com/semlinker/service/MyUserDetailsService.java
@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MyUser myUser = new MyUser();
        myUser.setUserName(username);
        myUser.setPassword(this.passwordEncoder.encode("hello"));

        // Use the implementation class User of UserDetails in Spring Security to create the User object
        return new User(username, myUser.getPassword(), myUser.isEnabled(),
                myUser.isAccountNonExpired(), myUser.isCredentialsNonExpired(),
                myUser.isAccountNonLocked(),
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

1.4 configure UserDetailsService Bean and AuthenticationManagerBuilder object

To use our customized MyUserDetailsService in Spring Security, you also need to configure it in the WebSecurityConfig class:

// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    UserDetailsService myUserDetailService() {
        return new MyUserDetailsService();
    }

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailService()).passwordEncoder(passwordEncoder());
    }
}

In the above configure method, we configured the custom MyUserDetailsService and PasswordEncoder objects.

1.5 create relevant Controller and user-defined login page and homepage

In Spring Security, the DefaultLoginPageGeneratingFilter will generate the default login interface for us:

I believe that many young people are "not used to" this page. Let's "facelift" this page.

HomeController class
// com/semlinker/controller/HomeController.java
@Controller
public class HomeController {

    @GetMapping("/")
    public String index() {
        return "index";
    }

}
UserController class
// com/semlinker/controller/UserController.java
@Controller
public class UserController {

    @GetMapping("/login")
    public String login() {
        return "login";
    }

}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Semlinker Home page of Xiuxian Road </title>
</head>
<body>
   <h3>Welcome to Semlinker Home page of Xiuxian Road</h3>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Semlinker Road to immortality landing page</title>
</head>
<body>
<form class="login-form" method="post" action="/login">
    <h1>Login</h1>
    <div class="form-field">
        <i class="fas fa-user"></i>
        <input type="text" name="username" id="username" class="form-field" 
               placeholder=" " required>
        <label for="username">Username</label>
    </div>
    <div class="form-field">
        <i class="fas fa-lock"></i>
        <input type="password" name="password" id="password" class="form-field" 
               placeholder=" " required>
        <label for="password">Password</label>
    </div>
    <button type="submit" value="Login" class="btn">Login</button>
</form>
</body>
</html>

1.6 configure default login page

After the login page is created, it needs to be configured in the WebSecurityConfig class to take effect. The corresponding configuration method is as follows:

// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    // Omit the previously set content
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
            .loginPage("/login");
    }
}

After completing the above configuration, let's test the effect. First, start the Spring Boot application, and then open it in the browser http://localhost:8080/login Address, if all goes well, you will see the following interface:

(page from https://codepen.io/alphardex/...)

Next, we will perform the login operation. The user name here can be arbitrary, and the password is the hello we set earlier. But when we input the correct user name and password and click to log in, we will see the following exception page:

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Mon Oct 28 14:27:25 CST 2019
There was an unexpected error (type=Forbidden, status=403).
Forbidden

What's the reason? Why is access forbidden? Don't worry. First open the application.properties file under the src/main/resources / directory of the current project, and then enter the following configuration information:

logging.level.org.springframework.security.web.FilterChainProxy=DEBUG

After the configuration is completed, restart the application, and then perform the above login operation again. If you have guessed correctly, you can log in again. The user name and password you entered are correct, but you still see the Whitelabel Error Page. In fact, the DEBUG mode of the Security FilterChainProxy that we have just enabled, so let's take a look at the exception information output by the console:

You can find the / login request through the figure above. After the csrfilter filter, it will no longer continue to execute. The csrfilter filter here is used to deal with cross site request forgery attacks. Cross Site Request Forgery, also known as one click attack or session riding, is usually abbreviated as CSRF or XSRF. It is an attack method to coerce users to perform unintended operations on the currently logged in Web applications.

Now we have a general idea of the reason. Since our login page does not need to enable Csrf defense, we first disable the Csrf filter:

// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login")
                .and().csrf().disable();
    }
}

After updating the WebSecurityConfig configuration class, run the previous login process again. This time, when you click login, you will see the content welcome to the home page of Semlinker fairy road.

2. Handling different types of requests

By default, when users access the protected resources through the browser, they will be automatically redirected to the default login address by default. This is not a big problem for traditional Web projects, but it is not suitable for projects with front-end and back-end separation. For projects separated from the front and the back, the server generally only needs to provide the API interface in JSON format.

In view of the above problems, there is a scheme as follows for reference. That is, according to whether the request ends in. html, different processing methods are corresponding. If it ends with. html, redirect to the login page, otherwise return to "access resources need authentication!" Information, and the HTTP status code is 401 (HttpStatus.UNAUTHORIZED).

To implement the above functions, we first define a WebSecurityController class, which is implemented as follows:

// com/semlinker/controller/WebSecurityController.java
@Slf4j
@RestController
public class WebSecurityController {
    // Cache and recovery of original request information
    private RequestCache requestCache = new HttpSessionRequestCache();

    // For redirection
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    /**
     * Default login page, used to handle different login authentication logic
     *
     * @param request
     * @param response
     * @return
     */
    @RequestMapping("/authentication/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public String requireAuthenication(HttpServletRequest request, 
      HttpServletResponse response) throws Exception {
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (savedRequest != null) {
            String targetUrl = savedRequest.getRedirectUrl();
            log.info("The request that caused the jump is:" + targetUrl);
            if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
                redirectStrategy.sendRedirect(request, response, "/login.html");
            }
        }

        return "The accessed service requires identity authentication. Please guide the user to the login page";
    }
}

Next, change the default login page of formLogin to / authentication/require, and set no interception through the ant matchers method:

// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/authentication/require")
                .and()
                .authorizeRequests()
                .antMatchers("/authentication/require", "/login.html").permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable()
        ;
    }
}

At the same time, modify the UserController class defined earlier to support / login.html path mapping:

// com/semlinker/controller/UserController.java
@Controller
public class UserController {

    @GetMapping({"login", "/login.html"})
    public String login() {
        return "login";
    }

}

After the above adjustment, visit us http://localhost:8080/index The page will automatically jump to http://localhost:8080/authentication/require , and output "the accessed service needs identity authentication, please guide the user to the login page". And when we visit http://localhost:8080/index.html When, the page will jump to the login page.

3. User defined login success and failure logic

In the front-end and back-end separation project, when users log in successfully or fail, they need to return corresponding information to the front-end, rather than directly jump the page. For the scenario of front-end and back-end separation, the two interfaces of AuthenticationSuccessHandler and AuthenticationFailureHandler in Spring Security, or inheriting simpleurauthenticationsuccesshandler or simpleurauthenticationfailurehandler class, can be used to implement the processing logic of user-defined login success and login failure.

3.1 user defined login success processing logic

Here we choose to inherit the SimpleUrlAuthenticationSuccessHandler class to implement the custom login success processing logic:

// com/semlinker/handler/MyAuthenctiationSuccessHandler.java
@Slf4j
@Component
public class MyAuthenctiationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
      HttpServletResponse response, Authentication authentication)
        throws IOException, ServletException {

        log.info("Login successfully");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
}

3.2 user defined login failure processing logic

We also choose to inherit the SimpleUrlAuthenticationFailureHandler class to implement the custom login failure processing logic:

// com/semlinker/handler/MyAuthenctiationFailureHandler.java
@Slf4j
@Component
public class MyAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, 
      HttpServletResponse response,AuthenticationException exception) 
        throws IOException, ServletException {

        log.info("Login failed");
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
    }
}

3.3 configure myauthorizationsuccesshandler and myauthorizationfailurehandler

Finally, for the login success and failure logic of custom processing to take effect, you need to configure the successHandler and failureHandler properties of the FormLoginConfigurer object in the WebSecurityConfig class. So far, the complete configuration of the WebSecurityConfig class is as follows:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyAuthenctiationFailureHandler myAuthenctiationFailureHandler;

    @Autowired
    private MyAuthenctiationSuccessHandler myAuthenctiationSuccessHandler;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    UserDetailsService myUserDetailService() {
        return new MyUserDetailsService();
    }

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailService()).passwordEncoder(passwordEncoder());
    }

    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login")
                .successHandler(myAuthenctiationSuccessHandler)
                .failureHandler(myAuthenctiationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers("/authentication/require", "/login").permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable()
        ;
    }
}

The previous article has introduced the process of implementing user-defined authentication in Spring Security. If you encounter other problems during the learning process, it is recommended to enable the DEBUG mode of FilterChainProxy for log troubleshooting.

Project address: Github - custom-user-authentication

IV. reference resources

Topics: Java Spring Maven Apache