This is the 8th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021.

Spring Security allows us to customize the authentication process, such as handling the user information retrieval logic, replacing Spring Security’s default login page with our custom login page, and customizing the logic after a successful or failed login. This will build on the source code from the previous section.

Customize the authentication process

The process of custom authentication requires the implementation of the UserDetailService interface provided by Spring Security, which has only one abstract method loadUserByUsername, the source code is as follows:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Copy the code

The loadUserByUsername method returns a UserDetail object, which is also an interface containing methods for describing user information. The source code is as follows:

public interface UserDetails extends Serializable {

    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword(a);

    String getUsername(a);

    boolean isAccountNonExpired(a);

    boolean isAccountNonLocked(a);

    boolean isCredentialsNonExpired(a);

    boolean isEnabled(a);
}
Copy the code

The implications of these methods are as follows:

  • getAuthoritiesGet the permissions contained by the user, return the permissions set, permissions are an inheritanceGrantedAuthorityThe object;
  • getPasswordandgetUsernameUsed to obtain passwords and user names.
  • isAccountNonExpiredThe Boolean () method returns a Boolean to determine whether the account has not expired. True if the account has expired, false if the account has expired.
  • isAccountNonLockedMethod is used to determine whether the account is unlocked.
  • isCredentialsNonExpiredIt is used to check whether the user credentials are not expired, that is, whether the password is not expired.
  • isEnabledThe method is used to determine whether a user is available.

Practice, we can customize populated UserDetails interface implementation class, also can use Spring Security directly provide populated UserDetails interface implementation class org. Springframework. Security. Core. Populated UserDetails. User.

With that said, let’s start implementing the loadUserByUsername method of the UserDetailService interface.

Create a MyUser object to store simulated user data (in practice, it is usually obtained from the database, here to facilitate direct simulation) :

public class MyUser implements Serializable {
    private static final long serialVersionUID = 3497935890426858541L;

    private String userName;

    private String password;

    private boolean accountNonExpired = true;

    private boolean accountNonLocked= true;

    private boolean credentialsNonExpired= true;

    private boolean enabled= true;

    / / get, set slightly
}
Copy the code

Then create MyUserDetailService to implement UserDetailService:

@Configuration
public class UserDetailService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Impersonate a user instead of database fetch logic
        MyUser user = new MyUser();
        user.setUserName(username);
        user.setPassword(this.passwordEncoder.encode("123456"));
        // Prints the encrypted password
        System.out.println(user.getPassword());

        return new User(username, user.getPassword(), user.isEnabled(),
                user.isAccountNonExpired(), user.isCredentialsNonExpired(),
                user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); }}Copy the code

Here we use the org. Springframework. Security. Core. Populated userdetails. The User class contains seven parameters of the constructor, It also contains a three-argument constructor User(String username, String password,Collection
authorities), since the permission parameter cannot be empty, So here AuthorityUtils.com maSeparatedStringToAuthorityList method is used to simulate a first admin permissions, this method can be a comma delimited string converted to privilege set.

We also injected the PasswordEncoder object, which is used for password encryption and needs to be configured manually before injection. We configure it in BrowserSecurityConfig:

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(a) {
        return newBCryptPasswordEncoder(); }... }Copy the code

PasswordEncoder is a password encryption interface, and BCryptPasswordEncoder is an implementation provided by Spring Security. You can also implement PasswordEncoder yourself. However, the BCryptPasswordEncoder implemented by Spring Security is powerful enough to encrypt the same password and produce different results.

Then restart the project, visit http://localhost:8080/login, you can use any user name and 123456 as the password to login system. After logging in several times, we can see the encrypted password output by the console as follows:

As you can see, the BCryptPasswordEncoder produces a different result each time for the same password.

Replace the default login page

The default login page is too crude, so we can define our own login page. For convenience, we directly in the SRC/main/resources/resources directory to define a login HTML (don’t need the Controller jump) :

<! DOCTYPEhtml>
<html>
<head>
    <meta charset="UTF-8">
    <title>The login</title>
    <link rel="stylesheet" href="css/login.css" type="text/css">
</head>
<body>
    <form class="login-page" action="/login" method="post">
        <div class="form">
            <h3>The account login</h3>
            <input type="text" placeholder="Username" name="username" required="required" />
            <input type="password" placeholder="Password" name="password" required="required" />
            <button type="submit">The login</button>
        </div>
    </form>
