前言

現(xiàn)在我們已經(jīng)掌握了如何防御會(huì)話固定攻擊,以及在會(huì)話過(guò)期時(shí)的處理策略,但是這些都是針對(duì)單個(gè)HttpSession來(lái)說(shuō)的,對(duì)于會(huì)話來(lái)說(shuō),我們還有另一種情況需要考慮:會(huì)話并發(fā)控制!

那什么是會(huì)話并發(fā)控制呢?假如我想實(shí)現(xiàn) “在我們的網(wǎng)站中,同一時(shí)刻只允許一個(gè)用戶登錄” 這樣的效果,該怎么做?

請(qǐng)各位帶著以上的這些問(wèn)題,跟著 一一哥 繼續(xù)往下學(xué)習(xí)吧。

一. 會(huì)話并發(fā)控制

1. 概念

首先我們來(lái)了解一下會(huì)話并發(fā)控制的概念。

有時(shí)候出于安全的目的,我們可能會(huì)有這樣的需求,就是規(guī)定在同一個(gè)系統(tǒng)中,只允許一個(gè)用戶在一個(gè)終端上登錄,這其實(shí)就是對(duì)會(huì)話的并發(fā)控制。

2. 并發(fā)控制實(shí)現(xiàn)思路

如果我們要想實(shí)現(xiàn)上述目標(biāo),即同一個(gè)用戶不能同時(shí)在兩臺(tái)設(shè)備上登錄,我們有兩種解決思路:

  • 后來(lái)的登錄自動(dòng)踢掉前面的登錄,例如QQ。
  • 如果用戶已經(jīng)登錄,則不允許后來(lái)者登錄。

以上兩種思路都能實(shí)現(xiàn)這個(gè)功能,具體使用哪一個(gè),還要看我們具體的需求。在 Spring Security 框架中,這兩種思路都很容易實(shí)現(xiàn),一個(gè)配置就可以搞定。

3. 實(shí)現(xiàn)方案一:踢掉原有登錄用戶

我們可以在SecurityConfig配置類中,通過(guò)maximumsessions()方法來(lái)置單個(gè)用戶允許同時(shí)在線的最大并發(fā)會(huì)話數(shù),如果沒(méi)有額外配置,重新登錄的會(huì)話會(huì)踢掉舊的會(huì)話。

