ConcurrentSessionControlAuthenticationStrategy 有问题?setMaximumSessions(1),还需要两次登入?

期望出现

同一个账号,第二次登入时,可以直接将第一个 JSESSIONID 设置为过期,且直接第二次登入,不会出现第二个 JSESSIONID 也过期。

当前结果

同一个账号,第二次登入时,第一个 JSESSIONID 会设置为过期,且第二个 JSESSIONID 也过期。导致用户需要再登入,也就是第三次才能登入成功。

以此类推,第四次登入又会因为第三次 JSESSIONID 的存在,导致第三个和第四个 JSESSIONID 也同时过期,只能再登入一次才能成功。第六次又不会成功登入,第七次又成功登入.......

我的代码

@Component
@Slf4j
public class MyAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
     @Override
    public void configure(HttpSecurity http) throws Exception {
        MyUsernamePasswordAuthenticationFilter authenticationFilter = new MyUsernamePasswordAuthenticationFilter(sessionRegistry);
//        ...
        ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlAuthenticationStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
        concurrentSessionControlAuthenticationStrategy.setMaximumSessions(1);
        authenticationFilter.setSessionAuthenticationStrategy(concurrentSessionControlAuthenticationStrategy);
        //...
        http.authenticationProvider(myAuthenticationProvider).addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
    }
}

源代码问题

org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy

    protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions,
            SessionRegistry registry) throws SessionAuthenticationException {
        //。..
        int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
        List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
        for (SessionInformation session : sessionsToBeExpired) {
            session.expireNow();
        }
    }

sessions 是一个账号所有没过期的 JSESSIONID 集合,假设concurrentSessionControlAuthenticationStrategy.setMaximumSessions(1) 也就是 allowableSessions 为 1,那么第二次登入后就会进入此方法,此时 int maximumSessionsExceededBy = 2 - 1 + 1 = 2;

List sessionsToBeExpired = sessions.subList(0, 2); 取出了第一次和第二次的 JSESSIOID.

for (SessionInformation session : sessionsToBeExpired) { session.expireNow(); }

循环将两个 JSESSIONID 都设置为过期了,导致同一个账号第二次登入无法直接使用。

继承 ConcurrentSessionControlAuthenticationStrategy.java

public class MyConcurrentSessionControlAuthenticationStrategy extends ConcurrentSessionControlAuthenticationStrategy {
   ...
    public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
                //...
                allowableSessionsExceeded(sessions, allowedSessions+1, this.sessionRegistry);
    }

}
@Component
@Slf4j
public class MyAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
     @Override
    public void configure(HttpSecurity http) throws Exception {
//        ...
        ConcurrentSessionControlAuthenticationStrategy myConcurrentSessionControlAuthenticationStrategy = new MyConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
        concurrentSessionControlAuthenticationStrategy.setMaximumSessions(1);
        authenticationFilter.setSessionAuthenticationStrategy(myConcurrentSessionControlAuthenticationStrategy);
        //...
    }
    }
}

解决了第二次登入,JSESSIONID 立马过期的问题,且不需要第三次登入


ConcurrentsessionControlAuthenticationStrategy has a problem? SetMaximumSessions(1), how many logins are needed?

Expected Behavior

For the same account, when logging in for the second time, you can directly set the first JSESSIONID to expire, and log in for the second time directly, so that the second JSESSIONID will not expire.

Current Behavior

For the same account, when logging in for the second time, the first JSESSIONID will be set to expire, and the second JSESSIONID will also expire. As a result, the user needs to log in again, that is, the third time to log in successfully.

By analogy, the fourth login will be due to the existence of the third JSESSIONID, which will lead to the expiration of the third and fourth JSESSIONIDs at the same time, and you can only log in again to succeed. The sixth login will not succeed, and the seventh login will succeed. .......

My Code

@Component
@Slf4j
public class MyAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Override
public void configure(HttpSecurity http) throws Exception {
MyUsernamePasswordAuthenticationFilter authenticationFilter = new MyUsernamePasswordAuthenticationFilter(sessionRegistry);
//         ...
ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlAuthenticationStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
concurrentSessionControlAuthenticationStrategy.setMaximumSessions(1);
authenticationFilter.setSessionAuthenticationStrategy(concurrentSessionControlAuthenticationStrategy);
// ...
http.authenticationProvider(myAuthenticationProvider).addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
}

Source code problem

org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy

protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions,
SessionRegistry registry) throws SessionAuthenticationException {
//。 ..
int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
for (SessionInformation session : sessionsToBeExpired) {
session.expireNow();
}
}

