Spring security automatically kicks off the previous login user, and a configuration is completed!

Time:2020-10-18

After logging in successfully, he automatically kicked out the previous login user. The first time SongGe saw this function was in the button. He thought it was very interesting.

After doing the development as like as two peas, they also met exactly the same needs. Just now the Spring Security series is being serialized. We will talk to you about how to realize this function by combining Spring Security.

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

  1. Dig a big hole and spring security will open up!
  2. SongGe takes you to spring security hand in hand. Don’t ask how to decrypt the password
  3. Hand in hand to teach you to customize the form login in spring security
  4. Spring security does front-end and back-end separation, so we don’t do page Jump! All JSON interaction
  5. Authorization in spring security is so simple
  6. How does spring security store user data in the database?
  7. Spring Security + spring data JPA join forces to make security management easier!
  8. Spring boot + spring security realizes automatic login function
  9. How to control the security risk of spring boot automatic login?
  10. In microservice projects, where is spring security better than Shiro?
  11. Two ways of spring security custom authentication logic (Advanced playing method)
  12. How to quickly view login user’s IP address and other information in spring security?

1. Demand analysis

In the same system, we may only allow one user to log in on one terminal. Generally speaking, this may be for security reasons, but there are also some cases for business considerations. SongGe’s previous requirement is that a user can only log in on one device for business reasons.

To realize that a user can not log in on two devices at the same time, we have two ideas:

  • Later login will automatically kick out the previous login, just as you can see in the button.
  • If the user is already logged in, latecomers are not allowed to log in.

This way of thinking can achieve this function. Which one to use depends on our specific needs.

In spring security, both of them are well implemented and can be done in one configuration.

2. Specific implementation

2.1 kick out the logged in user

To kick out the old login with a new login, we only need to set the maximum number of sessions to 1. The configuration is 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);
}

Maximum sessions means that the maximum number of sessions is set to 1, so that subsequent logins will automatically kick out the previous ones. The other configurations here are described in the previous article, so I will not repeat them. The complete code of the case can be downloaded at the end of the article.

After the configuration is completed, test it with chrome and Firefox (or use the multi-user function in chrome).

  1. After successful login on chrome, access the / Hello interface.
  2. After successful login on Firefox, access the / Hello interface.
  3. When you visit the / Hello interface again on chrome, you will see the following prompt:
This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).

As you can see, this session has expired because the same user is used for concurrent login.

2.2 prohibit new login

If the same user has already logged in, you don’t want to kick him off, but you want to prohibit new login operations. The configuration method is 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 maxsessions preventslogin configuration. At this time, after one browser successfully logs in, the other browser cannot log in.

Is it easy?

However, we still 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 to session destruction events. After users log in from different browsers, there will be corresponding sessions. When users log off and log in, the session will be invalid. However, the default invalidation is realized by calling the standardsession ා validate method. This failure event cannot be perceived by spring container, which will lead to spring security after users log off and login The session information table was not cleaned up in time, so that the user was still online, which led to the user being unable to log in again (partners can try not to add the bean above, and then ask the user to log out and then log in again).

In order to solve this problem, we provide an httpsessioneventpublisher. This class implements the HttpSessionListener interface. In this bean, you can sense the events created and destroyed by the session in time, and call the event mechanism in spring to publish the related creation and destruction events, and then spring security will release them Some source codes of 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 an additional configuration, it is still very simple!

3. Implementation principle

How is the above function implemented in spring security? Let’s analyze the source code a little bit.

First of all, we know that during the user login process, the usernamepasswordauthenticationfilter (Reference: SongGe hands you through the spring security login process), and the call of the filtering method in the usernamepasswordauthenticationfilter is triggered in the abstractauthentication processingfilter. Let’s take a look Abstractauthenticationprocessingfilter ා dofilter method call:

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, after completing the authentication process and coming back, the next step is to call the sessionStrategy.onAuthentication Method. This method is used to handle the concurrency of the session. The details are as follows:

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

I’d like to explain the core code to you

  1. Call first sessionRegistry.getAllSessions Method to get all sessions of the current user. When the method is called, two parameters are passed: one is the authentication of the current user, and the other parameter false means that the expired session is not included (after the user logs in successfully, the user’s sessionid will be saved, in which the key Is the principal of the user, and value is a collection of session IDs corresponding to the topic.
  2. Next, calculate the current user has several valid sessions, and obtain the number of session concurrency allowed.
  3. If the current session count is less than the session concurrency (allowedsessions), no processing is done. If the value of allowedsessions is – 1, there is no restriction on the number of sessions.
  4. If the current session count is equal to the number of allowed sessions, first check whether the current session is not null and already exists in sessions. If it already exists, it is our own person and does not do any processing. If the current session is null, it means that there will be a new session If it is created, the current session count will exceed the session concurrency number.
  5. If none of the previous codes can be returned, it will enter the policy judgment method allowablesessions exceeded.
  6. In the allowablesessionsexceeded method, there will be the exceptionifmaximumexceeded attribute first, which is the value of maxsessionspreventslogin configured in securityconfig. The default value is false. If it is true, an exception will be thrown directly. Then this login fails (corresponding to the effect of Section 2.2). If it is false, then sessions will be disabled Sort according to the request time, and then expire the extra sessions (corresponding to the effect of Section 2.1).

4. Summary

In this way, two lines of simple configuration can realize the concurrent management of session in spring security. Is it easy? However, there is a small hole here. 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

OK, I don’t know if my friends got there? If you get there, remember to watch and encourage SongGe

Recommended Today

Set and map notes

Personal understanding: set is similar to one-dimensional array, and map is similar to two-dimensional array or object Similarities The feeling is that there are several basic attributes and methods with the same name and function: The size attribute returns the number of elements Clear clears all elements Delete removes the specified element Foreach traverses data […]