An article takes you to get SpringBoot and SpringSecurity to achieve automatic login function

Automatic login is a very common function when we are developing software. For example, we log in to QQ mailbox:

Insert picture description here
On many websites, we will see similar options when logging in. After all, it is very troublesome to always let users enter their username and password.

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

As a common feature, our Spring Security must also provide corresponding support. In this article, we will look at how to implement this feature in Spring Security.

1. Join remember-me

For configuration convenience, just add two dependencies:

Insert picture description here
Add the following code to the configuration class:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    @Bean
    PasswordEncoder passwordEncoder(){
    
    
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.inMemoryAuthentication()
                .withUser("yolo")
                .password("123").roles("admin");
    }

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

We can see that here just add a .rememberMe()can, automatic login feature is added successfully entered.

Next, we randomly add a test interface:

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

Insert picture description here
At this time, everyone found that the default login page has one more option, which is to remember me. We enter the username and password, check the Remember me box, and then click the login button to perform the login operation.

As you can see, in the login data, in addition to username and password, there is also a remember-me. The reason for showing this is to tell everyone if you need to customize the login page, what about the key of RememberMe option? write.

After the login is successful, it will automatically jump to the hello interface. We note that when the system accesses the hello interface, the cookie carried:

Insert picture description here
It was noted here one more remember-me, which is the core implemented here, on this remember-meI will explain, let's test results.

Next, we close the browser, and then reopen the browser. Under normal circumstances, the browser is closed and reopened. If we need to access the hello interface again, we need to log in again. But at this time, we go to visit the hello interface again and find that we can access it directly without logging in again. This shows that our RememberMe configuration has taken effect (that is, the automatic login function will take effect next time).

2. Principle analysis

It stands to reason that if the browser is closed and then reopened, it is necessary to log in again. Now there is no need to wait. So how does this function work?

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


    @Test
    void contextLoads() {
    
    
        String s = new String(
                Base64.getDecoder().decode("eW9sbzoxNjAxNDczNTY2NTA1OjlmMGY5YjBjOTAzYmNjYmU3ZjMwYWM0NjVlZjEzNmQ5"));
        System.out.println("s = " + s);
    }

Execute this code, the output is as follows:

s = yolo:1601473566505:9f0f9b0c903bccbe7f30ac465ef136d9

It can be seen this fact with Base64 string :separated into three parts:

(1) The first paragraph is the user name, this need not be questioned.
(2) The second paragraph seems to be a timestamp. After analyzing it through online tools or Java code, we found that this is a data two weeks later.
(3) In the third paragraph, I won’t sell it. This is the value calculated using the MD5 hash function. Its plaintext format is
username + ":" + tokenExpiryTime + ":" + password + ":" + keythat the last key is a hash salt value, which can be used to prevent the token from being modified.

Learned a cookie remember-meafter meaning, then we have to remember my login process and very easy to guess the.

After the browser is closed, and then reopened, users go to access the interface hello, then will carry the cookie remember-meto the server, after the service to get the value, you can easily calculate the user name and expiration date, and then according to the user The user’s password is retrieved by name, and then the hash value is calculated through the MD5 hash function, and then the calculated hash value is compared with the hash value passed by the browser to confirm whether the token is valid.

The process is such a process. Next, we will verify the correctness of this process by analyzing the source code.

Three, source code analysis

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

Here are mainly introduced from two aspects, one is the process of remember-me token generation, and the other is the process of parsing it.

1. Generate

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())));
}

(1) First, extract the user name/password from the Authentication for successful login.
(2) After the login is successful, the password may be erased. Therefore, if the password is not obtained at the beginning, reload the user from UserDetailsService and obtain the password again.
(3) Next, get the validity period of the token, which is two weeks by default.
(4) The next invoked makeTokenSignaturemethod to calculate a hash value, a hash value is actually calculated in accordance with the username, and the token is valid password, key. If we do not own to set this key, the default is RememberMeConfigurer#getKeyset methods, its value is a UUID string.
(5) Finally, put the user name, token validity period, and calculated hash value into the Cookie.

Regarding the fourth point, let me talk about it again.

Since we do not set their own key, key default value is a UUID string, it will bring a problem, if the server is restarted, the key becomes, thus leading to distribute out before all remember-meautomatic logon token is invalid, so, We can specify this key. The way to specify is as follows:

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

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

This is the process of remember-me token generation. As for how to get to the onLoginSuccess method, here can give you a little reminder:

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

2. Analysis

When the user turns off and then open the browser, re-access /hellothe interface, then the certification process is kind of how it?

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

Spring Security provides RememberMeAuthenticationFilterclasses designed to do related things, we look at RememberMeAuthenticationFilterthe doFiltermethod:

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);
 }
}

This method is the most critical part is that, if the SecurityContextHoldercan not get the user to log in the current instance, then invoke rememberMeServices.autoLoginlogic to log in, we 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;
}

You can see, this is to extract cookieinformation, and the cookieinformation is decoded, after decoding, and then call the processAutoLoginCookiemethod to do the check, processAutoLoginCookiethe code approach I will not put up, the core process is to first obtain a user name and expiration date, then the user name The user password is queried, and then the hash value is calculated through the MD5 hash function, and then the obtained hash value is compared with the hash value passed by the browser to confirm whether the token is valid, and then whether to log in effective.

Four, summary

Read the above article, it may have found that if we open the RememberMefunction, the most central thing is to put cookiein a token, and the token is breaking the sessionlimits, even if the server is restarted, even if the browser is closed and reopened , As long as the token has not expired, the data can be accessed.

Once the token is lost, others can take this token to log in to our system 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 reduce the security risks to a minimum through technology.

Guess you like

Origin blog.csdn.net/nanhuaibeian/article/details/108630616