Spring Security 自動踢掉前一個登錄用戶的實現(xiàn)代碼
登錄成功后,自動踢掉前一個登錄用戶,松哥第一次見到這個功能,就是在扣扣里邊見到的,當時覺得挺好玩的。
自己做開發(fā)后,也遇到過一模一樣的需求,正好最近的 Spring Security 系列正在連載,就結合 Spring Security 來和大家聊一聊這個功能如何實現(xiàn)。
本文是本系列的第十三篇,閱讀前面文章有助于更好的理解本文:
- 挖一個大坑,Spring Security 開搞!
- 松哥手把手帶你入門 Spring Security,別再問密碼怎么解密了
- 手把手教你定制 Spring Security 中的表單登錄
- Spring Security 做前后端分離,咱就別做頁面跳轉了!統(tǒng)統(tǒng) JSON 交互
- Spring Security 中的授權操作原來這么簡單
- Spring Security 如何將用戶數(shù)據(jù)存入數(shù)據(jù)庫?
- Spring Security+Spring Data Jpa 強強聯(lián)手,安全管理只有更簡單!
- Spring Boot + Spring Security 實現(xiàn)自動登錄功能
- Spring Boot 自動登錄,安全風險要怎么控制?
- 在微服務項目中,Spring Security 比 Shiro 強在哪?
- SpringSecurity 自定義認證邏輯的兩種方式(高級玩法)
- Spring Security 中如何快速查看登錄用戶 IP 地址等信息?
1.需求分析
在同一個系統(tǒng)中,我們可能只允許一個用戶在一個終端上登錄,一般來說這可能是出于安全方面的考慮,但是也有一些情況是出于業(yè)務上的考慮,松哥之前遇到的需求就是業(yè)務原因要求一個用戶只能在一個設備上登錄。
要實現(xiàn)一個用戶不可以同時在兩臺設備上登錄,我們有兩種思路:
- 后來的登錄自動踢掉前面的登錄,就像大家在扣扣中看到的效果。
- 如果用戶已經登錄,則不允許后來者登錄。
這種思路都能實現(xiàn)這個功能,具體使用哪一個,還要看我們具體的需求。
在 Spring Security 中,這兩種都很好實現(xiàn),一個配置就可以搞定。
2.具體實現(xiàn)
2.1 踢掉已經登錄用戶
想要用新的登錄踢掉舊的登錄,我們只需要將最大會話數(shù)設置為 1 即可,配置如下:
@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 表示配置最大會話數(shù)為 1,這樣后面的登錄就會自動踢掉前面的登錄。這里其他的配置都是我們前面文章講過的,我就不再重復介紹,文末可以下載案例完整代碼。
配置完成后,分別用 Chrome 和 Firefox 兩個瀏覽器進行測試(或者使用 Chrome 中的多用戶功能)。
- Chrome 上登錄成功后,訪問 /hello 接口。
- Firefox 上登錄成功后,訪問 /hello 接口。
- 在 Chrome 上再次訪問 /hello 接口,此時會看到如下提示:
This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).
可以看到,這里說這個 session 已經過期,原因則是由于使用同一個用戶進行并發(fā)登錄。
2.2 禁止新的登錄
如果相同的用戶已經登錄了,你不想踢掉他,而是想禁止新的登錄操作,那也好辦,配置方式如下:
@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);
}
添加 maxSessionsPreventsLogin 配置即可。此時一個瀏覽器登錄成功后,另外一個瀏覽器就登錄不了了。
是不是很簡單?
不過還沒完,我們還需要再提供一個 Bean:
@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
為什么要加這個 Bean 呢?因為在 Spring Security 中,它是通過監(jiān)聽 session 的銷毀事件,來及時的清理 session 的記錄。用戶從不同的瀏覽器登錄后,都會有對應的 session,當用戶注銷登錄之后,session 就會失效,但是默認的失效是通過調用 StandardSession#invalidate 方法來實現(xiàn)的,這一個失效事件無法被 Spring 容器感知到,進而導致當用戶注銷登錄之后,Spring Security 沒有及時清理會話信息表,以為用戶還在線,進而導致用戶無法重新登錄進來(小伙伴們可以自行嘗試不添加上面的 Bean,然后讓用戶注銷登錄之后再重新登錄)。
為了解決這一問題,我們提供一個 HttpSessionEventPublisher ,這個類實現(xiàn)了 HttpSessionListener 接口,在該 Bean 中,可以將 session 創(chuàng)建以及銷毀的事件及時感知到,并且調用 Spring 中的事件機制將相關的創(chuàng)建和銷毀事件發(fā)布出去,進而被 Spring Security 感知到,該類部分源碼如下:
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,雖然多了一個配置,但是依然很簡單!
3.實現(xiàn)原理
上面這個功能,在 Spring Security 中是怎么實現(xiàn)的呢?我們來稍微分析一下源碼。
首先我們知道,在用戶登錄的過程中,會經過 UsernamePasswordAuthenticationFilter(參考: Spring Security 登錄流程),而 UsernamePasswordAuthenticationFilter 中過濾方法的調用是在 AbstractAuthenticationProcessingFilter 中觸發(fā)的,我們來看下 AbstractAuthenticationProcessingFilter#doFilter 方法的調用:
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);
在這段代碼中,我們可以看到,調用 attemptAuthentication 方法走完認證流程之后,回來之后,接下來就是調用 sessionStrategy.onAuthentication 方法,這個方法就是用來處理 session 的并發(fā)問題的。具體在:
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();
}
}
}
這段核心代碼我來給大家稍微解釋下:
- 首先調用 sessionRegistry.getAllSessions 方法獲取當前用戶的所有 session,該方法在調用時,傳遞兩個參數(shù),一個是當前用戶的 authentication,另一個參數(shù) false 表示不包含已經過期的 session(在用戶登錄成功后,會將用戶的 sessionid 存起來,其中 key 是用戶的主體(principal),value 則是該主題對應的 sessionid 組成的一個集合)。
- 接下來計算出當前用戶已經有幾個有效 session 了,同時獲取允許的 session 并發(fā)數(shù)。
- 如果當前 session 數(shù)(sessionCount)小于 session 并發(fā)數(shù)(allowedSessions),則不做任何處理;如果 allowedSessions 的值為 -1,表示對 session 數(shù)量不做任何限制。
- 如果當前 session 數(shù)(sessionCount)等于 session 并發(fā)數(shù)(allowedSessions),那就先看看當前 session 是否不為 null,并且已經存在于 sessions 中了,如果已經存在了,那都是自家人,不做任何處理;如果當前 session 為 null,那么意味著將有一個新的 session 被創(chuàng)建出來,屆時當前 session 數(shù)(sessionCount)就會超過 session 并發(fā)數(shù)(allowedSessions)。
- 如果前面的代碼中都沒能 return 掉,那么將進入策略判斷方法 allowableSessionsExceeded 中。
- allowableSessionsExceeded 方法中,首先會有 exceptionIfMaximumExceeded 屬性,這就是我們在 SecurityConfig 中配置的 maxSessionsPreventsLogin 的值,默認為 false,如果為 true,就直接拋出異常,那么這次登錄就失敗了(對應 2.2 小節(jié)的效果),如果為 false,則對 sessions 按照請求時間進行排序,然后再使多余的 session 過期即可(對應 2.1 小節(jié)的效果)。
4.小結
如此,兩行簡單的配置就實現(xiàn)了 Spring Security 中 session 的并發(fā)管理。是不是很簡單?不過這里還有一個小小的坑,松哥將在下篇文章中繼續(xù)和大家分析。
本文案例大家可以從 GitHub 上下載:https://github.com/lenve/spring-security-samples
到此這篇關于Spring Security 自動踢掉前一個登錄用戶的實現(xiàn)代碼的文章就介紹到這了,更多相關Spring Security 踢掉登錄用戶內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
java實現(xiàn)識別二維碼圖片功能方法詳解與實例源碼
這篇文章主要介紹了java實現(xiàn)識別二維碼圖片,java無法識別二維碼情況下對二維碼圖片調優(yōu)功能方法與實例源碼,需要的朋友可以參考下2022-12-12
MyBatis自定義TypeHandler如何解決字段映射問題
這篇文章主要介紹了MyBatis自定義TypeHandler如何解決字段映射問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12

