Spring Boot + Spring Security for automatic login

Keywords: Programming Spring Vue Java SpringBoot

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

Many websites will see similar options when we log in. After all, it's a very troublesome thing to 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 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, we will take a look at how to implement this function in Spring Security.

This is the eighth article in the Spring Security series that SongGe recently published. Reading the previous articles in this series can better understand this article (if you are interested in the Spring Security video recorded by SongGe, you can also see here: SpringBoot+Vue + micro HR video tutorial):

  1. Dig a big hole and start Spring Security!
  2. Song Ge takes you to Spring Security by hand. Don't ask how to decrypt the password again
  3. How to customize form login in Spring Security
  4. Spring Security does front and back separation, let's not do page Jump! All JSON interactions
  5. The authorization operation in Spring Security is so simple
  6. How does Spring Security store user data in the database?
  7. Spring Security+Spring Data Jpa join hands, security management is only simpler!

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

1. Actual 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, only one. rememberMe() needs to be added here, and the automatic login function has been added successfully.

Next, we randomly add a test interface:

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

Restart the project and visit 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 input the user name and password, check the remember me box, and then click the login button to perform the login operation:

As you can see, in addition to username and password, there is also a remember me in the login data. The reason why I want to 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 the login is successful, it will automatically jump to the hello interface. We note that when the system accesses the hello interface, it carries cookie s:

You will notice that there is an additional member me here, which is the core of the implementation here. I'll explain this member me later. Let's test the effect first.

Next, we close the browser and reopen it. Normally, the browser is closed and reopened. If you need to access the hello interface again, you need to log in again. But at this time, we go to the hello interface again, and find that it can be accessed directly without re login, which means that our remember me configuration has taken effect (that is, the next automatic login function takes effect).

2. Principle analysis

It is reasonable to say that when the browser is closed and reopened, it is necessary to log in again. Now it does not have to wait. How does this function come true?

First of all, let's analyze the remamber me in the cookie. The value is a Base64 transcoded string. We can use some online tools on the Internet 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 is as follows:

s = javaboy:1589104055373:2578ffbc26485c534a2ef929ac2efc47

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

  1. The first paragraph is the user name, which does not need to be questioned.
  2. The second paragraph seems to be a timestamp. We found it is a data in two weeks after parsing through online tools or Java code.
  3. In the third paragraph, I don't care. This is the value calculated by MD5 hash function. Its clear text format is username + ":" + tokenExpiryTime + ":" + password + ":" + key. The last key is a hash salt value, which can be used to prevent token modification.

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

After the browser is closed and reopened, the user will visit the hello interface again. At this time, the user will carry the remember me in the cookie to the server. After the service gets the value, the user name and expiration time can be calculated conveniently. Then the user password can be queried according to the user name, and then MD5 can be used The hash function calculates the hash value, and then compares the calculated hash value with the hash value passed by the browser to confirm whether the token is valid.

Process is such a process. Next, we analyze the source code to verify whether the process is right or not.

3. Source code analysis

Next, we use the source code to verify what we said above.

This article mainly introduces two aspects, one is the token generation process of remember me, the other is its parsing process.

3.1 generation

The generated core processing methods 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 well understood:

  1. First, extract the user name / password from the successful Authentication.
  2. Since the password may be erased after successful login, if the password is not obtained at the beginning, reload the user from UserDetailsService and get the password again.
  3. Next, get the validity period of the token, which is two weeks by default.
  4. Next, call the makeTokenSignature method to calculate the hash value. In fact, the hash value is calculated according to the username, token validity, password and key. If we do not set the key ourselves, it is set in the remembermeconfigurer ා getKey method by default. Its value is a UUID string.
  5. Finally, the user name, the token validity period and the calculated hash value are put into the Cookie.

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

Since we haven't set the key ourselves, the default value of the key is a UUID string, which will cause a problem, that is, if the server restarts, the key will change, which will result in the invalidation of all the previously distributed remember me automatic login tokens. 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 have configured the key, you can still access the hello interface even if the server is restarted and the browser is opened and closed.

This is the process of remamber me token generation. As for how to get to the onLoginSuccess method, you can refer to song GE's previous article: Song Ge takes you through the Spring Security login process . Here, I would like to remind you some ideas:

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

3.2 analysis

What about the authentication process when the user turns off and opens the browser and accesses the / hello interface again?

As we have said before, a series of functions in Spring Security are implemented through a filter chain. Of course, remember me is no exception.

Spring Security provides the RememberMeAuthenticationFilter class to do related things. Let's 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's here.

The key point of this method is that if you can't get the current login user instance from the SecurityContextHolder, then call the remombermeservices.autologin logic logic to log in. 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, I call the processAutoLoginCookie method for verification. I will not paste the code of processAutoLoginCookie method. The core process is to first obtain the user name and expiration time, then query the user password according to the user name, and then use MD5 The hash function calculates the hash value, and then compares the hash value obtained 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.

4. Summary

After reading the above article, you may have found that if we turn on the RememberMe function, the core thing is the token placed in the cookie, which breaks the session limit. Even if the server is restarted and the browser is closed and reopened, the data can be accessed only if the token does not expire.

Once the token is lost, others can log into our system at will with this token, 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 reduce the security risk to the minimum through technology.

So how can we make our remember me function more secure? In the next article, song Ge will continue to share with you the persistent token scheme.

If you think you understand me, you may as well give me some encouragement

WeChat official account for a little rain in the south, reply to 2TB, get super 2TB Java learning resources. In reply to 2020, get the index of Spring Boot+Vue front and back separate development articles.

Posted by mentalist on Tue, 28 Apr 2020 19:02:09 -0700