Spring Security automatically kicks off the previous logged-in user, a configuration is complete!

After successful login, automatically kick off the previous login user. Songge first saw this feature inside the button. It was fun at that time.

Once you've done your own development, you've met the same requirements. Just as the recent Spring Security series is being serialized, you can use Spring Security to talk about how this function works.

This is the thirteenth article in this series. Reading the previous articles will help you to better understand this article:

  1. Dig a big hole and start Spring Security!
  2. Pingo takes you to Spring Security by hand. Don't ask me how to decrypt the password anymore
  3. Hand-on instructions for customizing form logins in Spring Security
  4. Spring Security does front-end separation, let's not do page jumps!Unified JSON Interaction
  5. Authorization in Spring Security was so simple
  6. How does Spring Security store user data in the database?
  7. Spring Security+Spring Data Jpa join forces, so security management is only simpler!
  8. Spring Boot + Spring Security for automatic login
  9. Spring Boot automatic login, how to control security risk?
  10. Where is Spring Security better than Shiro in a microservice project?
  11. Two ways SpringSecurity customizes authentication logic (advanced gameplay)
  12. How can I quickly view information like login user IP address in Spring Security?

1. Demand Analysis

In the same system, we may only allow one user to log on to one terminal. Generally, this may be for security reasons, but there are also some cases for business reasons. The business reason that Songo encountered before is that a user can only log on to one device.

There are two ways to realize that a user cannot log on to two devices at the same time:

  • Later logins automatically kick off previous logins, just like what you see in the buttons.
  • Subsequent logins are not allowed if the user is already logged in.

This idea can achieve this function, which one to use depends on our specific needs.

In Spring Security, both are implemented well, and a single configuration is available.

2. Specific implementation

2.1 Kick logged-in users

To kick out the old login with the new login, we only need to set the maximum number of sessions to 1, which is configured as follows:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login.html")
            .permitAll()
            .and()
            .csrf().disable()
            .sessionManagement()
            .maximumSessions(1);
}

maximumSessions indicates that the maximum number of sessions is configured to be 1, so subsequent logins will automatically kick out previous ones.The other configurations here are described in the previous article, and I will not repeat them. At the end of the article, you can download the complete case code.

When the configuration is complete, test with two browsers, Chrome and Firefox (or use the multiuser functionality in Chrome).

  1. After successful login on Chrome, access/hello interface.
  2. After successful login on Firefox, access the / hello interface.
  3. Access the / hello interface again on Chrome and you'll see the following prompts:
This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).

As you can see, this session is said to be out of date because the same user is used for concurrent login.

2.2 Disable new logins

If the same user is already logged in and you don't want to kick him, but you want to disable the new logon operation, that's fine. Configure as follows:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login.html")
            .permitAll()
            .and()
            .csrf().disable()
            .sessionManagement()
            .maximumSessions(1)
            .maxSessionsPreventsLogin(true);
}

Add the maxSessionsPreventsLogin configuration.When one browser logs in successfully, the other browser will no longer be able to log in.

Is it simple?

But not yet, we need to provide another Bean:

@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}

Why add this Bean?Because in Spring Security, it cleans up session records in time by listening for session destruction events.Users log in from different browsers and have sessions. When a user logs out, sessions fail, but the default failure is achieved by calling the StandardSession#invalidate method. This failure event cannot be perceived by the Spring container, resulting in Spring Security after the user logs out.The session information table was not cleaned up in time to assume that the user is still online, which prevents the user from logging in again (small partners can try not to add the Bean above on their own and have the user log out before logging in again).

To solve this problem, we provide an HttpSessionEventPublisher, which implements the HttpSessionListener interface, in which events created and destroyed by session s can be sensed in time, and event mechanisms in Spring can be invoked to publish related creation and destruction events, which can then be published by Spring Security Perceptively, some of the sources for this class are as follows:

public void sessionCreated(HttpSessionEvent event) {
	HttpSessionCreatedEvent e = new HttpSessionCreatedEvent(event.getSession());
	getContext(event.getSession().getServletContext()).publishEvent(e);
}
public void sessionDestroyed(HttpSessionEvent event) {
	HttpSessionDestroyedEvent e = new HttpSessionDestroyedEvent(event.getSession());
	getContext(event.getSession().getServletContext()).publishEvent(e);
}

OK, although there is one more configuration, it is still very simple!

3. Implementation Principle

How does this function work in Spring Security?Let's analyze the source code a little bit.