@EnableWebSecurity(debug = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { () .antMatchers("/admin/**") .hasRole("ADMIN") .antMatchers("/user/**") .hasRole("USER") .antMatchers("/app/**") .permitAll() .anyrequest() .authenticated() .and() .csrf() .disable() .formLogin() .permitAll() .and() //進(jìn)行會(huì)話管理 .sessionManagement() //最大并發(fā)會(huì)話數(shù),設(shè)置單個(gè)用戶允許同時(shí)在線的最大會(huì)話數(shù),如果沒(méi)有額外配置,重新登錄的會(huì)話會(huì)踢掉舊的會(huì)話. .maximumSessions(1); } @Bean public PasswordEncoder passwordEncoder() { return NoO(); } }

maximumSessions: 表示最大并發(fā)會(huì)話數(shù),設(shè)置單個(gè)用戶允許同時(shí)在線的最大會(huì)話數(shù),如果沒(méi)有額外配置,重新登錄的會(huì)話會(huì)踢掉舊的會(huì)話。這里配置最大會(huì)話數(shù)為 1,這樣后面的登錄就會(huì)自動(dòng)踢掉前面的登錄。

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

  1. 我們先在Firefox 上訪問(wèn) /user/hello 接口。
  2. 然后在Chrome 上訪問(wèn) /user/hello 接口。

等我們?cè)?Firefox 上再次訪問(wèn) /user/hello 接口時(shí),此時(shí)會(huì)看到如下提示:

以上信息表明第一個(gè) session 已經(jīng)過(guò)期,原因則是由于使用同一個(gè)用戶進(jìn)行并發(fā)登錄。

4. 實(shí)現(xiàn)方案二:阻止新用戶登錄

如果我們已經(jīng)有一個(gè)用戶登錄了,這時(shí)候這個(gè)相同的賬號(hào)信息還想再次登錄,我們可以阻止新用戶登錄,配置方式如下:

@EnableWebSecurity(debug = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { () .antMatchers("/admin/**") .hasRole("ADMIN") .antMatchers("/user/**") .hasRole("USER") .antMatchers("/app/**") .permitAll() .anyRequest() .authenticated() .and() .csrf() .disable() .formLogin() .permitAll() .and() //進(jìn)行會(huì)話管理 .sessionManagement() //最大并發(fā)會(huì)話數(shù),設(shè)置單個(gè)用戶允許同時(shí)在線的最大會(huì)話數(shù),如果沒(méi)有額外配置,重新登錄的會(huì)話會(huì)踢掉舊的會(huì)話. .maximumSessions(1) //當(dāng)達(dá)到最大會(huì)話數(shù)時(shí),阻止建立新會(huì)話,而不是踢掉舊的會(huì)話.默認(rèn)為false .maxSessionsPreventsLogin(true); } /** * 監(jiān)聽(tīng)session創(chuàng)建及銷毀事件,來(lái)及時(shí)清理session的記錄,以確保最新的session狀態(tài)可以被及時(shí)感知到。 */ @Bean public HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); } @Bean public PasswordEncoder passwordEncoder() { return NoO(); } }

也就是說(shuō)我們只需要添加一個(gè) maxSessionsPreventsLogin(true) 配置即可,此時(shí)一個(gè)瀏覽器登錄成功后,另外一個(gè)瀏覽器就登錄不了了,效果如下:

另外我們?cè)谏厦娴脑创a中,還配置了HttpSessionEventPublisher對(duì)象的創(chuàng)建。

注意:

我們之所以要?jiǎng)?chuàng)建HttpSessionEventPublisher這個(gè)Bean,是因?yàn)樵?Spring Security 中,它可以通過(guò)監(jiān)聽(tīng)session的創(chuàng)建及銷毀事件,來(lái)及時(shí)地清理session記錄。用戶從不同的瀏覽器登錄,都會(huì)有對(duì)應(yīng)的session,當(dāng)用戶注銷登錄之后,session就會(huì)失效,但是默認(rèn)的失效是通過(guò)調(diào)用 StandardSession#invalidate 方法來(lái)實(shí)現(xiàn)的。這一失效事件無(wú)法被 Spring 容器感知到,進(jìn)而導(dǎo)致當(dāng)前用戶注銷登錄之后,Spring Security 沒(méi)有及時(shí)清理會(huì)話信息表,以為用戶還在線,進(jìn)而導(dǎo)致用戶無(wú)法重新登錄進(jìn)來(lái)。

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

public void sessionCreated(HttpSessionEvent event) { HttpSessionCreatedEvent e = new HttpSessionCreatedEven()); Log log = LogFac(LOGGER_NAME); if ()) { log.debug("Publishing event: " + e); } getContex().getServletContext()).publishEvent(e); } /** * Handles the HttpSessionEvent by publishing a {@link HttpSessionDestroyedEvent} to * the application appContext. * * @param event The HttpSessionEvent pass in by the container */ public void sessionDestroyed(HttpSessionEvent event) { HttpSessionDestroyedEvent e = new HttpSessionDestroyedEven()); Log log = LogFac(LOGGER_NAME); if ()) { log.debug("Publishing event: " + e); } getContex().getServletContext()).publishEvent(e); }

這樣我們就實(shí)現(xiàn)了會(huì)話的并發(fā)控制,代碼也很簡(jiǎn)單。

二. 會(huì)話并發(fā)控制源碼解析

兩種會(huì)話并發(fā)控制方案我都已經(jīng)帶著各位實(shí)現(xiàn)了,那么底層的實(shí)現(xiàn)原理是什么樣的呢? 我們一起來(lái)看看會(huì)話并發(fā)控制的源碼吧。