Sessions is a collection of all the JSESSIONID of an account that have not expired. Assuming that Concurrent Sessions Control Authentication Strategy. SetMaximumSessions (1) is 1, then this method will be entered after the second login, and at this time, IntMaximumSessions Sexeeded by = 2-1+1 = 2;

List sessionsToBeExpired = sessions.subList(0, 2); Took out the first and second JSESSIOID.

for (SessionInformation session : sessionsToBeExpired) { session.expireNow(); }

Cycle sets both JSESSIONID as expired, which makes it impossible to use the same account directly for the second login.

extends ConcurrentSessionControlAuthenticationStrategy.java

public class MyConcurrentSessionControlAuthenticationStrategy extends ConcurrentSessionControlAuthenticationStrategy {
...
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
// ...
allowableSessionsExceeded(sessions, allowedSessions+1, this.sessionRegistry);
}

}
@Component
@Slf4j
public class MyAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Override
public void configure(HttpSecurity http) throws Exception {
//         ...
ConcurrentSessionControlAuthenticationStrategy myConcurrentSessionControlAuthenticationStrategy = new MyConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
concurrentSessionControlAuthenticationStrategy.setMaximumSessions(1);
authenticationFilter.setSessionAuthenticationStrategy(myConcurrentSessionControlAuthenticationStrategy);
// ...
}
}
}

Solved the problem that JSESSIONID will expire immediately after the second login, and there is no need for the third login.


Comment From: sjohnr

@Crazy-GrowUp thanks for reaching out! However, I am having a hard time understanding you since some of your comment is not in English. I will just focus on the second part of your comment which appears to be translated into English.

For the same account, when logging in for the second time, the first JSESSIONID will be set to expire, and the second JSESSIONID will also expire.

There appears to be a misunderstanding of how the ConcurrentSessionControlAuthenticationStrategy is designed to work. Per the javadoc:

When invoked following an authentication, it will check whether the user in question should be allowed to proceed, by comparing the number of sessions they already have active with the configured maximumSessions value.

This means that the component should be invoked prior to the second login attempt. However, your description implies that this is not the case. The following unit test in ConcurrentSessionControlAuthenticationStrategyTests demonstrates what should happen in your case:

https://github.com/spring-projects/spring-security/blob/09b6e4c3253f8f0b766b40f573d2a5289a3ec936/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java#L114-L121

If there is one existing session, and a new login attempt is made, the one existing session becomes expired.

To proceed further, please provide a minimal, reproducible sample or a corresponding unit test to demonstrate the issue you are facing, and we can investigate further.

Comment From: Crazy-GrowUp

你好,感谢回复,我可能要2周后才能写一个示例项目,因为我现在在老家过春节。

Hello, thanks for your reply, I may need 2 weeks to write a sample project, because I am now in my hometown for the Spring Festival.

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: Crazy-GrowUp

项目地址:https://github.com/Crazy-GrowUp/TestSession/tree/master ,在 master 分支下 。 我写了一个小项目,使用了Java 17 和 maven,运行 src/test/java/com/zyl/testsession/TestSessionApplicationTests.java 就可看到效果。 README.md 文件也解释了测试结果。


Project address:https://github.com/Crazy-GrowUp/TestSession/tree/master , under the master branch. I wrote a small project, using Java 17 and maven, and you can see the effect by running src/test/Java/com/zyl/testsession/testsessionapplicationtests.java. The README.md file also explains the test results.

Comment From: sjohnr

Thanks for the sample project and unit test, @Crazy-GrowUp! I appreciate you putting in the work to help me understand what you're seeing. However, please review my above comment again. It outlines that your understanding of how ConcurrentSessionControlAuthenticationStrategy is used is incorrect. Your unit tests confirm that, and are using it incorrectly.

Here is the the main processing of the AbstractAuthenticationProcessingFilter filter in which it is called:

https://github.com/spring-projects/spring-security/blob/8e2a4bf3562133c78230ec5a96ec993c5c92374b/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java#L224-L251

The session authentication strategy is called prior to the second session being created and stored, and will result in the first session being expired. Your unit tests do not work this way, and instead call the session authentication strategy after the second session is stored. This is what is causing your odd behavior of every other session being expired.

The ConcurrentSessionControlAuthenticationStrategy is working as designed, and is being called by the AbstractAuthenticationProcessingFilter correctly. At this point, I'm going to close this issue as invalid.

Comment From: Crazy-GrowUp

My test is modeled after spring-security/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java

Do you mean that in AbstractAuthenticationProcessingFilter.java, after calling this.sessionStrategy.onAuthentication(authenticationResult, request, response), the session will expire, and then in successfulAuthentication(request, response, chain, authenticationResult), the session will be re - registered? I seem not to have seen it.

Or do you need to implement the subsequent chain.doFilter(request, response) by yourself to re - register the session or prompt the user with "The account has been logged in. Please confirm whether to log in again"?