First of all, we know that the UsernamePasswordAuthenticationFilter passes through the user login process (Reference: Songge takes you through the Spring Security login process ), while the call to the filter method in UsernamePasswordAuthenticationFilter is triggered in AbstractAuthenticationProcessingFilter, let's take a look at the call to the AbstractAuthenticationProcessingFilter#doFilter method:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
		throws IOException, ServletException {
	HttpServletRequest request = (HttpServletRequest) req;
	HttpServletResponse response = (HttpServletResponse) res;
	if (!requiresAuthentication(request, response)) {
		chain.doFilter(request, response);
		return;
	}
	Authentication authResult;
	try {
		authResult = attemptAuthentication(request, response);
		if (authResult == null) {
			return;
		}
		sessionStrategy.onAuthentication(authResult, request, response);
	}
	catch (InternalAuthenticationServiceException failed) {
		unsuccessfulAuthentication(request, response, failed);
		return;
	}
	catch (AuthenticationException failed) {
		unsuccessfulAuthentication(request, response, failed);
		return;
	}
	// Authentication success
	if (continueChainBeforeSuccessfulAuthentication) {
		chain.doFilter(request, response);
	}
	successfulAuthentication(request, response, chain, authResult);

In this code, we can see that after calling the attemptAuthentication method and going through the authentication process, the next step is to call the sessionStrategy.onAuthentication method, which is used to handle the concurrency problem of sessions.Specifically:

public class ConcurrentSessionControlAuthenticationStrategy implements
		MessageSourceAware, SessionAuthenticationStrategy {
	public void onAuthentication(Authentication authentication,
			HttpServletRequest request, HttpServletResponse response) {

		final List<sessioninformation> sessions = sessionRegistry.getAllSessions(
				authentication.getPrincipal(), false);

		int sessionCount = sessions.size();
		int allowedSessions = getMaximumSessionsForThisUser(authentication);

		if (sessionCount &lt; allowedSessions) {
			// They haven't got too many login sessions running at present
			return;
		}

		if (allowedSessions == -1) {
			// We permit unlimited logins
			return;
		}

		if (sessionCount == allowedSessions) {
			HttpSession session = request.getSession(false);

			if (session != null) {
				// Only permit it though if this request is associated with one of the
				// already registered sessions
				for (SessionInformation si : sessions) {
					if (si.getSessionId().equals(session.getId())) {
						return;
					}
				}
			}
			// If the session is null, a new one will be created by the parent class,
			// exceeding the allowed number
		}

		allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);
	}
	protected void allowableSessionsExceeded(List<sessioninformation> sessions,
			int allowableSessions, SessionRegistry registry)
			throws SessionAuthenticationException {
		if (exceptionIfMaximumExceeded || (sessions == null)) {
			throw new SessionAuthenticationException(messages.getMessage(
					"ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
					new Object[] {allowableSessions},
					"Maximum sessions of {0} for this principal exceeded"));
		}

		// Determine least recently used sessions, and mark them for invalidation
		sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
		int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
		List<sessioninformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
		for (SessionInformation session: sessionsToBeExpired) {
			session.expireNow();
		}
	}
}

Let me explain a little bit about this core code:

  1. First call the sessionRegistry.getAllSessions method to get all sessions of the current user. When called, this method passes two parameters, one is the authentication of the current user and the other is the false parameter to indicate that it does not contain an expired session (after the user logs in successfully, the user's session ID is saved with the key Is the principal of the user, and value is a collection of sessionid s corresponding to that topic.
  2. Next, we calculate that the current user already has several valid sessions and get the number of sessions that are allowed to concurrently.
  3. If the current number of sessions (sessionCount) is less than the number of sessions concurrently (allowedSessions), no processing is done; if the allowedSessions value is -1, there is no limit on the number of sessions.
  4. If the current number of sessions (sessionCount) equals the number of sessions concurrently (allowedSessions), then first check to see if the current session is not null and already exists in sessions. If it already exists, it is all in your own family and will not do anything; if the current session is null, it means there will be a new sessionWhen created, the current number of sessions (sessionCounts) will exceed the number of sessions concurrently (allowedSessions).
  5. If none of the previous codes can be return ed, it will go into allowableSessionsExceeded, the policy judgment method.
  6. The allowableSessionsExceeded method starts with the exceptionIfMaximumExceeded property, which is the value of maxSessionsPreventsLogin that we configure in SecurityConfig. The default is false. If true, the exception is thrown directly, then the login fails (corresponding to the effect of section 2.2), if false, for sessionsSort the requests by time, then expire the extra sessions (corresponding to the effect of section 2.1).

4. Summary

Thus, a simple two-line configuration implements concurrent management of session s in Spring Security.Is it simple?However, there is still a small pit here, and Songge will continue to analyze it with you in the next article.

This case can be downloaded from GitHub: https://github.com/lenve/spring-security-samples

Okay, don't know if they have GET?If GET remembers to click on a pineapple while watching encouragement </sessioninformation></sessioninformation></sessioninformation>

Keywords: Programming Spring Session Firefox github

Added by bl00dshooter on Thu, 07 May 2020 04:01:23 +0300