自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

SpringSecurity系列之只允許一臺設(shè)備在線

開發(fā) 架構(gòu)
在同一個系統(tǒng)中,我們可能只允許一個用戶在一個終端上登錄,一般來說這可能是出于安全方面的考慮,但是也有一些情況是出于業(yè)務(wù)上的考慮,松哥之前遇到的需求就是業(yè)務(wù)原因要求一個用戶只能在一個設(shè)備上登錄。

[[399043]]

 登錄成功后,自動踢掉前一個登錄用戶,松哥第一次見到這個功能,就是在扣扣里邊見到的,當(dāng)時覺得挺好玩的。

自己做開發(fā)后,也遇到過一模一樣的需求,正好最近的 Spring Security 系列正在連載,就結(jié)合 Spring Security 來和大家聊一聊這個功能如何實現(xiàn)。

1.需求分析

在同一個系統(tǒng)中,我們可能只允許一個用戶在一個終端上登錄,一般來說這可能是出于安全方面的考慮,但是也有一些情況是出于業(yè)務(wù)上的考慮,松哥之前遇到的需求就是業(yè)務(wù)原因要求一個用戶只能在一個設(shè)備上登錄。

要實現(xiàn)一個用戶不可以同時在兩臺設(shè)備上登錄,我們有兩種思路:

后來的登錄自動踢掉前面的登錄,就像大家在扣扣中看到的效果。

如果用戶已經(jīng)登錄,則不允許后來者登錄。

這種思路都能實現(xiàn)這個功能,具體使用哪一個,還要看我們具體的需求。

在 Spring Security 中,這兩種都很好實現(xiàn),一個配置就可以搞定。

2.具體實現(xiàn)

2.1 踢掉已經(jīng)登錄用戶

想要用新的登錄踢掉舊的登錄,我們只需要將最大會話數(shù)設(shè)置為 1 即可,配置如下:

  1. @Override 
  2. protected void configure(HttpSecurity http) throws Exception { 
  3.     http.authorizeRequests() 
  4.             .anyRequest().authenticated() 
  5.             .and() 
  6.             .formLogin() 
  7.             .loginPage("/login.html"
  8.             .permitAll() 
  9.             .and() 
  10.             .csrf().disable() 
  11.             .sessionManagement() 
  12.             .maximumSessions(1); 

maximumSessions 表示配置最大會話數(shù)為 1,這樣后面的登錄就會自動踢掉前面的登錄。這里其他的配置都是我們前面文章講過的,我就不再重復(fù)介紹,文末可以下載案例完整代碼。

配置完成后,分別用 Chrome 和 Firefox 兩個瀏覽器進行測試(或者使用 Chrome 中的多用戶功能)。

  1. Chrome 上登錄成功后,訪問 /hello 接口。
  2. Firefox 上登錄成功后,訪問 /hello 接口。
  3. 在 Chrome 上再次訪問 /hello 接口,此時會看到如下提示:
  1. This session has been expired (possibly due to multiple concurrent logins being attempted as the same user). 

可以看到,這里說這個 session 已經(jīng)過期,原因則是由于使用同一個用戶進行并發(fā)登錄。

2.2 禁止新的登錄

如果相同的用戶已經(jīng)登錄了,你不想踢掉他,而是想禁止新的登錄操作,那也好辦,配置方式如下:

  1. @Override 
  2. protected void configure(HttpSecurity http) throws Exception { 
  3.     http.authorizeRequests() 
  4.             .anyRequest().authenticated() 
  5.             .and() 
  6.             .formLogin() 
  7.             .loginPage("/login.html"
  8.             .permitAll() 
  9.             .and() 
  10.             .csrf().disable() 
  11.             .sessionManagement() 
  12.             .maximumSessions(1) 
  13.             .maxSessionsPreventsLogin(true); 

添加 maxSessionsPreventsLogin 配置即可。此時一個瀏覽器登錄成功后,另外一個瀏覽器就登錄不了了。

是不是很簡單?

不過還沒完,我們還需要再提供一個 Bean:

  1. @Bean 
  2. HttpSessionEventPublisher httpSessionEventPublisher() { 
  3.     return new HttpSessionEventPublisher(); 

為什么要加這個 Bean 呢?因為在 Spring Security 中,它是通過監(jiān)聽 session 的銷毀事件,來及時的清理 session 的記錄。用戶從不同的瀏覽器登錄后,都會有對應(yīng)的 session,當(dāng)用戶注銷登錄之后,session 就會失效,但是默認(rèn)的失效是通過調(diào)用 StandardSession#invalidate 方法來實現(xiàn)的,這一個失效事件無法被 Spring 容器感知到,進而導(dǎo)致當(dāng)用戶注銷登錄之后,Spring Security 沒有及時清理會話信息表,以為用戶還在線,進而導(dǎo)致用戶無法重新登錄進來(小伙伴們可以自行嘗試不添加上面的 Bean,然后讓用戶注銷登錄之后再重新登錄)。

為了解決這一問題,我們提供一個 HttpSessionEventPublisher ,這個類實現(xiàn)了 HttpSessionListener 接口,在該 Bean 中,可以將 session 創(chuàng)建以及銷毀的事件及時感知到,并且調(diào)用 Spring 中的事件機制將相關(guān)的創(chuàng)建和銷毀事件發(fā)布出去,進而被 Spring Security 感知到,該類部分源碼如下:

  1. public void sessionCreated(HttpSessionEvent event) { 
  2.  HttpSessionCreatedEvent e = new HttpSessionCreatedEvent(event.getSession()); 
  3.  getContext(event.getSession().getServletContext()).publishEvent(e); 
  4. public void sessionDestroyed(HttpSessionEvent event) { 
  5.  HttpSessionDestroyedEvent e = new HttpSessionDestroyedEvent(event.getSession()); 
  6.  getContext(event.getSession().getServletContext()).publishEvent(e); 

OK,雖然多了一個配置,但是依然很簡單!

3.實現(xiàn)原理

上面這個功能,在 Spring Security 中是怎么實現(xiàn)的呢?我們來稍微分析一下源碼。

首先我們知道,在用戶登錄的過程中,會經(jīng)過 UsernamePasswordAuthenticationFilter,而 UsernamePasswordAuthenticationFilter 中過濾方法的調(diào)用是在 AbstractAuthenticationProcessingFilter 中觸發(fā)的,我們來看下 AbstractAuthenticationProcessingFilter#doFilter 方法的調(diào)用:

  1. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 
  2.   throws IOException, ServletException { 
  3.  HttpServletRequest request = (HttpServletRequest) req; 
  4.  HttpServletResponse response = (HttpServletResponse) res; 
  5.  if (!requiresAuthentication(request, response)) { 
  6.   chain.doFilter(request, response); 
  7.   return
  8.  } 
  9.  Authentication authResult; 
  10.  try { 
  11.   authResult = attemptAuthentication(request, response); 
  12.   if (authResult == null) { 
  13.    return
  14.   } 
  15.   sessionStrategy.onAuthentication(authResult, request, response); 
  16.  } 
  17.  catch (InternalAuthenticationServiceException failed) { 
  18.   unsuccessfulAuthentication(request, response, failed); 
  19.   return
  20.  } 
  21.  catch (AuthenticationException failed) { 
  22.   unsuccessfulAuthentication(request, response, failed); 
  23.   return
  24.  } 
  25.  // Authentication success 
  26.  if (continueChainBeforeSuccessfulAuthentication) { 
  27.   chain.doFilter(request, response); 
  28.  } 
  29.  successfulAuthentication(request, response, chain, authResult); 

在這段代碼中,我們可以看到,調(diào)用 attemptAuthentication 方法走完認(rèn)證流程之后,回來之后,接下來就是調(diào)用 sessionStrategy.onAuthentication 方法,這個方法就是用來處理 session 的并發(fā)問題的。具體在:

  1. public class ConcurrentSessionControlAuthenticationStrategy implements 
  2.   MessageSourceAware, SessionAuthenticationStrategy { 
  3.  public void onAuthentication(Authentication authentication, 
  4.    HttpServletRequest request, HttpServletResponse response) { 
  5.  
  6.   final List<SessionInformation> sessions = sessionRegistry.getAllSessions( 
  7.     authentication.getPrincipal(), false); 
  8.  
  9.   int sessionCount = sessions.size(); 
  10.   int allowedSessions = getMaximumSessionsForThisUser(authentication); 
  11.  
  12.   if (sessionCount < allowedSessions) { 
  13.    // They haven't got too many login sessions running at present 
  14.    return
  15.   } 
  16.  
  17.   if (allowedSessions == -1) { 
  18.    // We permit unlimited logins 
  19.    return
  20.   } 
  21.  
  22.   if (sessionCount == allowedSessions) { 
  23.    HttpSession session = request.getSession(false); 
  24.  
  25.    if (session != null) { 
  26.     // Only permit it though if this request is associated with one of the 
  27.     // already registered sessions 
  28.     for (SessionInformation si : sessions) { 
  29.      if (si.getSessionId().equals(session.getId())) { 
  30.       return
  31.      } 
  32.     } 
  33.    } 
  34.    // If the session is null, a new one will be created by the parent class, 
  35.    // exceeding the allowed number 
  36.   } 
  37.  
  38.   allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry); 
  39.  } 
  40.  protected void allowableSessionsExceeded(List<SessionInformation> sessions, 
  41.    int allowableSessions, SessionRegistry registry) 
  42.    throws SessionAuthenticationException { 
  43.   if (exceptionIfMaximumExceeded || (sessions == null)) { 
  44.    throw new SessionAuthenticationException(messages.getMessage( 
  45.      "ConcurrentSessionControlAuthenticationStrategy.exceededAllowed"
  46.      new Object[] {allowableSessions}, 
  47.      "Maximum sessions of {0} for this principal exceeded")); 
  48.   } 
  49.  
  50.   // Determine least recently used sessions, and mark them for invalidation 
  51.   sessions.sort(Comparator.comparing(SessionInformation::getLastRequest)); 
  52.   int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1; 
  53.   List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy); 
  54.   for (SessionInformation session: sessionsToBeExpired) { 
  55.    session.expireNow(); 
  56.   } 
  57.  } 

這段核心代碼我來給大家稍微解釋下:

  1. 首先調(diào)用 sessionRegistry.getAllSessions 方法獲取當(dāng)前用戶的所有 session,該方法在調(diào)用時,傳遞兩個參數(shù),一個是當(dāng)前用戶的 authentication,另一個參數(shù) false 表示不包含已經(jīng)過期的 session(在用戶登錄成功后,會將用戶的 sessionid 存起來,其中 key 是用戶的主體(principal),value 則是該主題對應(yīng)的 sessionid 組成的一個集合)。
  2. 接下來計算出當(dāng)前用戶已經(jīng)有幾個有效 session 了,同時獲取允許的 session 并發(fā)數(shù)。
  3. 如果當(dāng)前 session 數(shù)(sessionCount)小于 session 并發(fā)數(shù)(allowedSessions),則不做任何處理;如果 allowedSessions 的值為 -1,表示對 session 數(shù)量不做任何限制。
  4. 如果當(dāng)前 session 數(shù)(sessionCount)等于 session 并發(fā)數(shù)(allowedSessions),那就先看看當(dāng)前 session 是否不為 null,并且已經(jīng)存在于 sessions 中了,如果已經(jīng)存在了,那都是自家人,不做任何處理;如果當(dāng)前 session 為 null,那么意味著將有一個新的 session 被創(chuàng)建出來,屆時當(dāng)前 session 數(shù)(sessionCount)就會超過 session 并發(fā)數(shù)(allowedSessions)。
  5. 如果前面的代碼中都沒能 return 掉,那么將進入策略判斷方法 allowableSessionsExceeded 中。
  6. allowableSessionsExceeded 方法中,首先會有 exceptionIfMaximumExceeded 屬性,這就是我們在 SecurityConfig 中配置的 maxSessionsPreventsLogin 的值,默認(rèn)為 false,如果為 true,就直接拋出異常,那么這次登錄就失敗了(對應(yīng) 2.2 小節(jié)的效果),如果為 false,則對 sessions 按照請求時間進行排序,然后再使多余的 session 過期即可(對應(yīng) 2.1 小節(jié)的效果)。

4.小結(jié)

如此,兩行簡單的配置就實現(xiàn)了 Spring Security 中 session 的并發(fā)管理。是不是很簡單?不過這里還有一個小小的坑,松哥將在下篇文章中繼續(xù)和大家分析。

本文案例大家可以從 GitHub 上下載:https://github.com/lenve/spring-security-samples

本文轉(zhuǎn)載自微信公眾號「江南一點雨」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系江南一點雨公眾號。

 

責(zé)任編輯:武曉燕 來源: 江南一點雨
相關(guān)推薦

2021-01-14 10:57:29

Zabbix監(jiān)控網(wǎng)絡(luò)設(shè)備

2021-05-08 10:44:35

SpringSecur登錄詳情

2019-07-03 11:34:30

物聯(lián)網(wǎng)數(shù)據(jù)技術(shù)

2021-07-12 06:52:48

Zabbix監(jiān)控Linux

2021-04-21 10:38:44

Spring Boot RememberMe安全

2021-07-02 10:45:53

SpringBootCAS登錄

2009-09-16 17:04:58

第一臺PC王之

2014-03-21 10:16:17

2017-05-31 10:27:25

戴爾教育云

2011-03-22 15:05:45

IBMWatson智慧

2013-03-07 09:12:41

巨型電腦云計算

2012-03-16 17:00:09

2016-07-12 10:40:35

服務(wù)器

2021-07-06 11:42:05

數(shù)據(jù)庫SpringSecurCAS

2021-08-18 06:47:55

物聯(lián)網(wǎng)管理服務(wù)IOT

2017-12-27 16:54:46

華為

2019-12-23 09:32:43

科達

2012-02-16 09:54:52

NASA大型機

2009-09-16 16:18:51

2024-01-12 08:03:28

安裝軟件電腦Mac
點贊
收藏

51CTO技術(shù)棧公眾號