[In-depth explanation of Spring Security (7)] Explain the implementation principle of RememberMe in detail

1. Basic usage of RememberMe

Let’s take a look at the default page effect changes for the simplest usage.

SecurityConfig configuration class

@EnableWebSecurity
public class SecurityConfig {
    
    


    @Bean
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(){
    
    
        return new InMemoryUserDetailsManager(
                User.withUsername("admin")
                        .password("{noop}123")
                        .roles("admin")
                        .build()
        );
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
    
    
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(inMemoryUserDetailsManager())
                .and()
                .build();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    
    
        return http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .and()
                .rememberMe()
                .and()
                .csrf()
                .disable().build();
    }

}

Test the TestController code

@RestController
public class TestController {
    
    

    @GetMapping("/test")
    public String test(){
    
    
        return "test";
    }

}

Following is the default login page given.

insert image description here
remember-meObserving the source code of the page, we can find that there is an additional checkbox option named as before RememberMe is not configured .

insert image description here
If we check it and log in successfully, when we close the current browser and reopen it, we can access the resources that the logged in user can access without having to log in again. In [Introduction to Spring Security (2)] The implementation principle of Spring Security, the editor lists the filters that come with Spring Security. Among them, there is one in the queue that is not loaded by default RememberMeAuthenticationFilter, which is used to process RememberMe login. Let's analyze the source code of RememberMeAuthenticationFilter to understand the implementation principle of RememberMe login.

Two, RememberMeAuthenticationFilter source code analysis

We know that RememberMeAuthenticationFilter is a filter, its core code is in doFilterthe method, and the next step is to analyze the source code of this method.

doFilterThe main implementation of the method can be divided into three steps:

  1. After the request reaches the filter, it first judges whether there is a value in the SecurityContextHolder. If there is no value, it means that the user has not logged in. At this time, call the autoLogin method to log in automatically.
  2. When the rememberMeAuth returned after the automatic login is not null, it means that the automatic login is successful. At this time, call the authenticate method to verify the key, and save the user information of the successful login to the SecurityContextHolder object, and then call the callback of the successful login, and Publish the successful login time. It should be noted that the callback for successful login does not include the loginSuccess method in RememberMeServices.
  3. If it fails, some login failure callbacks will be performed, and the failed log information will be printed.

It can be seen that its implementation depends on rememberMeServicesthe autoLogin method in , whether the login is successful or not determines the subsequent success and failure callbacks.

// RememberMeAuthenticationFilter 中的 doFilter 方法
	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
    
    
			/*
			1. 请求到达过滤器后,首先判断 SecurityContextHolder 中是否有值,
			没值的话表示用户尚未登录,此时调用 autoLogin 方法进行自动登录
*/
		if (SecurityContextHolder.getContext().getAuthentication() != null) {
    
    
			this.logger.debug(LogMessage
					.of(() -> "SecurityContextHolder not populated with remember-me token, as it already contained: '"
							+ SecurityContextHolder.getContext().getAuthentication() + "'"));
			chain.doFilter(request, response);
			return;
		}
		Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
		/*
		2. 当自动登录成功后返回的 rememberMeAuth 不为 null 时,
		表示自动登录成功,此时调用 authentication 方法对 key 进行校验,
		并且将登录成功的用户信息保存到 SecurityContextHolder 对象中,
		然后调用登录成功的回调,并发布登录成功时间。
		需要注意的是:登录成功的回调并不包含 RememberMeServices 中的 loginSuccess 方法。
		*/
		if (rememberMeAuth != null) {
    
    
			// Attempt authenticaton via AuthenticationManager
			try {
    
    
				rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
				SecurityContext context = SecurityContextHolder.createEmptyContext();
				context.setAuthentication(rememberMeAuth);
				SecurityContextHolder.setContext(context);
				onSuccessfulAuthentication(request, response, rememberMeAuth);
				// 这里打印了保存到 SecurityContextHolder 中的authentication日志信息
				this.logger.debug(LogMessage.of(() -> "SecurityContextHolder populated with remember-me token: '"
						+ SecurityContextHolder.getContext().getAuthentication() + "'"));
				this.securityContextRepository.saveContext(context, request, response);
				if (this.eventPublisher != null) {
    
    
					this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
							SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
				}
				if (this.successHandler != null) {
    
    
					this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
					return;
				}
			}
			catch (AuthenticationException ex) {
    
    
			/*
			3. 失败的话会进行一些登录失败的回调,
			打印失败的日志信息
*/
				this.logger.debug(LogMessage
						.format("SecurityContextHolder not populated with remember-me token, as AuthenticationManager "
								+ "rejected Authentication returned by RememberMeServices: '%s'; "
								+ "invalidating remember-me token", rememberMeAuth),
						ex);
				this.rememberMeServices.loginFail(request, response);
				onUnsuccessfulAuthentication(request, response, ex);
			}
		}
		chain.doFilter(request, response);
	}

