欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Spring Security 前后端分離場景下的會話并發(fā)管理

 更新時間:2025年08月15日 08:30:22   作者:阿龜在奔跑  
本文介紹了在前后端分離架構(gòu)下實現(xiàn)Spring Security會話并發(fā)管理的問題,傳統(tǒng)Web開發(fā)中只需簡單配置sessionManagement()即可實現(xiàn)會話數(shù)限制,但在前后端分離場景下,由于采用自定義認證過濾器替代了默認的UsernamePasswordAuthenticationFilter,感興趣的可以了解一下

背景

Spring Security 可以通過控制 Session 并發(fā)數(shù)量來控制同一用戶在同一時刻多端登錄的個數(shù)限制。
在傳統(tǒng)的 web 開發(fā)實現(xiàn)時,通過開啟如下的配置即可實現(xiàn)目標:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests().anyRequest().authenticated()
                .and().formLogin()
//                .and().sessionManagement().maximumSessions(1)
//                .and()
                .and().csrf().disable()
                // 開啟 session 管理
                .sessionManagement()
                // 設(shè)置同一用戶在同一時刻允許的 session 最大并發(fā)數(shù)
                .maximumSessions(1)
                // 過期會話【即被踢出的舊會話】的跳轉(zhuǎn)路徑
                .expiredUrl("/login")
        ;
    }
    
}

通過上述的配置【sessionManagement() 之后的配置】即可開啟并發(fā)會話數(shù)量管理。

然而,在前后端分離開發(fā)中,只是簡單的開啟這個配置,是無法實現(xiàn) Session 的并發(fā)會話管理的。這是我們本次要討論并解決的問題。

分析

傳統(tǒng) web 開發(fā)中的 sessionManagement 入口

由于前后端交互我們通過是采用了 application/json 的數(shù)據(jù)格式做交互,因此,前后端分離開發(fā)中,我們通常會自定義認證過濾器,即 UsernamePasswordAuthenticationFilter 的平替。這個自定義的認證過濾器,就成為了實現(xiàn)會話并發(fā)管理的最大阻礙。因此,我們有必要先參考下傳統(tǒng)的 web 開發(fā)模式中,并發(fā)會話管理的業(yè)務(wù)邏輯處理。

我們先按照傳統(tǒng) web 開發(fā)模式,走一下源碼,看看 sessionManagement 是在哪里發(fā)揮了作用:
org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) 方法中,有 sessionManagement 的入口:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
		
	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		try {
			// 調(diào)用具體的子類實現(xiàn)的 attemptAuthentication 方法,嘗試進行認證
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				return;
			}
			
			//【????】根據(jù)配置的 session 管理策略,對 session 進行管理
			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
			// Authentication success
			if (this.continueChainBeforeSuccessfulAuthentication) {
				chain.doFilter(request, response);
			}
			// 認證成功后的處理,主要是包含兩方面:
			// 1、rememberMe 功能的業(yè)務(wù)邏輯
			// 2、登錄成功的 handler 回調(diào)處理
			successfulAuthentication(request, response, chain, authenticationResult);
		}
		catch (InternalAuthenticationServiceException failed) {
			this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
			unsuccessfulAuthentication(request, response, failed);
		}
		catch (AuthenticationException ex) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, ex);
		}
	}

}

在認證過濾器的公共抽象父類的 doFilter() 中即有對 session 的管理業(yè)務(wù)邏輯入口。
在傳統(tǒng)的 web 開發(fā)模式中, this.sessionStrategy 會賦予的對象類型是CompositeSessionAuthenticationStrategy

CompositeSessionAuthenticationStrategy 翻譯過來即為 “聯(lián)合認證會話策略”,是一個套殼類,相當于一個容器,里面封裝了多個真正執(zhí)行業(yè)務(wù)邏輯的 SessionAuthenticationStrategy。

從 debug 截圖中可以看出,他里面有三個真正執(zhí)行業(yè)務(wù)邏輯的 Strategy,分別是:

  • ConcurrentSessionControlAuthenticationStrategy:并發(fā)會話控制策略
  • ChangeSessionIdAuthenticationStrategy:修改會話的 sessionid 策略【此次不會派上用場,可以忽略】
  • RegisterSessionAuthenticationStrategy:新會話注冊策略

