Spring Boot+Spring Security realize automatic login function (actual combat + source code analysis)

Automatic login is a very common function in software development. For example, we log in to QQ email:

In many websites, we will see similar options when logging in. After all, it is very troublesome to always let users enter their user name and password.

The automatic login function is that after the user logs in successfully, if the user closes the browser and reopens it within a certain period of time, or the server restarts, the user does not need to log in again, and the user can still directly access the interface data.

As a common function, our Spring Security must also provide corresponding support. In this article, let's see how to implement this function in Spring Security.

This function is simple to implement, but it will still involve many details, so I will introduce it one by one in two articles. This is the first article.

1. Actual combat code

First of all, to realize the function of remembering me, you only need to add the following code in the configuration of Spring Security:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .rememberMe()
            .and()
            .csrf().disable();
}

As you can see, we only need to add one here rememberMe(), and the automatic login function is successfully added.

Next, we add a test interface at will:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

Restart the project, and we will access the hello interface. At this time, we will automatically jump to the login page:

At this time, we found that the default login page has one more option, that is, remember me. We enter the user name and password, check the remember me box, and then click the login button to perform the login operation:

You can see that in the login data, in addition to username and password, there is also a remember me. The reason why I show you this is to tell you how to write the key of the remember me option if you need to customize the login page.

After successful login, it will automatically jump to the hello interface. Note that when the system accesses the hello interface, the following cookie s are carried:

You have noticed that there is a more remember me here, which is the core of the implementation here. I will explain this remember me later. Let's test the effect first.

Next, we close the browser and reopen the browser. Normally, the browser closes and reopens. If we need to access the hello interface again, we need to log in again. However, at this time, we visit the hello interface and find that we can access it directly without logging in again, which indicates that our memberme configuration has taken effect (that is, the next automatic login function has taken effect).

2. Principle analysis

It is reasonable to say that if the browser is closed and reopened, you have to log in again. Now you don't have to wait, so how does this function come true?

First, let's analyze the extra remember me in the cookie. This value is a Base64 transcoded string. We can use some online tools to decode it. We can simply write two lines of code to decode it:

@Test
void contextLoads() throws UnsupportedEncodingException {
    String s = new String(Base64.getDecoder().decode("amF2YWJveToxNTg5MTA0MDU1MzczOjI1NzhmZmJjMjY0ODVjNTM0YTJlZjkyOWFjMmVmYzQ3"), "UTF-8");
    System.out.println("s = " + s);
}

Execute this code, and the output results are as follows:

s = javaboy:1589104055373:2578ffbc26485c534a2ef929ac2efc47

You can see that this Base64 string is actually separated by:, and is divided into three parts:

  1. The first paragraph is the user name, which needs no doubt.
  2. The second paragraph looks like a timestamp. We found that this is the data after two weeks through online tools or Java code analysis.
  3. In the third paragraph, I won't show off. This is the value calculated by using the MD5 hash function. Its plaintext format is username + ":" + tokenExpiryTime + ":" + password + ":" + key. The last key is a hash salt value, which can be used to prevent the token from being modified.

After understanding the meaning of remember me in cookie s, we can easily guess the process of remembering my login.

After the browser is closed and re opened, the user will access the hello interface. At this time, the user will carry the remember me in the cookie to the server. After the service gets the value, it can easily calculate the user name and expiration time, query the user password according to the user name, and then calculate the hash value through the MD5 hash function, Then compare the calculated hash value with the hash value passed by the browser to confirm whether the token is valid.

The process is such a process. Next, we analyze the source code to verify whether the process is correct.

3. Source code analysis

Next, we verify the correctness of what we said above through the source code.

Here we mainly introduce it from two aspects: one is the process of remember me token generation, and the other is the process of its parsing.

3.1 generation

The core processing methods generated are:
TokenBasedRememberMeServices#onLoginSuccess:

@Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
  Authentication successfulAuthentication) {
 String username = retrieveUserName(successfulAuthentication);
 String password = retrievePassword(successfulAuthentication);
 if (!StringUtils.hasLength(password)) {
  UserDetails user = getUserDetailsService().loadUserByUsername(username);
  password = user.getPassword();
 }
 int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
 long expiryTime = System.currentTimeMillis();
 expiryTime += 1000L * (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime);
 String signatureValue = makeTokenSignature(expiryTime, username, password);
 setCookie(new String[] { username, Long.toString(expiryTime), signatureValue },
   tokenLifetime, request, response);
}
protected String makeTokenSignature(long tokenExpiryTime, String username,
  String password) {
 String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
 MessageDigest digest;
 digest = MessageDigest.getInstance("MD5");
 return new String(Hex.encode(digest.digest(data.getBytes())));
}

The logic of this method is actually well understood:

  1. First, extract the user name / password from the successfully logged in Authentication.
  2. Since the password may be erased after successful login, if you don't get the password at first, reload the user from UserDetailsService and obtain the password again.
  3. Next, get the validity period of the token. The default validity period of the token is two weeks.
  4. Next, call the makeTokenSignature method to calculate the hash value. In fact, it calculates a hash value according to username, token validity, password and key. If we do not set this key ourselves, it is set in the membermeconfigurer #getkey method by default, and its value is a UUID string.
  5. Finally, put the user name, token validity and calculated hash value into the Cookie.

On the fourth point, I'll say it again here.

Since we do not set the key ourselves, the default value of the key is a UUID string, which will cause a problem. If the server restarts, the key will change, which will invalidate all the remember me automatic login tokens previously distributed. Therefore, we can specify the key. Specify as follows:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .rememberMe()
            .key("javaboy")
            .and()
            .csrf().disable();
}

If you configure the key, "even if the server is restarted, even if the browser is opened and then closed", you can still access the hello interface.

This is the process of remember me token generation. As for how to get to the onLoginSuccess method, you can refer to brother song's previous article: brother song takes you through the Spring Security login process. Here's a little reminder:


AbstractAuthenticationProcessingFilter#doFilter -> AbstractAuthenticationProcessingFilter#successfulAuthentication -> AbstractRememberMeServices#loginSuccess -> TokenBasedRememberMeServices#onLoginSuccess.

3.2 analysis

So what is the authentication process when the user closes and opens the browser and revisits the / hello interface?

As we said before, a series of functions in Spring Security are implemented through a filter chain. Of course, the function of RememberMe is no exception.

Spring Security provides
The RememberMeAuthenticationFilter class is specifically used to do related things. Let's take a look at the doFilter method of RememberMeAuthenticationFilter:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
  throws IOException, ServletException {
 HttpServletRequest request = (HttpServletRequest) req;
 HttpServletResponse response = (HttpServletResponse) res;
 if (SecurityContextHolder.getContext().getAuthentication() == null) {
  Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
    response);
  if (rememberMeAuth != null) {
    rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
    SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
    onSuccessfulAuthentication(request, response, rememberMeAuth);
    if (this.eventPublisher != null) {
     eventPublisher
       .publishEvent(new InteractiveAuthenticationSuccessEvent(
         SecurityContextHolder.getContext()
           .getAuthentication(), this.getClass()));
    }
    if (successHandler != null) {
     successHandler.onAuthenticationSuccess(request, response,
       rememberMeAuth);
     return;
    }
   }
  chain.doFilter(request, response);
 }
 else {
  chain.doFilter(request, response);
 }
}

As you can see, it is implemented here.

The key point of this method is that if the currently logged in user instance cannot be obtained from the SecurityContextHolder, it will be called
rememberMeServices. Log in with autologin logic. Let's look at this method:

public final Authentication autoLogin(HttpServletRequest request,
  HttpServletResponse response) {
 String rememberMeCookie = extractRememberMeCookie(request);
 if (rememberMeCookie == null) {
  return null;
 }
 logger.debug("Remember-me cookie detected");
 if (rememberMeCookie.length() == 0) {
  logger.debug("Cookie was empty");
  cancelCookie(request, response);
  return null;
 }
 UserDetails user = null;
 try {
  String[] cookieTokens = decodeCookie(rememberMeCookie);
  user = processAutoLoginCookie(cookieTokens, request, response);
  userDetailsChecker.check(user);
  logger.debug("Remember-me cookie accepted");
  return createSuccessfulAuthentication(request, user);
 }
 catch (CookieTheftException cte) {
  
  throw cte;
 }
 cancelCookie(request, response);
 return null;
}

As you can see, here is to extract the cookie information and decode the cookie information. After decoding, call the processAutoLoginCookie method for verification. I won't post the code of the processAutoLoginCookie method. The core process is to first obtain the user name and expiration time, and then query the user password according to the user name, Then calculate the hash value through the MD5 hash function, and then compare the obtained hash value with the hash value passed by the browser to confirm whether the token is valid, and then confirm whether the login is valid.

Well, I've also roughly sorted out the process here according to everyone.

4. Summary

After reading the above article, you may have found that if we enable the RememberMe function, the most core thing is the token placed in the cookie. This token breaks through the session limit. Even if the server is restarted and the browser is closed and reopened, you can access the data as long as the token does not expire.

Once the token is lost, others can log in to our system with this token at will, which is a very dangerous operation.

But in fact, this is a paradox. In order to improve the user experience (less login), our system inevitably leads to some security problems, but we can minimize the security risk through technology.

Keywords: Java Spring Spring Boot Programmer architecture

Added by abhi_10_20 on Wed, 05 Jan 2022 10:22:17 +0200