RememberMeServices

RememberMeServices is an interface that defines three methods:

  • The autoLogin method can extract the required parameters from the request to complete the automatic login function;
  • The loginFail method is a callback for automatic login failure;
  • The loginSuccess method is a callback for automatic login success.

The following is the implementation class for RememberMeServices in Spring Security
insert image description here

TokenBasedRememberMeServices

TokenBasedRememberMeServices is the default implementation of Spring Security. Next, analyze the source code of its autoLogin. In fact, autoLogin is a method in AbstractRememberMeServices, and its subclasses have not been rewritten. Subclasses focus on overriding processAutoLoginCookiethe method.

First look at the autoLogin method implemented in AbstractRememberMeServices (the core method called internally will be analyzed below)

// AbstractRememberMeServices 中实现的 autoLogin 方法
	@Override
	public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
    
    
	// 从request 对象中取对应 remember-me 名的 cookie 值
		String rememberMeCookie = extractRememberMeCookie(request);
		// 如果不存在的话就返回空
		if (rememberMeCookie == null) {
    
    
			return null;
		}
		this.logger.debug("Remember-me cookie detected");
		// 或者说是一个空值也返回空
		if (rememberMeCookie.length() == 0) {
    
    
			this.logger.debug("Cookie was empty");
			cancelCookie(request, response);
			return null;
		}
		try {
    
    
		// 先进行base64解码
		// 然后对rememberMeCookie进行 :分割
			String[] cookieTokens = decodeCookie(rememberMeCookie);
			// 自动登录认证吧,算是
			// 去把用户名、密码、时长搞成MD5然后和cookie返回来的进行比对,认证过程在这个方法里
			UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
			this.userDetailsChecker.check(user);
			this.logger.debug("Remember-me cookie accepted");
			// 创建一个RememberMeAuthenticationToken即为认证数据源
			return createSuccessfulAuthentication(request, user);
		}
		cancelCookie(request, response);
		return null;
}

The source code of the method to obtain the cookie value corresponding to the remember-name name is as follows

insert image description here
Base64-decode the Cookie value, divide it into a string array and return it.

insert image description here

Implementation of the processAutoLoginCookie method in TokenBasedRememberMeServices

The implementation of the method in TokenBasedRememberMeServices processAutoLoginCookieis mainly used to verify whether the token information in the Cookie is legal (the CookieTokens mentioned below are string arrays that are decrypted with base64 and then split):

  1. First judge whether the length of CookieTokens is 3, and throw an exception if it is not 3.
  2. From the value subscripted as 1 in CookieTokens, that is, the expiration time, it is judged whether the token has expired, and if it has expired, an exception is thrown;
  3. Query the current user object according to the user name (CookieTokens subscript 0 value);
  4. Call the makeTokenSignature method to generate a signature. The signature generation process is as follows: first form 用户名、令牌过期时间、用户名密码以及keya string separated by " :", then encrypt the string with MD5the message digest algorithm, and convert the encrypted result into a string and return it.
  5. Judge whether the signature generated in step 4 is equal to the signature sent by Cookie (that is, the subscript of the cookieTokens array is the value of 2), if they are equal, it means that the token is legal, and the user object is returned directly, otherwise an exception is thrown.

The following is its source code, the small label number corresponds to the above analysis

	@Override
	protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
			HttpServletResponse response) {
    
    
			// 1
		if (cookieTokens.length != 3) {
    
    
			throw new InvalidCookieException(
					"Cookie token did not contain 3" + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
		}
		// 2
		long tokenExpiryTime = getTokenExpiryTime(cookieTokens);
		if (isTokenExpired(tokenExpiryTime)) {
    
    
			throw new InvalidCookieException("Cookie token[1] has expired (expired on '" + new Date(tokenExpiryTime)
					+ "'; current time is '" + new Date() + "')");
		}
		// 3
		UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);
		Assert.notNull(userDetails, () -> "UserDetailsService " + getUserDetailsService()
				+ " returned null for username " + cookieTokens[0] + ". " + "This is an interface contract violation");
		// 4
		String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),
				userDetails.getPassword());
			// 5
		if (!equals(expectedTokenSignature, cookieTokens[2])) {
    
    
			throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2]
					+ "' but expected '" + expectedTokenSignature + "'");
		}
		return userDetails;
	}

Summarize

