中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

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

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Spring Security如何自動踢掉前一個登錄用戶

發布時間:2020-07-28 13:58:11 來源:億速云 閱讀:353 作者:小豬 欄目:編程語言

這篇文章主要講解了Spring Security如何自動踢掉前一個登錄用戶,內容清晰明了,對此有興趣的小伙伴可以學習一下,相信大家閱讀完之后會有幫助。

1.需求分析

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

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

  • 后來的登錄自動踢掉前面的登錄,就像大家在扣扣中看到的效果。
  • 如果用戶已經登錄,則不允許后來者登錄。

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

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

2.具體實現

2.1 踢掉已經登錄用戶

想要用新的登錄踢掉舊的登錄,我們只需要將最大會話數設置為 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 表示配置最大會話數為 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 已經過期,原因則是由于使用同一個用戶進行并發登錄。

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 中,它是通過監聽 session 的銷毀事件,來及時的清理 session 的記錄。用戶從不同的瀏覽器登錄后,都會有對應的 session,當用戶注銷登錄之后,session 就會失效,但是默認的失效是通過調用 StandardSession#invalidate 方法來實現的,這一個失效事件無法被 Spring 容器感知到,進而導致當用戶注銷登錄之后,Spring Security 沒有及時清理會話信息表,以為用戶還在線,進而導致用戶無法重新登錄進來(小伙伴們可以自行嘗試不添加上面的 Bean,然后讓用戶注銷登錄之后再重新登錄)。

為了解決這一問題,我們提供一個 HttpSessionEventPublisher ,這個類實現了 HttpSessionListener 接口,在該 Bean 中,可以將 session 創建以及銷毀的事件及時感知到,并且調用 Spring 中的事件機制將相關的創建和銷毀事件發布出去,進而被 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.實現原理

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

首先我們知道,在用戶登錄的過程中,會經過 UsernamePasswordAuthenticationFilter(參考: Spring Security 登錄流程),而 UsernamePasswordAuthenticationFilter 中過濾方法的調用是在 AbstractAuthenticationProcessingFilter 中觸發的,我們來看下 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 的并發問題的。具體在:

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

看完上述內容,是不是對Spring Security如何自動踢掉前一個登錄用戶有進一步的了解,如果還想學習更多內容,歡迎關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

金塔县| 徐闻县| 平凉市| 新兴县| 开原市| 射阳县| 法库县| 红桥区| 于田县| 雷州市| 漳州市| 天气| 翁源县| 淄博市| 崇文区| 昆明市| 三亚市| 台安县| 黔江区| 临夏县| 杂多县| 桦川县| 莱州市| 息烽县| 共和县| 襄汾县| 逊克县| 鸡西市| 宜春市| 南康市| 六盘水市| 托克逊县| 海兴县| 福海县| 宾阳县| 大同市| 玛曲县| 安阳市| 伊宁市| 巨野县| 乐都县|