Spring Security Learning -- form based authentication

Posted by aaronxbond on Thu, 20 Jan 2022 11:56:58 +0100

preface

Starting from this article, we are officially learning spring security. On the basis of the first two blogs, we slowly enrich our examples (not mine, but a course on mk online)

helloworld

The development of spring security is different from the application development of our ordinary projects. We first run through the spring security simple hello world on the original basis

security.basic.enabled = true

Add the above configuration in the spring security demo application (if you don't know the relationship between applications, you can refer to the first chapter of this series of blogs), and then start the project,

There is such a line in the startup log

This is the default password. If you visit any business interface, a password box will pop up

Enter the user name - user, and the password is the long string in the log before you can access the business interface normally

The above is not all about helloworld. There are some more to talk about

Add the following configuration classes to the browser module

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * autor:liman
 * createtime:2021/7/6
 * comment:
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()//Log in with forms
                .and()
                .authorizeRequests()//And request authentication
                .anyRequest()//Request for arbitrary
                .authenticated();//All need to be certified
    }
}

The login page configured here will change to the following instead of popping up a basic dialog box for login. Form login is implemented by default here. If you want to change the above configuration class into a basic dialog box for login, you only need to set http Formlogin becomes http Httpbasic.

Talk about the principle

Spring security is different from our traditional development method. A diagram can briefly describe the principle of spring security

Spring security is developed based on the filter chain. The core is a set of filters. The one on the right actually represents our application. These filters in spring security will be injected automatically when springboot starts. Among them, the green filter is used to authenticate user identity. For example, helloworld on the upper surface actually introduces two authentication methods, one is the form login of user name and password, and the other is the login of HTTP basic. Each filter will check whether there are required parameters in the request header. If so, it will process the current user authentication, and mark the user as logged in after processing. The last FilterSecurityInterceptor is the final gatekeeper. In this interceptor, it will determine whether the current request can access the subsequent business interface. If it can be accessed, the current request will be ignored. If it cannot be accessed, different exceptions will be thrown for different reasons. Subsequent exceptiontranslationfilters will catch the relevant exceptions thrown by FilterSecurityInterceptor and handle them differently.

In practice, the filter chain in spring security is much more complex than this. Here is a brief introduction to the most basic helloworld example.

Custom user authentication logic

helloworld briefly introduces the form login, but the password is automatically generated by spring security, which certainly can not meet our customized development.

Acquisition of user information

For obtaining user information, spring security is defined in the UserDetailsService interface, which has only one method. The source code is as follows

public interface UserDetailsService {
	// ~ Methods
	// ========================================================================================================

	/**
	 * Locates the user based on the username. In the actual implementation, the search
	 * may possibly be case sensitive, or case insensitive depending on how the
	 * implementation instance is configured. In this case, the <code>UserDetails</code>
	 * object that comes back may have a username that is of a different case than what
	 * was actually requested..
	 *
	 * @param username the username identifying the user whose data is required.
	 *
	 * @return a fully populated user record (never <code>null</code>)
	 *
	 * @throws UsernameNotFoundException if the user could not be found or the user has no
	 * GrantedAuthority
	 */
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

We just need to customize and implement this interface, and then hand it over to spring management

In the user-defined read user information service, there is no operation database here, but this class has been declared through @ Component, which can rely on other customized processing logic.

@Component
@Slf4j
public class MyUserDetailService implements UserDetailsService {


    /**
     * Find user information by user name
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("Find user information by user name:{}",username);
        //Initially, there is a User object. The User object implements the UserDetails information. At present, the third parameter can be ignored
        return new User(username,"123456",AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

After that, the password for the form login becomes 123456 instead of the password automatically generated every time spring security is started.

Verification of user information

User verification is divided into two parts: password verification and other user status verification (such as whether the account has expired). These are defined in the UserDetails interface

public interface UserDetails extends Serializable {

	/**
	 * Returns the authorities granted to the user. Cannot return <code>null</code>.
	 *
	 * @return the authorities, sorted by natural key (never <code>null</code>)
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * Returns the password used to authenticate the user.
	 *
	 * @return the password
	 */
	String getPassword();

	/**
	 * Returns the username used to authenticate the user. Cannot return <code>null</code>
	 * .
	 *
	 * @return the username (never <code>null</code>)
	 */
	String getUsername();

	/**
	 * Indicates whether the user's account has expired. An expired account cannot be
	 * authenticated.
	 *
	 * @return <code>true</code> if the user's account is valid (ie non-expired),
	 * <code>false</code> if no longer valid (ie expired)
	 */
	boolean isAccountNonExpired();

	/**
	 * Indicates whether the user is locked or unlocked. A locked user cannot be
	 * authenticated.
	 *
	 * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
	 */
	boolean isAccountNonLocked();