When the user successfully logs in through the form of username/password, the system will calculate a signature based on the user's username, password and the expiration time of the token. This signature is generated using the MD5 message digest algorithm and is irreversible. Then splice the user name, token expiration time, and signature into a string, separated by " :", base64 encode the spliced ​​string, and then encapsulate the encoded string into a Cookie value and return it to The front end, which is the token we see in the browser. When the browser is closed and opened again, the token in the cookie will be automatically carried when accessing system resources. After the server gets the token in the cookie, it first performs Base64 decoding, and then extracts the three items of data in the token after decoding; According to the data in the token, it is judged whether the token has expired. If it has not expired, the user information is queried according to the user name in the token; then a signature is calculated and compared with the signature in the token. If it is consistent, it means that it will If the card is a legal token, the automatic login is successful, otherwise the automatic login fails.

insert image description here

Schematic

insert image description here

3. Improve security

Through the above source code analysis, we know that the successful login authentication will call the onLoginSuccess method in TokenBasedRememberMeServices to generate a cookie response to the browser (In fact, the corresponding Cookie value is the Base6
encoded value
), the browser will save it. When we close the browser, we can use the cookie contained in the request message to authenticate and access the resource. This login-free method is not safe. The cookie value related to remember-me is exposed to the outside, which is harmful to privacy. Is there any way to make it safe? The answer is that there is no absolute security, it can only be said to improve its security.

PersistentTokenBasedRememberMeServices

The onLoginSuccess and processAutoLoginCookie in PersistentTokenBasedRememberMeServices are different from TokenBasedRememberMeServices. Let's take a look at its specific implementation.

concrete implementation of onLoginSuccess

insert image description here

  1. First obtain the user name from the authentication data source, and then encapsulate a serial number and user name into a PersistentRememberMeToken data source;
  2. Put this data source into the tokenRepository warehouse, which is a memory-based warehouse;
  3. Finally, the cookie is created to respond to the browser. This cookie value is a value that is combined by series and token and then encoded by Base64.

ProcessAutlLoginCookie specific implementation

insert image description here

  1. Extract the series and token from the CookieTokens array, and then query a PersistentRememberMeToken object in the memory according to the series. If the queried object is null, it means that there is no value corresponding to series in the memory, and this login fails. If the queried token is different from the token parsed from CookieTokens, it means that the automatic login token has been leaked (the token in the memory will change after a malicious user logs in with the token), and all automatic login tokens of the current user will be removed at this time. Logs a record and throws an exception.
  2. Determine whether the token is expired according to the query results in the database, and throw an exception if it expires;
  3. Generate a new PersistentRememberMeToken object, the username and series remain unchanged, the token is regenerated, and the data also uses the current time. After newToken is generated, modify the token and data in the memory according to the series (that is, a new token and date will be generated after each automatic login)
  4. Call the addCookie method to add cookies. In the addCookie method, the essence is to call AbstractRememberMeServicesthe setCookie method in the parent class, but it should be noted that there are only two items in the first array parameter: series and token (that is, the token returned to the front end is passed to the series and token base64 encoded)
  5. Finally, the user object will be queried based on the username and returned.

Memory Token Login Test

Security configuration class

@EnableWebSecurity
public class SecurityConfig {
    
    


    @Bean
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(){
    
    
        return new InMemoryUserDetailsManager(
                User.withUsername("admin")
                        .password("{noop}123")
                        .roles("admin")
                        .build()
        );
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
    
    
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(inMemoryUserDetailsManager())
                .and()
                .build();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    
    
        return http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .and()
                .rememberMe()
                // .rememberMeParameter("rememberMe")
                .rememberMeCookieName("rememberMe")
                .rememberMeServices(rememberMeServices())
                .and()
                .csrf()
                .disable().build();
    }

    @Bean
    public RememberMeServices rememberMeServices(){
    
    
        return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),
                inMemoryUserDetailsManager(),
                new InMemoryTokenRepositoryImpl());
    }

}

Test results: Open a browser for login authentication. After the authentication is successful, it carries the remember-me Cookie. Reopen a browser and carry this cookie to access the resources that need to be authenticated by the server. Then close the browser that was just started, and then open it again. When accessing the same resource, 403 will be redirected to the login page and the server will report an exception (the token in the memory is gone at this time), and the report is CookieTheftException (cookie has been stolen).

Note: Refresh does not report an exception and you can access resources because there is a SessionID, which can also be authenticated on the server side, so you need to close the browser so that the SessionID does not match (the corresponding Session on the server side will naturally be destroyed due to timeout) and then proceed test.

4. Persistence of Token Database

The warehouse for storing tokens in PersistentTokenBasedRememberMeServices PersistentTokenRepositoryis implemented based on memory by default (that is InMemoryTokenRepositoryImpl), there will be a problem with this method. When the server restarts the application, the browser user needs to log in again, because the memory must be There is no corresponding token anymore.