這三個真正執(zhí)行業(yè)務(wù)邏輯處理的會話管理策略中,對于控制并發(fā)會話管理來限制同一用戶多端登錄的數(shù)量這一功能實現(xiàn)的,只需要第一個和第三個策略聯(lián)合使用,即可完成該功能實現(xiàn)。
其中,第一個策略,負責對同一用戶已有的會話進行管理,并在新會話創(chuàng)建【即該用戶通過新的客戶端登錄進系統(tǒng)】時,負責計算需要將多少舊的會話進行過期標識,并進行標識處理。
第三個策略,負責將本次創(chuàng)建的用戶新會話,給注冊進 sessionRegistry 對象中進行管理。

貼上 CompositeSessionAuthenticationStrategy.onAuthentication() 的源碼:

	@Override
	public void onAuthentication(Authentication authentication, HttpServletRequest request,
			HttpServletResponse response) throws SessionAuthenticationException {
		int currentPosition = 0;
		int size = this.delegateStrategies.size();
		for (SessionAuthenticationStrategy delegate : this.delegateStrategies) {
			if (this.logger.isTraceEnabled()) {
				this.logger.trace(LogMessage.format("Preparing session with %s (%d/%d)",
						delegate.getClass().getSimpleName(), ++currentPosition, size));
			}
			// 依次調(diào)用內(nèi)部的每一個 Strategy 的 onAuthentication()
			delegate.onAuthentication(authentication, request, response);
		}
	}

接著,我們就得開始看里面的每一個具體的 Strategy 的作用了。

ConcurrentSessionControlAuthenticationStrategy 核心業(yè)務(wù)邏輯處理

先貼源碼:

public class ConcurrentSessionControlAuthenticationStrategy
		implements MessageSourceAware, SessionAuthenticationStrategy {

	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

	private final SessionRegistry sessionRegistry;

	private boolean exceptionIfMaximumExceeded = false;

	private int maximumSessions = 1;

	// 從構(gòu)造器可以看出,該策略的實例【強依賴】于 SessionRegistry,因此,如果我們自己創(chuàng)建該策略實例,就得先擁有 SessionRegistry 實例
	public ConcurrentSessionControlAuthenticationStrategy(SessionRegistry sessionRegistry) {
		Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null");
		this.sessionRegistry = sessionRegistry;
	}


	@Override
	public void onAuthentication(Authentication authentication, HttpServletRequest request,
			HttpServletResponse response) {
		// 獲取配置的每一個用戶同一時刻允許最多登錄的端數(shù),即并發(fā)會話的個數(shù),也就是我們在配置類中配置的 maximumSessions() 
		int allowedSessions = getMaximumSessionsForThisUser(authentication);
		if (allowedSessions == -1) {
			// We permit unlimited logins
			return;
		}
		// 從 sessionRegistry 中根據(jù)本次認證的用戶信息,來獲取它關(guān)聯(lián)的所有 sessionid 集合
		// ?? 注意:由于比較的時候是去調(diào)用 key 對象的 hashcode(),因此如果是自己實現(xiàn)了 UserDetails 實例用于封裝用戶信息,必須要重寫 hashcode()
		List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
		int sessionCount = sessions.size();
		// 還沒有達到并發(fā)會話的最大限制數(shù),就直接 return 了
		if (sessionCount < allowedSessions) {
			// They haven't got too many login sessions running at present
			return;
		}
		// 如果當前已有的最大會話數(shù)已經(jīng)達到了限制的并發(fā)會話數(shù),就判斷本次請求的會話的id,是否已經(jīng)囊括在已有的會話id集合中了,如果包含在其中,也不進行后續(xù)的業(yè)務(wù)處理了,直接 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
		}
		//【??】:如果當前用戶所關(guān)聯(lián)的會話已經(jīng)達到了并發(fā)會話管理的限制個數(shù),并且本次的會話不再已有的會話集合中,即本次的會話是一個新創(chuàng)建的會話,那么就會走下面的方法
		// 該方法主要是計算需要踢出多少個該用戶的舊會話來為新會話騰出空間,所謂的踢出,只是將一定數(shù)量的舊會話進行標識為“已過期”,真正進行踢出動作的不是策略本身
		allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry);
	}

	protected int getMaximumSessionsForThisUser(Authentication authentication) {
		return this.maximumSessions;
	}

	protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions,
			SessionRegistry registry) throws SessionAuthenticationException {
		if (this.exceptionIfMaximumExceeded || (sessions == null)) {
			throw new SessionAuthenticationException(
					this.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
		// 根據(jù)會話的最新一次活動時間來排序
		sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
		// 當前已有的會話數(shù) + 1【本次新創(chuàng)建的會話】- 限制的最大會話并發(fā)數(shù) = 需要踢出的舊會話個數(shù)
		int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
		List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
		for (SessionInformation session : sessionsToBeExpired) {
			// 將 會話 的過期標識設(shè)置為 true
			session.expireNow();
		}
	}

}

主要的核心業(yè)務(wù),就是判斷本次用戶的會話是否是新創(chuàng)建的會話,并且是否超出了并發(fā)會話個數(shù)限制。如果兩個條件都滿足,那么就針對一定數(shù)量的舊會話進行標識,將它們標識為過期會話。

過期的用戶會話并不在 ConcurrentSessionControlAuthenticationStrategy 中進行更多的處理。而是當這些舊會話在后續(xù)再次進行資源請求時,會被 Spring Security 中的一個特定的 Filter 進行移除處理。具體是什么 Filter,我們后面會提到。

RegisterSessionAuthenticationStrategy 核心業(yè)務(wù)邏輯處理

照樣貼源碼:

public class RegisterSessionAuthenticationStrategy implements SessionAuthenticationStrategy {

	private final SessionRegistry sessionRegistry;

	// 從構(gòu)造器可以看出,該策略的實例【強依賴】于 SessionRegistry,因此,如果我們自己創(chuàng)建該策略實例,就得先擁有 SessionRegistry 實例
	public RegisterSessionAuthenticationStrategy(SessionRegistry sessionRegistry) {
		Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null");
		this.sessionRegistry = sessionRegistry;
	}

	@Override
	public void onAuthentication(Authentication authentication, HttpServletRequest request,
			HttpServletResponse response) {
		//負責將本次創(chuàng)建的新會話注冊進 sessionRegistry 中
		this.sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal());
	}

}

前面我們提到說,第一個策略在進行舊會話標識過期狀態(tài)時,會有一個計算公式計算需要標識多少個舊會話,其中的 +1 就是因為本次的新會話還沒被加入到 sessionRegistry 中,而新會話就是在這第三個策略執(zhí)行時,才會加入其中的。
所以,這個策略的核心功能就是為了將新會話注冊進 sessionRegistry 中。

SessionRegistry

前面的兩個策略,我們從構(gòu)造器中都可以看出,這倆策略都是強依賴于 SessionRegistry 實例。那么這個 SessionRegistry 是干嘛的呢?
照樣貼源碼如下:

public interface SessionRegistry {
	List<Object> getAllPrincipals();
	List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions);
	SessionInformation getSessionInformation(String sessionId);
	void refreshLastRequest(String sessionId);
	void registerNewSession(String sessionId, Object principal);
	void removeSessionInformation(String sessionId);
}

它在 Spring Security 中只有一個唯一的實現(xiàn):

public class SessionRegistryImpl implements SessionRegistry, ApplicationListener<AbstractSessionEvent> {

	protected final Log logger = LogFactory.getLog(SessionRegistryImpl.class);

	// <principal:Object,SessionIdSet>
	// 存儲了同一個用戶所關(guān)聯(lián)的所有 session 的 id 集合,以用戶實例對象的 hashcode() 返回值為 key
	private final ConcurrentMap<Object, Set<String>> principals;

	// <sessionId:Object,SessionInformation>
	// 存儲了每一個用戶會話的詳細信息,以 sessionId 為 key
	private final Map<String, SessionInformation> sessionIds;

	public SessionRegistryImpl() {
		this.principals = new ConcurrentHashMap<>();
		this.sessionIds = new ConcurrentHashMap<>();
	}

	public SessionRegistryImpl(ConcurrentMap<Object, Set<String>> principals,
			Map<String, SessionInformation> sessionIds) {
		this.principals = principals;
		this.sessionIds = sessionIds;
	}

	@Override
	public List<Object> getAllPrincipals() {
		return new ArrayList<>(this.principals.keySet());
	}

