溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何實(shí)現(xiàn)SpringSecurity只允許一臺(tái)設(shè)備在線

發(fā)布時(shí)間:2021-10-11 11:12:55 來(lái)源:億速云 閱讀:142 作者:iii 欄目:開(kāi)發(fā)技術(shù)

本篇內(nèi)容介紹了“如何實(shí)現(xiàn)SpringSecurity只允許一臺(tái)設(shè)備在線”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

如何實(shí)現(xiàn)SpringSecurity只允許一臺(tái)設(shè)備在線

1.需求分析

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

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

后來(lái)的登錄自動(dòng)踢掉前面的登錄,就像大家在扣扣中看到的效果。

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

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

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

2.具體實(shí)現(xiàn)

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

想要用新的登錄踢掉舊的登錄,我們只需要將最大會(huì)話數(shù)設(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 表示配置最大會(huì)話數(shù)為  1,這樣后面的登錄就會(huì)自動(dòng)踢掉前面的登錄。這里其他的配置都是我們前面文章講過(guò)的,我就不再重復(fù)介紹,文末可以下載案例完整代碼。

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

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. Chrome 上登錄成功后,訪問(wèn) /hello 接口。

  3. Firefox 上登錄成功后,訪問(wèn) /hello 接口。

  4. 在 Chrome 上再次訪問(wèn) /hello 接口,此時(shí)會(huì)看到如下提示:

This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).

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

2.2 禁止新的登錄

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

@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 配置即可。此時(shí)一個(gè)瀏覽器登錄成功后,另外一個(gè)瀏覽器就登錄不了了。

是不是很簡(jiǎn)單?

不過(guò)還沒(méi)完,我們還需要再提供一個(gè) Bean:

@Bean HttpSessionEventPublisher httpSessionEventPublisher() {     return new HttpSessionEventPublisher(); }

為什么要加這個(gè) Bean 呢?因?yàn)樵?Spring Security 中,它是通過(guò)監(jiān)聽(tīng) session 的銷毀事件,來(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)的,這一個(gè)失效事件無(wú)法被 Spring 容器感知到,進(jìn)而導(dǎo)致當(dāng)用戶注銷登錄之后,Spring  Security 沒(méi)有及時(shí)清理會(huì)話信息表,以為用戶還在線,進(jìn)而導(dǎo)致用戶無(wú)法重新登錄進(jìn)來(lái)(小伙伴們可以自行嘗試不添加上面的  Bean,然后讓用戶注銷登錄之后再重新登錄)。

為了解決這一問(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 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,雖然多了一個(gè)配置,但是依然很簡(jiǎn)單!

3.實(shí)現(xiàn)原理

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

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

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);

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

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();   }  } }

這段核心代碼我來(lái)給大家稍微解釋下:

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 首先調(diào)用 sessionRegistry.getAllSessions 方法獲取當(dāng)前用戶的所有  session,該方法在調(diào)用時(shí),傳遞兩個(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è)集合)。

  3. 接下來(lái)計(jì)算出當(dāng)前用戶已經(jīng)有幾個(gè)有效 session 了,同時(shí)獲取允許的 session 并發(fā)數(shù)。

  4. 如果當(dāng)前 session 數(shù)(sessionCount)小于 session 并發(fā)數(shù)(allowedSessions),則不做任何處理;如果  allowedSessions 的值為 -1,表示對(duì) session 數(shù)量不做任何限制。

  5. 如果當(dāng)前 session 數(shù)(sessionCount)等于 session 并發(fā)數(shù)(allowedSessions),那就先看看當(dāng)前 session  是否不為 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)。

  6. 如果前面的代碼中都沒(méi)能 return 掉,那么將進(jìn)入策略判斷方法 allowableSessionsExceeded 中。

  7. allowableSessionsExceeded 方法中,首先會(huì)有 exceptionIfMaximumExceeded 屬性,這就是我們?cè)? SecurityConfig 中配置的 maxSessionsPreventsLogin 的值,默認(rèn)為 false,如果為  true,就直接拋出異常,那么這次登錄就失敗了(對(duì)應(yīng) 2.2 小節(jié)的效果),如果為 false,則對(duì) sessions 按照請(qǐng)求時(shí)間進(jìn)行排序,然后再使多余的  session 過(guò)期即可(對(duì)應(yīng) 2.1 小節(jié)的效果)。

“如何實(shí)現(xiàn)SpringSecurity只允許一臺(tái)設(shè)備在線”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI