記錄 pmsys 中使用 ConcurrentSessionControlAuthenticationStrategy
作為 session
的並發控制策略,實現用戶單一登錄,擠掉前一個登錄的用戶。當出現了一些小問題,所以以此記錄。
問題
使用了 ConcurrentSessionControlAuthenticationStrategy
是實現了單一登錄, 但沒完全實現。因為后一個登錄的用戶也被擠掉, 即這個賬號所有用戶都被下線。
原因
ConcurrentSessionControlAuthenticationStrategy
源碼中 onAuthentication
方法.
官方 方法說明: In addition to the steps from the superclass, the sessionRegistry will be updated with the new session information.
機翻: 除了超類中的步驟之外,還將使用新的會話信息更新sessionRegistry。
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
// 全部的 session 數量
int sessionCount = sessions.size();
// 這里得到可允許的 session 並發數量
int allowedSessions = this.getMaximumSessionsForThisUser(authentication);
if (sessionCount >= allowedSessions) {
if (allowedSessions != -1) {
if (sessionCount == allowedSessions) {
HttpSession session = request.getSession(false);
if (session != null) {
Iterator var8 = sessions.iterator();
while(var8.hasNext()) {
SessionInformation si = (SessionInformation)var8.next();
if (si.getSessionId().equals(session.getId())) {
return;
}
}
}
}
// 超出允許的並發數量,就會調用此方法, 原因
this.allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry);
}
}
}
allowableSessionsExceeded
方法源碼:
protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions, SessionRegistry registry) throws SessionAuthenticationException {
if (!this.exceptionIfMaximumExceeded && sessions != null) {
sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
// 以下兩行代碼就是主要原因
int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
Iterator var6 = sessionsToBeExpired.iterator();
while(var6.hasNext()) {
SessionInformation session = (SessionInformation)var6.next();
// 使當前 session 失效
session.expireNow();
}
} else {
throw new SessionAuthenticationException(this.messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed", new Object[]{allowableSessions}, "Maximum sessions of {0} for this principal exceeded"));
}
}
為了實現用戶單一登錄, 那么最大並發數量肯定是 1 (默認也是 1 ), maximumSessionsExceededBy
= 2 - 1 + 1 = 2 . 那么 sessionsToBeExpired
就是把兩個 session
都包含其中,然后都令其失效. 從而導致兩個用戶都下線.
這里提一下 subList 方法
List<E> subList(int fromIndex, int toIndex);
這個方法是左開右閉的, 即
tiIndex
這個位置是沒有選到的, 例如: sublist(0,2) , 得到只有集合中下標為 0 , 1 的元素.還有一些情況,就自行百度.
個人解決方案
繼承 ConcurrentSessionControlAuthenticationStrategy
重寫 allowableSessionsExceeded
方法. 用自己的 session
並發控制策略
@Component
public class MyConcurrentSessionControlAuthenticationStrategy extends ConcurrentSessionControlAuthenticationStrategy {
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
public MyConcurrentSessionControlAuthenticationStrategy(SessionRegistry sessionRegistry) {
super(sessionRegistry);
}
@Override
protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions, SessionRegistry registry) throws SessionAuthenticationException {
if (sessions != null) {
sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
int maximumSessionsExceededBy = sessions.size() - allowableSessions;
List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
Iterator var6 = sessionsToBeExpired.iterator();
while(var6.hasNext()) {
SessionInformation session = (SessionInformation)var6.next();
session.expireNow();
}
} else {
throw new SessionAuthenticationException(this.messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed", new Object[]{allowableSessions}, "Maximum sessions of {0} for this principal exceeded"));
}
}
}
實驗結果可行.
GitHub 也有人提出這個問題,但問題待解決. ConcurrentSessionControlAuthenticationStrategy.allowableSessionsExceeded set the latest session to invalid, is this a bug? #9343