	// 根本 UserDetails 實例對象的 hashcode(),獲取到該用戶關(guān)聯(lián)到的所有的 Session
	@Override
	public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
		Set<String> sessionsUsedByPrincipal = this.principals.get(principal);
		if (sessionsUsedByPrincipal == null) {
			return Collections.emptyList();
		}
		List<SessionInformation> list = new ArrayList<>(sessionsUsedByPrincipal.size());
		for (String sessionId : sessionsUsedByPrincipal) {
			SessionInformation sessionInformation = getSessionInformation(sessionId);
			if (sessionInformation == null) {
				continue;
			}
			if (includeExpiredSessions || !sessionInformation.isExpired()) {
				list.add(sessionInformation);
			}
		}
		return list;
	}

	// 根據(jù) sessionId 獲取到具體的 Session 信息(其中包含了過期標識位)
	@Override
	public SessionInformation getSessionInformation(String sessionId) {
		Assert.hasText(sessionId, "SessionId required as per interface contract");
		return this.sessionIds.get(sessionId);
	}

	@Override
	public void onApplicationEvent(AbstractSessionEvent event) {
		if (event instanceof SessionDestroyedEvent) {
			SessionDestroyedEvent sessionDestroyedEvent = (SessionDestroyedEvent) event;
			String sessionId = sessionDestroyedEvent.getId();
			removeSessionInformation(sessionId);
		}
		else if (event instanceof SessionIdChangedEvent) {
			SessionIdChangedEvent sessionIdChangedEvent = (SessionIdChangedEvent) event;
			String oldSessionId = sessionIdChangedEvent.getOldSessionId();
			if (this.sessionIds.containsKey(oldSessionId)) {
				Object principal = this.sessionIds.get(oldSessionId).getPrincipal();
				removeSessionInformation(oldSessionId);
				registerNewSession(sessionIdChangedEvent.getNewSessionId(), principal);
			}
		}
	}

	// 刷新會話的最新活躍時間(用于標識過期邏輯中的排序)
	@Override
	public void refreshLastRequest(String sessionId) {
		Assert.hasText(sessionId, "SessionId required as per interface contract");
		SessionInformation info = getSessionInformation(sessionId);
		if (info != null) {
			info.refreshLastRequest();
		}
	}

	// 注冊新的用戶會話信息
	@Override
	public void registerNewSession(String sessionId, Object principal) {
		Assert.hasText(sessionId, "SessionId required as per interface contract");
		Assert.notNull(principal, "Principal required as per interface contract");
		if (getSessionInformation(sessionId) != null) {
			removeSessionInformation(sessionId);
		}
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(LogMessage.format("Registering session %s, for principal %s", sessionId, principal));
		}
		this.sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date()));
		this.principals.compute(principal, (key, sessionsUsedByPrincipal) -> {
			if (sessionsUsedByPrincipal == null) {
				sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
			}
			sessionsUsedByPrincipal.add(sessionId);
			this.logger.trace(LogMessage.format("Sessions used by '%s' : %s", principal, sessionsUsedByPrincipal));
			return sessionsUsedByPrincipal;
		});
	}

	// 移除過期的用戶會話信息
	@Override
	public void removeSessionInformation(String sessionId) {
		Assert.hasText(sessionId, "SessionId required as per interface contract");
		SessionInformation info = getSessionInformation(sessionId);
		if (info == null) {
			return;
		}
		if (this.logger.isTraceEnabled()) {
			this.logger.debug("Removing session " + sessionId + " from set of registered sessions");
		}
		this.sessionIds.remove(sessionId);
		this.principals.computeIfPresent(info.getPrincipal(), (key, sessionsUsedByPrincipal) -> {
			this.logger.debug(
					LogMessage.format("Removing session %s from principal's set of registered sessions", sessionId));
			sessionsUsedByPrincipal.remove(sessionId);
			if (sessionsUsedByPrincipal.isEmpty()) {
				// No need to keep object in principals Map anymore
				this.logger.debug(LogMessage.format("Removing principal %s from registry", info.getPrincipal()));
				sessionsUsedByPrincipal = null;
			}
			this.logger.trace(
					LogMessage.format("Sessions used by '%s' : %s", info.getPrincipal(), sessionsUsedByPrincipal));
			return sessionsUsedByPrincipal;
		});
	}

}

從源碼中可以看出,SessionRegistryImpl 主要是用來存儲用戶關(guān)聯(lián)的所有會話信息的統(tǒng)計容器,以及每一個會話的詳細信息也會存入其中,供各個策略進行會話數(shù)據(jù)的查詢和調(diào)用。即SessionRegistryImpl 是一個 session 的“數(shù)據(jù)中心”。

處理過期會話的過濾器