------------------ 原始邮件 ------------------ 发件人: "spring-projects/spring-security" @.>; 发送时间: 2025年2月7日(星期五) 上午7:23 @.>; @.**@.**>; 主题: Re: [spring-projects/spring-security] ConcurrentsessionControlAuthenticationStrategy has a problem? SetMaximumSessions(1), how many logins are needed? (Issue #16437)

Thanks for the sample project and unit test, @Crazy-GrowUp! I appreciate you putting in the work to help me understand what you're seeing. However, please review my above comment again. It outlines that your understanding of how ConcurrentSessionControlAuthenticationStrategy is used is incorrect. Your unit tests confirm that, and are using it incorrectly.

Here is the the main processing of the AbstractAuthenticationProcessingFilter filter in which it is called:

https://github.com/spring-projects/spring-security/blob/8e2a4bf3562133c78230ec5a96ec993c5c92374b/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java#L224-L251

The authentication strategy is called prior to the second session being created and stored, and will result in the first session being expired. Your unit tests do not work this way, and instead call the authentication strategy after the second session is stored. This is what is causing your odd behavior of every other session being expired.

The ConcurrentSessionControlAuthenticationStrategy is working as designed, and is being called by the AbstractAuthenticationProcessingFilter correctly. At this point, I'm going to close this issue as invalid.

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>

Comment From: sjohnr

My test is modeled after spring-security/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java

In my original comment, I linked to an existing test that demonstrates what you are reporting, and validates that ConcurrentSessionControlAuthenticationStrategy handles this situation correctly. Your tests do not work the same way.

Do you mean that in AbstractAuthenticationProcessingFilter.java, after calling this.sessionStrategy.onAuthentication(authenticationResult, request, response), the session will expire,

The existing session (first session) is marked as expired by this.sessionStrategy.onAuthentication(...). When the first session is used in a subsequent request, the ConcurrentSessionFilter handles an expired session:

https://github.com/spring-projects/spring-security/blob/8e2a4bf3562133c78230ec5a96ec993c5c92374b/web/src/main/java/org/springframework/security/web/session/ConcurrentSessionFilter.java#L136-L149

and then in successfulAuthentication(request, response, chain, authenticationResult), the session will be re - registered?

The SecurityContextRepository is used to save the SecurityContext for the second login in successfulAuthentication(...) which will indirectly register a new session for this user.

https://github.com/spring-projects/spring-security/blob/8e2a4bf3562133c78230ec5a96ec993c5c92374b/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java#L320-L325

At this time, I do not see a bug in ConcurrentSessionControlAuthenticationStrategy. There are a lot of moving parts to this functionality, so I understand if it is confusing. If you do feel you've found a bug, please provide a minimal, reproducible sample of a fully working application so that I can take a further look.

Comment From: Crazy-GrowUp

I added my own test method to web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java .

@Test
public void testOnAuthenticationWhenMaxSessionsExceededByMultipleThenMultipleSessionsExpired() {
    onAuthenticationWhenMaxSessionsExceededByMultipleThenMultipleSessionsExpired(4,3);
}

public void onAuthenticationWhenMaxSessionsExceededByMultipleThenMultipleSessionsExpired(int count,int maximumSessions) {
    long firstExpiredTime = 1374766134216L+1;
    List<SessionInformation> sessionInformationList = new ArrayList<>();
    sessionInformationList.add(this.sessionInformation);
    for (int i = 0; i < count; i++) {
        SessionInformation sessionInfo = new SessionInformation(this.authentication.getPrincipal(), "unique"+(i+1),
                new Date(firstExpiredTime+i));
        sessionInformationList.add(sessionInfo);
    }
    given(this.sessionRegistry.getAllSessions(any(), anyBoolean())).willReturn(
            sessionInformationList);
    this.strategy.setMaximumSessions(maximumSessions);
    this.strategy.onAuthentication(this.authentication, this.request, this.response);
    List<SessionInformation> allSessions = this.sessionRegistry.getAllSessions(this.authentication.getPrincipal(), true);
    for (SessionInformation session : allSessions) {
        System.out.println( "principal: " + session.getPrincipal() + ", sessionId: " + session.getSessionId() + ", isExpired: " + session.isExpired());
    }
}

result:

principal: user, sessionId: unique, isExpired: true
principal: user, sessionId: unique1, isExpired: true
principal: user, sessionId: unique2, isExpired: true
principal: user, sessionId: unique3, isExpired: false
principal: user, sessionId: unique4, isExpired: false

So the number of sessions alive at the same time is always less than the value of maximumSessions by 1. Can I understand it this way?

So when maximumSessions = 1, are all sessions expired? Is that the design?