</body>
</html>
Copy the code

What do we do to get Spring Security to jump to our own login page? It’s as simple as adding some configuration to Configure in BrowserSecurityConfig:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin() // Form login
            // http.httpBasic() // HTTP Basic
            .loginPage("/login.html") 
            .loginProcessingUrl("/login")
            .and()
            .authorizeRequests() // Authorization configuration
            .antMatchers("/login.html").permitAll()
            .anyRequest()  // All requests
            .authenticated(); // Both require authentication
}
Copy the code

In the code above,.loginPage(“/login.html”) specifies the request URL to jump to the login page,.loginProcessingURL (“/login”) corresponds to the action=”/login” of the login page form form, .antmatchers (“/login.html”).permitall () means that the request to jump to the login page is not blocked, otherwise it will enter an infinite loop.

Then start the system, visit http://localhost:8080/hello, will see the page has been redirected to http://localhost:8080/login.html:

An error occurs when you enter the user name and password:

Configure BrowserSecurityConfig:

Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin() // Form login
            // http.httpBasic() // HTTP Basic
            .loginPage("/login.html") // Log in to the redirect URL
            .loginProcessingUrl("/login") // Process the form login URL
            .and()
            .authorizeRequests() // Authorization configuration
            .antMatchers("/login.html").permitAll() // No authentication is required to log in to the redirect URL
            .anyRequest()  // All requests
            .authenticated() // Both require authentication
            .and().csrf().disable();
}
Copy the code

Restart the project to log in normally.

Suppose you now have a requirement to jump to the login page when the user accesses an HTML resource without logging in, otherwise return JSON data with status code 401.

To do this we change the loginPage URL to /authentication/require and add this URL to antMatchers to make it interception-free:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin() // Form login
            // http.httpBasic() // HTTP Basic
            .loginPage("/authentication/require") // Log in to the redirect URL
            .loginProcessingUrl("/login") // Process the form login URL
            .and()
            .authorizeRequests() // Authorization configuration
            .antMatchers("/authentication/require"."/login.html").permitAll() // No authentication is required to log in to the redirect URL
            .anyRequest()  // All requests
            .authenticated() // Both require authentication
            .and().csrf().disable();
}
Copy the code

Then define a controller BrowserSecurityController, deal with the request:

@RestController
public class BrowserSecurityController {
    private RequestCache requestCache = new HttpSessionRequestCache();
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @GetMapping("/authentication/require")
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public String requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if(savedRequest ! =null) {
            String targetUrl = savedRequest.getRedirectUrl();
            if (StringUtils.endsWithIgnoreCase(targetUrl, ".html"))
                redirectStrategy.sendRedirect(request, response, "/login.html");
        }
        return "Access to resources requires authentication!"; }}Copy the code

Where HttpSessionRequestCache is the object provided by Spring Security for caching requests. The HTTP information of this request can be obtained by calling its getRequest method. DefaultRedirectStrategy’s sendRedirect provides a method for Spring Security to handle redirects.

The above code gets the request to trigger the jump, and the processing varies depending on whether the request ends in.html or not. If it ends in.html, redirect to the login page, otherwise return “Accessed resource requires authentication!” Information, and the HTTP status code is 401 (httpStatus.unauthorized).

So when we visit http://localhost:8080/hello page will jump to http://localhost:8080/authentication/require, and output “access resources need to identity authentication!” When we visit http://localhost:8080/hello.html page will jump to the login page.

Deal with successes and failures

Spring Security has a default login processing method of success and failure: when a user login successfully, the page will jump will trigger a login request, such as in the case of not log on to http://localhost:8080/hello, the page will jump to the login page and log in again after successful jump back; Failure to log in redirects you to Spring Security’s default error page. Let’s replace this default processing mechanism with some custom configurations.

Customize login success logic

Changing the default processing success logic is simple, Only need to implement org. Springframework. Security. Web. Authentication. AuthenticationSuccessHandler onAuthenticationSuccess method can interface:

@Component
public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json; charset=utf-8"); response.getWriter().write(mapper.writeValueAsString(authentication)); }}Copy the code

The Authentication parameter not only contains some information about the Authentication request, such as IP, request SessionId, etc., but also contains the User information, namely the User object mentioned above. After the preceding configuration, information about the Authentication object is displayed on the page after the user logs in successfully.