前面我們提到說,ConcurrentSessionControlAuthenticationStrategy 會對會話的并發(fā)數(shù)進行統(tǒng)計,并在必要時對一定數(shù)量的舊會話進行過期標識。
但它只做標識,不做具體的業(yè)務(wù)處理。
那么真正對過期會話進行處理的是什么呢?
鏘鏘鏘鏘鏘鏘鏘!答案揭曉: ConcurrentSessionFilter

public class ConcurrentSessionFilter extends GenericFilterBean {

	private final SessionRegistry sessionRegistry;

	private String expiredUrl;

	private RedirectStrategy redirectStrategy;

	private LogoutHandler handlers = new CompositeLogoutHandler(new SecurityContextLogoutHandler());

	private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;

	@Override
	public void afterPropertiesSet() {
		Assert.notNull(this.sessionRegistry, "SessionRegistry required");
	}

	// 過濾器嘛,最重要的就是這個方法啦
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
	}

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		// 獲取本次請求的 session
		HttpSession session = request.getSession(false);
		if (session != null) {
			// 判斷當前會話是否已經(jīng)注冊進了 sessionRegistry 數(shù)據(jù)中心
			SessionInformation info = this.sessionRegistry.getSessionInformation(session.getId());
			if (info != null) {
				// 如果已在 sessionRegistry,那么判斷下該 session 是否已經(jīng)過期了【通過前面提到的過期標識位來判斷】
				if (info.isExpired()) {
					// Expired - abort processing
					this.logger.debug(LogMessage
							.of(() -> "Requested session ID " + request.getRequestedSessionId() + " has expired."));
					// 對于已經(jīng)過期的會話,就不放行本次請求了,而是對本次會話進行 logout 處理,即注銷登錄處理【具體實現(xiàn)看下一個方法】
					doLogout(request, response);
					// 調(diào)用會話過期處理策略進行過期業(yè)務(wù)處理
					// 如果在自定義的配置類中有顯式聲明了SessionManagementConfigurer.expiredSessionStrategy() 配置,那么此處就會去回調(diào)我們聲明的策略實現(xiàn)
					this.sessionInformationExpiredStrategy
							.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
					return;
				}
				// Non-expired - update last request date/time
				// 如果會話沒有過期,就刷新該會話的最新活躍時間【用于淘汰過期會話時排序使用】
				this.sessionRegistry.refreshLastRequest(info.getSessionId());
			}
		}
		chain.doFilter(request, response);
	}

	private void doLogout(HttpServletRequest request, HttpServletResponse response) {
		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
		// 執(zhí)行 logout 操作,包括移除會話、重定向到登錄請求、返回注銷成功的 json 數(shù)據(jù)等
		this.handlers.logout(request, response, auth);
	}

	// SessionInformationExpiredStrategy 的私有默認實現(xiàn)
	// 如果我們在自定義配置類中沒有指定 expiredSessionStrategy() 的具體配置,那么就會使用這個實現(xiàn),這個實現(xiàn)不做任何業(yè)務(wù)邏輯處理,只負責打印響應(yīng)日志
	private static final class ResponseBodySessionInformationExpiredStrategy
			implements SessionInformationExpiredStrategy {

		@Override
		public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {
			HttpServletResponse response = event.getResponse();
			response.getWriter().print("This session has been expired (possibly due to multiple concurrent "
					+ "logins being attempted as the same user).");
			response.flushBuffer();
		}

	}

}

通過上面的 doFilter() 解讀,可以看出它主要是對每次請求所綁定的會話進行過期判斷,并針對過期會話進行特定處理。

落地實現(xiàn)

好了,現(xiàn)在傳統(tǒng) web 開發(fā)的會話并發(fā)管理源碼已經(jīng)解讀完畢了。下一步,我們該來實現(xiàn)前后端分離中的會話并發(fā)管理功能了。
從上述的分析中我們可知,在認證過濾器【AbstractAuthenticationProcessingFilter】中,當認證通過后,就會針對 Session 會話進行邏輯處理。
而在 UsernamePasswordAuthenticationFilter 中,使用的是聯(lián)合會話處理策略,其中有兩個會話處理策略是我們必須要有的。因此,在我們前后端分離開發(fā)時,由于我們自定義了認證過濾器用來取代UsernamePasswordAuthenticationFilter,因此,我們需要給我們自定義的認證過濾器封裝好對應(yīng)的SessionAuthenticationStrategy。