	/**
	 * Indicates whether the user's credentials (password) has expired. Expired
	 * credentials prevent authentication.
	 *
	 * @return <code>true</code> if the user's credentials are valid (ie non-expired),
	 * <code>false</code> if no longer valid (ie expired)
	 */
	boolean isCredentialsNonExpired();

	/**
	 * Indicates whether the user is enabled or disabled. A disabled user cannot be
	 * authenticated.
	 *
	 * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
	 */
	boolean isEnabled();
}

There are four boolean methods, which respectively indicate whether the account expires, whether the account is locked, whether the authentication expires, and whether the user is available (in the order of method declaration).

In the previous summary, we did not specify these four parameters when initializing the User object. In fact, the User object also provides several constructors. These extra parameters specify these four values.

return new User(username,"123456",
        true,true,true,false,
        AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));

In the User-defined UserDetailService, the above User object can be constructed. The fourth parameter indicates that the User is locked (in actual development, these must be set according to their own logic). At this time, even if the password is entered correctly, the following prompt will appear

Encryption and decryption

There should be no system to store users' plaintext passwords. Encryption and decryption must still be available. The interface for encryption and decryption in spring security is a PasswordEncoder interface. There are only two methods in this interface

public interface PasswordEncoder {

	/**
	 * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
	 * greater hash combined with an 8-byte or greater randomly generated salt.
	 */
	String encode(CharSequence rawPassword);

	/**
	 * Verify the encoded password obtained from storage matches the submitted raw
	 * password after it too is encoded. Returns true if the passwords match, false if
	 * they do not. The stored password itself is never decoded.
	 *
	 * @param rawPassword the raw password to encode and match
	 * @param encodedPassword the encoded password from storage to compare with
	 * @return true if the raw password, after encoding, matches the encoded password from
	 * storage
	 */
	boolean matches(CharSequence rawPassword, String encodedPassword);

}

It can also be seen from the name that one method is encryption and the other method is matching ciphertext.

Just add the specific implementation class of PasswordEncoder in the configuration, and spring security will automatically encrypt the password sent by the front end for us

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()//Log in with forms
                .and()
                .authorizeRequests()//And request authentication
                .anyRequest()//Request for arbitrary
                .authenticated();//All need to be certified
    }
}

Add the encryption and decryption components we need in the configuration class. Later, if we need to use PasswordEncoder to match ciphertext, we only need to inject.

@Component
@Slf4j
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;


    /**
     * Find user information by user name
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("Find user information by user name:{}",username);
        //Here is the result of matching encryption
        return new User(username,passwordEncoder.encode("123456"),
                true,true,true,true,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

By printing the log, you can see that the ciphertext you see is different for each request.

Custom authentication process

If you simply need to implement custom user authentication logic, it is not complicated. The above examples are basically sufficient, but if you want to implement custom authentication process, it is a little complicated

Custom login page

It is easy to specify a custom login page. You only need to configure the following results in our previous configuration class - BrowserSecurityConfig

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.formLogin()//Log in with forms
            .loginPage("/self-login.html")//Specify the page to log in to
            .and()
            .authorizeRequests()//And request authentication
        	//If you add a custom login page, you must release the custom login page request, otherwise there will be too many redirects
            .antMatchers("/self-login.html").permitAll()//The request for the login page does not require authentication
            .anyRequest()//Request for arbitrary
            .authenticated()//All need to be certified
}

If only the login page is specified and the login authentication is not released for the request for the login page, the following page exceptions will appear.

The HTML of the above login page is as follows:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Custom login page</title>
</head>
<body>
<form action="authentication/form" method="post">
    <table>
        <tr>
            <td>user name:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>password:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan="2"><button type="submit">Sign in</button></td>
        </tr>
    </table>
</form>
</body>
</html>

There is nothing strange about simple user name and password and simple login button. However, it should be noted that the submission request of our login form has changed. It is no longer the default login in spring security, but authentication/form. Therefore, we need to let spring security know that our customized login request is not login, but authentication/form. The following configuration is required

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.formLogin()//Log in with forms
            .loginPage("/self-login.html")//Specify the page to log in to
            .loginProcessingUrl("/authentication/form")//Overwrite the request configuration in UsernamePasswordAuthenticationFilter, but it is UsernamePasswordAuthenticationFilter that finally handles the request
            .and()
            .authorizeRequests()//And request authentication
            .antMatchers("/self-login.html").permitAll()//The request for the login page does not require authentication
            .anyRequest()//Request for arbitrary
            .authenticated()//All need to be certified
            .and().csrf().disable();//Turn off csrf
}

The configuration of loginprocessing url is added here, and the cross domain protection from csrf is turned off (described later). For the request of this url, spring security will still call UsernamePasswordAuthenticationFilter for login authentication processing (described later in the user-defined Filter for login processing)

As for the configuration of loginPage, in consideration of the extensibility, the specified loginPage can be a request for controller access, which can be judged in the target controller according to the url of the jump. If it is a jump caused by the end of html, it can be redirected to the user-defined login page. If it is a jump caused by a Restful style request, Relevant response message can be returned directly. The following is an implementation reference

/**
 * autor:liman
 * createtime:2021/7/9
 * comment:
 */