1. 會(huì)話并發(fā)控制執(zhí)行流程

在Spring Security中,對(duì)會(huì)話的并發(fā)控制也有特定的執(zhí)行控制流程,該流程是在如下類中被觸發(fā)執(zhí)行的:

UsernamePasswordAuthenticationFilter-->AbstractAuthenticationProcessingFilter-->AbstractAuthenticationProcessingFilter#doFilter()

2. AbstractAuthenticationProcessingFilter#doFilter()源碼

所以我們打開(kāi)AbstractAuthenticationProcessingFilter過(guò)濾器來(lái)看看它的doFilter()方法是怎么定義的。

在這段源代碼中我們可以看到,在調(diào)用完 attemptAuthentication方法執(zhí)行完認(rèn)證流程之后,接下來(lái)就會(huì)調(diào)用 方法,這個(gè)方法就是用來(lái)處理 session 的并發(fā)問(wèn)題的。

3. SessionAuthenticationStrategy接口

接著我們看看sessionStrategy對(duì)象所在類SessionAuthenticationStrategy的源碼,內(nèi)部定義了一個(gè)onAuthentication()方法。

public interface SessionAuthenticationStrategy { /** * 當(dāng)一個(gè)新的認(rèn)證發(fā)生時(shí),執(zhí)行與session相關(guān)的功能。 * */ void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) throws SessionAuthenticationException; }

我們看看該接口有哪些子類,發(fā)現(xiàn)該接口有多個(gè)子類,如下圖所示:

其中默認(rèn)的實(shí)現(xiàn)子類是NullAuthenticatedSessionStrategy,如下圖所示:

4. ConcurrentSessionControlAuthenticationStrategy

但是當(dāng)涉及到session的并發(fā)處理時(shí),會(huì)由ConcurrentSessionControlAuthenticationStrategy這個(gè)子類來(lái)實(shí)現(xiàn)會(huì)話并發(fā)處理,核心方法如下:

public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) { final List<SessionInformation> sessions = ( au(), false); int sessionCount = (); 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 = reque(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 ().equal())) { 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( "Concurren;, new Object[] {allowableSessions}, "Maximum sessions of {0} for this principal exceeded")); } // Determine least recently used sessions, and mark them for invalidation (SessionInformation::getLastRequest)); int maximumSessionsExceededBy = () - allowableSessions + 1; List<SessionInformation> sessionsToBeExpired = (0, maximumSessionsExceededBy); for (SessionInformation session: sessionsToBeExpired) { (); } }

這段核心代碼主要含義如下:

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

至此,壹哥 就跟大家講解了會(huì)話并發(fā)控制的兩種實(shí)現(xiàn)方案,并且給大家講解了底層的實(shí)現(xiàn)源碼,請(qǐng)各位照著我的文章進(jìn)行實(shí)現(xiàn)和閱讀,有不明白之處,請(qǐng)?jiān)谠u(píng)論區(qū)留言!

1.《如何qq只能等一個(gè)號(hào) 如何讓qq只能在一臺(tái)手機(jī)?》援引自互聯(lián)網(wǎng),旨在傳遞更多網(wǎng)絡(luò)信息知識(shí),僅代表作者本人觀點(diǎn),與本網(wǎng)站無(wú)關(guān),侵刪請(qǐng)聯(lián)系頁(yè)腳下方聯(lián)系方式。

2.《如何qq只能等一個(gè)號(hào) 如何讓qq只能在一臺(tái)手機(jī)?》僅供讀者參考,本網(wǎng)站未對(duì)該內(nèi)容進(jìn)行證實(shí),對(duì)其原創(chuàng)性、真實(shí)性、完整性、及時(shí)性不作任何保證。

3.文章轉(zhuǎn)載時(shí)請(qǐng)保留本站內(nèi)容來(lái)源地址,http://f99ss.com/keji/3218251.html