To make this configuration work, we also configure it in Configure for BrowserSecurityConfig:

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyAuthenticationSucessHandler authenticationSucessHandler;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() // Form login
                // http.httpBasic() // HTTP Basic
                .loginPage("/authentication/require") // Log in to the redirect URL
                .loginProcessingUrl("/login") // Process the form login URL
                .successHandler(authenticationSucessHandler) // The login succeeded
                .and()
                .authorizeRequests() // Authorization configuration
                .antMatchers("/authentication/require"."/login.html").permitAll() // No authentication is required to log in to the redirect URL
                .anyRequest()  // All requests
                .authenticated() // Both require authentication.and().csrf().disable(); }}Copy the code

We will MyAuthenticationSucessHandler injection to come in, and configured by successHandler method.

After the project is restarted, the following JSON information will be displayed:

{
  "authorities": [{"authority": "admin"}]."details": {
    "remoteAddress": "0:0:0:0:0:0:0:1"."sessionId": "8D50BAF811891F4397E21B4B537F0544"
  },
  "authenticated": true."principal": {
    "password": null."username": "mrbird"."authorities": [{"authority": "admin"}]."accountNonExpired": true."accountNonLocked": true."credentialsNonExpired": true."enabled": true
  },
  "credentials": null."name": "mrbird"
}
Copy the code

Spring Security has already masked sensitive information such as passwords and credentials.

In addition, we also can do jump of the page after login successfully, modify MyAuthenticationSucessHandler:

@Component
public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler {
    private RequestCache requestCache = new HttpSessionRequestCache();
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    private ObjectMapper mapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { SavedRequest savedRequest = requestCache.getRequest(request, response); redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl()); }}Copy the code

After a successful login, the page is redirected to the page that triggered the login. Page if you want to skip, jump to/index, for example, can be savedRequest. GetRedirectUrl () is modified to/index:

@Component
public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler {
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        redirectStrategy.sendRedirect(request, response, "/index"); }}Copy the code

Then define a method to handle the request in TestController:

@RestController
public class TestController {
    @GetMapping("index")
    public Object index(a){
        returnSecurityContextHolder.getContext().getAuthentication(); }}Copy the code

After the success of the login, you can use SecurityContextHolder. GetContext () getAuthentication () to the Authentication object information. In addition to obtaining Authentication object information in this way, you can also use the following method:

@RestController
public class TestController {
    @GetMapping("index")
    public Object index(Authentication authentication) {
        returnauthentication; }}Copy the code

Restart the project, after the success of the login, the page will jump to http://localhost:8080/index:

Custom logon failure logic

Similar to custom logon success processing logic, The custom login failure processing logic to implement org. Springframework. Security. Web. Authentication. AuthenticationFailureHandler onAuthenticationFailure method:

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {}}Copy the code

The AuthenticationException parameter to the onAuthenticationFailure method is an abstract class, and Spring Security encapsulates a number of corresponding implementation classes based on the cause of the login failure, View AuthenticationException’s Hierarchy:

Corresponding to the failure causes of different abnormalities, such as a user name or password error corresponding BadCredentialsException, user does not exist corresponding is UsernameNotFoundException, user was locked LockedException, etc.

If we need to return a failure message in case of a login failure, we can do this:

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper mapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json; charset=utf-8"); response.getWriter().write(mapper.writeValueAsString(exception.getMessage())); }}Copy the code

The status code is defined as 500 (httpstatus.internal_server_error.value ()), indicating an internal system exception.

Again, we need to configure it in BrowserSecurityConfig:

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyAuthenticationSucessHandler authenticationSucessHandler;

    @Autowired
    private MyAuthenticationFailureHandler authenticationFailureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() // Form login
                // http.httpBasic() // HTTP Basic
                .loginPage("/authentication/require") // Log in to the redirect URL
                .loginProcessingUrl("/login") // Process the form login URL
                .successHandler(authenticationSucessHandler) // The login succeeded
                .failureHandler(authenticationFailureHandler) // Process login failure
                .and()
                .authorizeRequests() // Authorization configuration
                .antMatchers("/authentication/require"."/login.html").permitAll() // No authentication is required to log in to the redirect URL
                .anyRequest()  // All requests
                .authenticated() // Both require authentication.and().csrf().disable(); }}Copy the code

Restart the project. If an incorrect password is entered, the following output is displayed: