您好,登錄后才能下訂單哦!
這篇文章主要講解了Spring Security如何自動(dòng)踢掉前一個(gè)登錄用戶,內(nèi)容清晰明了,對此有興趣的小伙伴可以學(xué)習(xí)一下,相信大家閱讀完之后會(huì)有幫助。
1.需求分析
在同一個(gè)系統(tǒng)中,我們可能只允許一個(gè)用戶在一個(gè)終端上登錄,一般來說這可能是出于安全方面的考慮,但是也有一些情況是出于業(yè)務(wù)上的考慮,松哥之前遇到的需求就是業(yè)務(wù)原因要求一個(gè)用戶只能在一個(gè)設(shè)備上登錄。
要實(shí)現(xiàn)一個(gè)用戶不可以同時(shí)在兩臺(tái)設(shè)備上登錄,我們有兩種思路:
這種思路都能實(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)踢掉前面的登錄。這里其他的配置都是我們前面文章講過的,我就不再重復(fù)介紹,文末可以下載案例完整代碼。
配置完成后,分別用 Chrome 和 Firefox 兩個(gè)瀏覽器進(jìn)行測試(或者使用 Chrome 中的多用戶功能)。
This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).
可以看到,這里說這個(gè) session 已經(jīng)過期,原因則是由于使用同一個(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è)瀏覽器就登錄不了了。
是不是很簡單?
不過還沒完,我們還需要再提供一個(gè) Bean:
@Bean HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); }
為什么要加這個(gè) Bean 呢?因?yàn)樵?Spring Security 中,它是通過監(jiān)聽 session 的銷毀事件,來及時(shí)的清理 session 的記錄。用戶從不同的瀏覽器登錄后,都會(huì)有對應(yīng)的 session,當(dāng)用戶注銷登錄之后,session 就會(huì)失效,但是默認(rèn)的失效是通過調(diào)用 StandardSession#invalidate 方法來實(shí)現(xiàn)的,這一個(gè)失效事件無法被 Spring 容器感知到,進(jìn)而導(dǎo)致當(dāng)用戶注銷登錄之后,Spring Security 沒有及時(shí)清理會(huì)話信息表,以為用戶還在線,進(jìn)而導(dǎo)致用戶無法重新登錄進(jìn)來(小伙伴們可以自行嘗試不添加上面的 Bean,然后讓用戶注銷登錄之后再重新登錄)。
為了解決這一問題,我們提供一個(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è)配置,但是依然很簡單!
3.實(shí)現(xiàn)原理
上面這個(gè)功能,在 Spring Security 中是怎么實(shí)現(xiàn)的呢?我們來稍微分析一下源碼。
首先我們知道,在用戶登錄的過程中,會(huì)經(jīng)過 UsernamePasswordAuthenticationFilter(參考: Spring Security 登錄流程),而 UsernamePasswordAuthenticationFilter 中過濾方法的調(diào)用是在 AbstractAuthenticationProcessingFilter 中觸發(fā)的,我們來看下 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)證流程之后,回來之后,接下來就是調(diào)用 sessionStrategy.onAuthentication 方法,這個(gè)方法就是用來處理 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(); } } }
這段核心代碼我來給大家稍微解釋下:
看完上述內(nèi)容,是不是對Spring Security如何自動(dòng)踢掉前一個(gè)登錄用戶有進(jìn)一步的了解,如果還想學(xué)習(xí)更多內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。