前后端分離開發(fā)的實現(xiàn)步驟:

  1. 平平無奇的自定義認證過濾器
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    @Autowired
    private SessionRegistry sessionRegistry;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        if (MediaType.APPLICATION_JSON_VALUE.equals(request.getContentType()) ||
                MediaType.APPLICATION_JSON_UTF8_VALUE.equals(request.getContentType())) {
            try {
                Map<String, String> map = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String username = map.get(getUsernameParameter());
                String password = map.get(getPasswordParameter());
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                // Allow subclasses to set the "details" property
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return super.attemptAuthentication(request, response);
    }

}

  1. 在配置類中聲明自定義的認證過濾器實例(代碼與第3步合在了一起)
  2. 為認證過濾器封裝 SessionAuthenticationStrategy,由于 SessionAuthenticationStrategy 是實例化需要依賴 SessionRegistry,因此也需要聲明該 Bean 實例
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(User.builder().username("root").password("{noop}123").authorities("admin").build());
        return userDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

	// 使用默認的 SessionRegistryImpl 實現(xiàn)類作為 Bean 類型即可
    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }
    
	@Bean
    public LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setFilterProcessesUrl("/login");
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
            response.setStatus(200);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println("登錄成功!");
        });
        loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
            response.setStatus(500);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println("登錄失敗!");
            response.getWriter().println(exception.getMessage());
            response.getWriter().flush();
        });
        // ???????????????????????????????????????
        // 為自定義的認證過濾器封裝 SessionAuthenticationStrategy。需要兩個 Strategy 組合使用才能發(fā)揮作用
        // ConcurrentSessionControlAuthenticationStrategy -》 控制并發(fā)數(shù),讓超出的并發(fā)會話過期【ConcurrentSessionFilter 會在過期會話再次請求資源時,將過期會話進行 logout 操作并重定向到登錄頁面】
        // RegisterSessionAuthenticationStrategy -》注冊新會話進 SessionRegistry 實例中
        ConcurrentSessionControlAuthenticationStrategy strategy1 = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
        RegisterSessionAuthenticationStrategy strategy2 = new RegisterSessionAuthenticationStrategy(sessionRegistry());
        CompositeSessionAuthenticationStrategy compositeStrategy = new CompositeSessionAuthenticationStrategy(Arrays.asList(strategy1, strategy2));
        loginFilter.setSessionAuthenticationStrategy(compositeStrategy);
        return loginFilter;
    }
   
}
  1. 重寫配置類的 configure(HttpSecurity http) 方法,在其中添加上會話并發(fā)管理的相關(guān)配置,并將自定義的認證過濾器用于替換 UsernamePasswordAuthenticationFilter 位置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	// .......
	// 省略第3步中已經(jīng)貼出來的配置代碼
	
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests().anyRequest().authenticated()
                .and().csrf().disable()
                .exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
                    response.setStatus(401);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().println("未認證,請登錄!");
                    response.getWriter().flush();
                })
                // 開啟會話管理,設(shè)置會話最大并發(fā)數(shù)為 1
                .and().sessionManagement().maximumSessions(1)
                // 控制的是 ConcurrentSessionFilter 的 this.sessionInformationExpiredStrategy 屬性的實例化賦值對象
                .expiredSessionStrategy(event -> {
                    HttpServletResponse response = event.getResponse();
                    response.setContentType("application/json;charset=UTF-8");
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    Map<String, String> result = new HashMap<>();
                    result.put("msg", "當前用戶已在其他設(shè)備登錄,請重新登錄!");
                    response.getWriter().println(new ObjectMapper().writeValueAsString(result));
                })
        ;
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

啟動測試

通過 apifox 的桌面版和網(wǎng)頁版,來模擬兩個客戶端去請求我們的系統(tǒng):

首先,在系統(tǒng)中設(shè)置了受保護資源 /hello,并進行訪問,結(jié)果返回如下:

網(wǎng)頁版也會返回相同內(nèi)容。

接著,在桌面版先進行用戶信息登錄,結(jié)果如下:

再訪問受保護資源,結(jié)果如下:

網(wǎng)頁端作為另一個客戶端,也用同一用戶進行系統(tǒng)登錄

網(wǎng)頁端訪問受保護資源

桌面版再次訪問受保護資源

從結(jié)果截圖中可以看出,由于我們設(shè)置了會話最大并發(fā)數(shù)為1,當網(wǎng)頁端利用同一用戶進行登錄時,原本已經(jīng)登錄了的桌面版apifox客戶端就會被擠兌下線,無法訪問受保護資源。
響應(yīng)的內(nèi)容來源于我們在配置類中配置的 expiredSessionStrategy() 處理策略。

到此這篇關(guān)于Spring Security 前后端分離場景下的會話并發(fā)管理的文章就介紹到這了,更多相關(guān)Spring Security 前后端分離會話并發(fā)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • springboot通過注解、接口創(chuàng)建定時任務(wù)詳解

    springboot通過注解、接口創(chuàng)建定時任務(wù)詳解

    使用SpringBoot創(chuàng)建定時任務(wù)其實是挺簡單的,這篇文章主要給大家介紹了關(guān)于springboot如何通過注解、接口創(chuàng)建這兩種方法實現(xiàn)定時任務(wù)的相關(guān)資料,需要的朋友可以參考下
    2021-07-07
  • SpringCloud服務(wù)注冊和發(fā)現(xiàn)組件Eureka

    SpringCloud服務(wù)注冊和發(fā)現(xiàn)組件Eureka

    對于微服務(wù)的治理而言,其核心就是服務(wù)的注冊和發(fā)現(xiàn)。在SpringCloud 中提供了多種服務(wù)注冊與發(fā)現(xiàn)組件,官方推薦使用Eureka。本篇文章,我們來講解springcloud的服務(wù)注冊和發(fā)現(xiàn)組件,感興趣的可以了解一下
    2021-05-05
  • Spring Boot與Docker部署詳解

    Spring Boot與Docker部署詳解

    本篇文章主要介紹了Spring Boot與Docker部署詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-08-08
  • 詳解關(guān)于Windows10 Java環(huán)境變量配置問題的解決辦法

    詳解關(guān)于Windows10 Java環(huán)境變量配置問題的解決辦法

    這篇文章主要介紹了關(guān)于Windows10 Java環(huán)境變量配置問題的解決辦法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-03-03
  • java如何防止表單重復(fù)提交的注解@RepeatSubmit

    java如何防止表單重復(fù)提交的注解@RepeatSubmit

    @RepeatSubmit是一個自定義注解,用于防止表單重復(fù)提交,它通過AOP和攔截器模式實現(xiàn),結(jié)合了線程安全和分布式環(huán)境的考慮,注解參數(shù)包括interval(間隔時間)和message(提示信息),使用時需要注意并發(fā)處理、用戶體驗、性能和安全性等方面,失效原因是多方面的
    2024-11-11
  • Spring?Boot?整合?Fisco?Bcos部署、調(diào)用區(qū)塊鏈合約的案例

    Spring?Boot?整合?Fisco?Bcos部署、調(diào)用區(qū)塊鏈合約的案例

    本篇文章介紹?Spring?Boot?整合?Fisco?Bcos?的相關(guān)技術(shù),最最重要的技術(shù)點,部署、調(diào)用區(qū)塊鏈合約的工程案例,本文通過流程分析給大家介紹的非常詳細,需要的朋友參考下吧
    2022-01-01
  • 使用Java實現(xiàn)DNS域名解析的簡單示例

    使用Java實現(xiàn)DNS域名解析的簡單示例

    這篇文章主要介紹了使用Java實現(xiàn)DNS域名解析的簡單示例,包括對一個動態(tài)IP主機的域名解析例子,需要的朋友可以參考下
    2015-10-10
  • Java中類的定義和初始化示例詳解

    Java中類的定義和初始化示例詳解

    這篇文章主要給大家介紹了關(guān)于Java中類的定義和初始化的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2021-01-01
  • Java中的CopyOnWriteArrayList你了解嗎

    Java中的CopyOnWriteArrayList你了解嗎

    CopyOnWriteArrayList是Java集合框架中的一種線程安全的List實現(xiàn),這篇文章主要來和大家聊聊CopyOnWriteArrayList的簡單使用,需要的可以參考一下
    2023-06-06
  • MyBatis中使用#{}和${}占位符傳遞參數(shù)的各種報錯信息處理方案

    MyBatis中使用#{}和${}占位符傳遞參數(shù)的各種報錯信息處理方案

    這篇文章主要介紹了MyBatis中使用#{}和${}占位符傳遞參數(shù)的各種報錯信息處理方案,分別介紹了兩種占位符的區(qū)別,本文給大家介紹的非常詳細,需要的朋友可以參考下
    2024-01-01

最新評論