@RestController
@Slf4j
public class SecurityHandlerController {

    //The cache object that raised the request
    private RequestCache requestCache = new HttpSessionRequestCache();

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

    @Autowired
    private SecurityProperties securityProperties;

    /**
     * When you need identity authentication, jump here
     * @param request
     * @param response
     * @return
     */
    @RequestMapping("/authentication/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public BaseResponse authenticationHandle(HttpServletRequest request, HttpServletResponse response) throws IOException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if(null!=savedRequest){
            String target = savedRequest.getRedirectUrl();
            log.info("The request that caused the jump is:{}",target);
            if(StringUtils.endsWithIgnoreCase(target,".html")){
                //If the user configures the login page, it will jump to the login page. If not configured, it will return 401, unauthenticated status code
                String loginPage = securityProperties.getBrowser().getLoginPage();
                redirectStrategy.sendRedirect(request,response,loginPage);
            }
        }
        return new BaseResponse(StatusCode.NEED_LOGIN);
    }
}

Custom login successful processing

Spring security user-defined login processing is also relatively easy. You only need to simply implement the AuthenticationSuccessHandler interface

/**
 * autor:liman
 * createtime:2021/7/10
 * comment: Custom login successful processor
 */
@Component("selfAuthenticationSuccessHandler")
@Slf4j
public class SelfAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("Custom login successful processor");
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(objectMapper.writeValueAsString(authentication));//Write authentication to the front end as json

    }
}

After that, the successfully logged in processor is handed over to spring security management

http.formLogin()//Log in with forms
        .loginPage("/authentication/require")//Specify the page to log in to
        .loginProcessingUrl("/authentication/form")//Overwrite the request configuration in UsernamePasswordAuthenticationFilter, but it is UsernamePasswordAuthenticationFilter that finally handles the request
        .successHandler(selfAuthenticationSuccessHandler)//Custom login successful processor
        .and()
        .authorizeRequests()//And request authentication
        .antMatchers("/authentication/require",securityProperties.getBrowser().getLoginPage()).permitAll()//The request for the login page does not require authentication
        .anyRequest()//Request for arbitrary
        .authenticated()//All need to be certified
        .and().csrf().disable();//Turn off csrf

The third parameter contains the parameters of the login request and the relevant user information returned after login. Here, the authentication information is returned to the front end as json. You can see the following information through the debugging tool

The principal is the returned UserDetail information. If a third-party login (QQ login or wechat login) is used, the authentication information will be different

Custom login failure handling

The processing of user-defined login failure is similar to that of user-defined login success. Directly implement the AuthenticationFailureHandler interface, and then inform spring security in the login configuration (the configuration information will not be posted here)

/**
 * autor:liman
 * createtime:2021/7/10
 * comment:Custom login failure handling
 */
@Component("selfAuthenticationFailureHandler")
@Slf4j
public class SelfAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response
            , AuthenticationException exception) throws IOException, ServletException {
        log.info("Custom authentication failed processor");
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception));//Write exception to the front end as json
    }
}

Start the system and get the following information through simple debugging

The stack information is also returned to the front end. We don't need to pay attention here. We just need to pay attention to message.

Custom login extension

In the above customized login success and failure processing, we implement the AuthenticationSuccessHandler and AuthenticationFailureHandler interfaces to implement the customized login success and failure processing logic. However, there is a default implementation in spring security - SimpleUrlAuthenticationFailureHandler. We can inherit this class, Make our login processing more flexible

Take the processing of successful login as an example

/**
 * autor:liman
 * createtime:2021/7/10
 * comment: Custom login successful processor
 */
@Component("selfAuthenticationSuccessHandler")
@Slf4j
public class SelfAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response
            , Authentication authentication) throws IOException, ServletException {
//        log.info("custom login success processor");
//        response.setContentType("application/json;charset=utf-8");
//        response.getWriter().write(objectMapper.writeValueAsString(authentication));// Write authentication to the front end as json

        log.info("Custom login successful processor");
        if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {//If the configured login method is json, return
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(objectMapper.writeValueAsString(authentication));//Write authentication to the front end as json
        }else{//If it is not the login return method of json, it will call the parent class to jump (which is why it inherits SimpleUrlAuthenticationSuccessHandler)
            super.onAuthenticationSuccess(request,response,authentication);
        }
    }
}

We can let developers who use our login module configure whether the processing logic for successful login returns to json or jumps to the page accessed before login, which can be configured

summary

This paper briefly summarizes some contents of form based login. For the source code, please refer to the following github—— my 2021_learn_project_source_code . The module starting with spring security is the code of this series. The next blog will summarize the problems related to verification code login in spring security.

Topics: Java Spring Spring Security