In addition to providing the implementation of InMemoryTokenRepositoryImpl, Spring Security also provides JdbcTokenRepositoryImplthe implementation of PersistentTokenRepository. The token will be stored in the database, so there is no need to worry about the problems described above.

In JdbcTokenRepositoryImpl, the required SQL has been written for us, and the implementation of adding, deleting, modifying and checking tokens has provided us with support for interacting with the database.

insert image description here

Test a wave, the following is the configuration code

@EnableWebSecurity
public class SecurityConfig {
    
    


    @Bean
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(){
    
    
        return new InMemoryUserDetailsManager(
                User.withUsername("admin")
                        .password("{noop}123")
                        .roles("admin")
                        .build()
        );
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
    
    
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(inMemoryUserDetailsManager())
                .and()
                .build();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    
    
        return http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .and()
                .rememberMe()
                // .rememberMeParameter("rememberMe")
                .rememberMeCookieName("remember-me")
                // .rememberMeServices(rememberMeServices())
                .tokenRepository(persistentTokenRepository())
                .and()
                .csrf()
                .disable()
                .build();
    }

    @Resource
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
    
    
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 设置为true要保障数据库该表不存在,不然会报异常哦
        // 所以第二次打开服务器应用程序的时候得把它设为false
        tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

}

When the server application is started, the table is automatically created for us, as shown in the following figure

insert image description here
After authentication, new data will be added to the database table

insert image description here

V. Summary

  • The essence of RememberMe is to respond the token to the browser in the form of a cookie, and then the browser stores it locally. When the resource is accessed again next time, it will take the token and send it to the server together with the request. The token will make Automatic login (ie user authentication) is performed in the background.
  • When we customize the SecurityFilterChain security filter chain, if rememberMe() is configured, it RememberMeAuthenticationFilterwill be a member of the filter chain.
  • Explain the process (the most important class to implement this function is RememberMeServicesthe first authentication response and automatic login authentication, which are the protagonists): In the absence of custom filters, UsernamePasswordAuthenticationFilter is generally used for user authentication. When clicking When the checkbox button and user information are requested to the server, the UsernamePasswordAuthenticationFilter is used first to complete the authentication. If the authentication is successful, the method in RememberMeServices will be called onLoginSuccess(this depends on the specific implementation), and the token information already exists in the response (ie cookies). When the browser is closed or the Session expires, when accessing resources that require authentication on the server side, the request message will carry this cookie to the server side, and will enter the filter, which will call the method in RememberMeServices to automatically log RememberMeAuthenticationFilterin autoLogin. autoLogin During the automatic login process, processAutoLoginCookiethe method will be called for token authentication (this method depends on the implementation class of RememberMeServices). After the autoLogin method is executed and the authentication is successful, the user data source information is returned, and then some encapsulated data information is sent to the SecurityContextHolder, etc. some operations.
  • RememberMeServices is the core class. Spring Security provides two implementation classes. One is that TokenBasedRememberMeServicesthis method is to base64 encode the user name, timeout time, password and other information, that is, to send the token composed of some operations to the server. The verification token is Anti-encode the data sent from the browser to see if it is consistent. This method is very low in security; another implementation is PersistentTokenBasedRememberMeServicesthat it relies on PersistentTokenRepositorythe warehouse internally and provides a memory-based and Jdbc-based implementation, which is more efficient than the previous one. It is safe because it will update the token (ie Cookie) after each automatic authentication. If the token is found to be inconsistent during the automatic authentication process, it will be deleted in time (that is, deleted from the PersistentTokenRepository warehouse), and the Cookie will be reported as being stolen.
  • PersistentTokenBasedRememberMeServices is the same as TokenBasedRememberMeServices in that it also uses Base64 encoding to set the token, and it also needs to be decoded when automatically logging in to the authentication token; the difference is that it is composed of two data (serial number (fixed), token (will vary)), while TokenBasedRememberMeServices is three. It can't be said that the serial number is fixed, that is to say, it is onLoginSuccessgenerated in the method, and then stored in the browser. processAutoLoginCookieThe serial number will not be changed in the method, only the token will be changed. It can be said that the serial number decoded by the browser Cookie The number is fixed (without re-login verification).
  • It is very simple to use (PersistentTokenBasedRememberMeServices is generally used, and the warehouse implementation used is JdbcTokenRepositoryImpl), when configuring rememberMeConfigurer,Just configure a tokenRepository (PersistentTokenRepository), it will automatically configure rememberMeServices for us, because Spring Security implements both. If you configure PersistentTokenRepository, you will use PersistentTokenBasedRememberMeServices by default.

Guess you like

Origin blog.csdn.net/qq_63691275/article/details/131106726