亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

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

背景

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

@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()
                // 開(kāi)啟 session 管理
                .sessionManagement()
                // 設(shè)置同一用戶在同一時(shí)刻允許的 session 最大并發(fā)數(shù)
                .maximumSessions(1)
                // 過(guò)期會(huì)話【即被踢出的舊會(huì)話】的跳轉(zhuǎn)路徑
                .expiredUrl("/login")
        ;
    }
    
}

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

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

分析

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

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

我們先按照傳統(tǒng) web 開(kāi)發(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)用具體的子類實(shí)現(xiàn)的 attemptAuthentication 方法,嘗試進(jìn)行認(rèn)證
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				return;
			}
			
			//【????】根據(jù)配置的 session 管理策略,對(duì) session 進(jìn)行管理
			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
			// Authentication success
			if (this.continueChainBeforeSuccessfulAuthentication) {
				chain.doFilter(request, response);
			}
			// 認(rèn)證成功后的處理,主要是包含兩方面:
			// 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);
		}
	}

}

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

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

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

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

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

貼上 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)部的每一個(gè) Strategy 的 onAuthentication()
			delegate.onAuthentication(authentication, request, response);
		}
	}

接著,我們就得開(kāi)始看里面的每一個(gè)具體的 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)造器可以看出,該策略的實(shí)例【強(qiáng)依賴】于 SessionRegistry,因此,如果我們自己創(chuàng)建該策略實(shí)例,就得先擁有 SessionRegistry 實(shí)例
	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) {
		// 獲取配置的每一個(gè)用戶同一時(shí)刻允許最多登錄的端數(shù),即并發(fā)會(huì)話的個(gè)數(shù),也就是我們?cè)谂渲妙愔信渲玫?maximumSessions() 
		int allowedSessions = getMaximumSessionsForThisUser(authentication);
		if (allowedSessions == -1) {
			// We permit unlimited logins
			return;
		}
		// 從 sessionRegistry 中根據(jù)本次認(rèn)證的用戶信息,來(lái)獲取它關(guān)聯(lián)的所有 sessionid 集合
		// ?? 注意:由于比較的時(shí)候是去調(diào)用 key 對(duì)象的 hashcode(),因此如果是自己實(shí)現(xiàn)了 UserDetails 實(shí)例用于封裝用戶信息,必須要重寫 hashcode()
		List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
		int sessionCount = sessions.size();
		// 還沒(méi)有達(dá)到并發(fā)會(huì)話的最大限制數(shù),就直接 return 了
		if (sessionCount < allowedSessions) {
			// They haven't got too many login sessions running at present
			return;
		}
		// 如果當(dāng)前已有的最大會(huì)話數(shù)已經(jīng)達(dá)到了限制的并發(fā)會(huì)話數(shù),就判斷本次請(qǐng)求的會(huì)話的id,是否已經(jīng)囊括在已有的會(huì)話id集合中了,如果包含在其中,也不進(jìn)行后續(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
		}
		//【??】:如果當(dāng)前用戶所關(guān)聯(lián)的會(huì)話已經(jīng)達(dá)到了并發(fā)會(huì)話管理的限制個(gè)數(shù),并且本次的會(huì)話不再已有的會(huì)話集合中,即本次的會(huì)話是一個(gè)新創(chuàng)建的會(huì)話,那么就會(huì)走下面的方法
		// 該方法主要是計(jì)算需要踢出多少個(gè)該用戶的舊會(huì)話來(lái)為新會(huì)話騰出空間,所謂的踢出,只是將一定數(shù)量的舊會(huì)話進(jìn)行標(biāo)識(shí)為“已過(guò)期”,真正進(jìn)行踢出動(dòng)作的不是策略本身
		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ù)會(huì)話的最新一次活動(dòng)時(shí)間來(lái)排序
		sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
		// 當(dāng)前已有的會(huì)話數(shù) + 1【本次新創(chuàng)建的會(huì)話】- 限制的最大會(huì)話并發(fā)數(shù) = 需要踢出的舊會(huì)話個(gè)數(shù)
		int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
		List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
		for (SessionInformation session : sessionsToBeExpired) {
			// 將 會(huì)話 的過(guò)期標(biāo)識(shí)設(shè)置為 true
			session.expireNow();
		}
	}

}

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

過(guò)期的用戶會(huì)話并不在 ConcurrentSessionControlAuthenticationStrategy 中進(jìn)行更多的處理。而是當(dāng)這些舊會(huì)話在后續(xù)再次進(jìn)行資源請(qǐng)求時(shí),會(huì)被 Spring Security 中的一個(gè)特定的 Filter 進(jìn)行移除處理。具體是什么 Filter,我們后面會(huì)提到。

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

照樣貼源碼:

public class RegisterSessionAuthenticationStrategy implements SessionAuthenticationStrategy {

	private final SessionRegistry sessionRegistry;

	// 從構(gòu)造器可以看出,該策略的實(shí)例【強(qiáng)依賴】于 SessionRegistry,因此,如果我們自己創(chuàng)建該策略實(shí)例,就得先擁有 SessionRegistry 實(shí)例
	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) {
		//負(fù)責(zé)將本次創(chuàng)建的新會(huì)話注冊(cè)進(jìn) sessionRegistry 中
		this.sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal());
	}

}

前面我們提到說(shuō),第一個(gè)策略在進(jìn)行舊會(huì)話標(biāo)識(shí)過(guò)期狀態(tài)時(shí),會(huì)有一個(gè)計(jì)算公式計(jì)算需要標(biāo)識(shí)多少個(gè)舊會(huì)話,其中的 +1 就是因?yàn)楸敬蔚男聲?huì)話還沒(méi)被加入到 sessionRegistry 中,而新會(huì)話就是在這第三個(gè)策略執(zhí)行時(shí),才會(huì)加入其中的。
所以,這個(gè)策略的核心功能就是為了將新會(huì)話注冊(cè)進(jìn) sessionRegistry 中。

SessionRegistry

前面的兩個(gè)策略,我們從構(gòu)造器中都可以看出,這倆策略都是強(qiáng)依賴于 SessionRegistry 實(shí)例。那么這個(gè) 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 中只有一個(gè)唯一的實(shí)現(xiàn):

public class SessionRegistryImpl implements SessionRegistry, ApplicationListener<AbstractSessionEvent> {

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

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

	// <sessionId:Object,SessionInformation>
	// 存儲(chǔ)了每一個(gè)用戶會(huì)話的詳細(xì)信息,以 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 實(shí)例對(duì)象的 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 信息(其中包含了過(guò)期標(biāo)識(shí)位)
	@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);
			}
		}
	}

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

	// 注冊(cè)新的用戶會(huì)話信息
	@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;
		});
	}

	// 移除過(guò)期的用戶會(huì)話信息
	@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 主要是用來(lái)存儲(chǔ)用戶關(guān)聯(lián)的所有會(huì)話信息的統(tǒng)計(jì)容器,以及每一個(gè)會(huì)話的詳細(xì)信息也會(huì)存入其中,供各個(gè)策略進(jìn)行會(huì)話數(shù)據(jù)的查詢和調(diào)用。即SessionRegistryImpl 是一個(gè) session 的“數(shù)據(jù)中心”。

處理過(guò)期會(huì)話的過(guò)濾器

前面我們提到說(shuō),ConcurrentSessionControlAuthenticationStrategy 會(huì)對(duì)會(huì)話的并發(fā)數(shù)進(jìn)行統(tǒng)計(jì),并在必要時(shí)對(duì)一定數(shù)量的舊會(huì)話進(jìn)行過(guò)期標(biāo)識(shí)。
但它只做標(biāo)識(shí),不做具體的業(yè)務(wù)處理。
那么真正對(duì)過(guò)期會(huì)話進(jìn)行處理的是什么呢?
鏘鏘鏘鏘鏘鏘鏘!答案揭曉: 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");
	}

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

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

	// SessionInformationExpiredStrategy 的私有默認(rèn)實(shí)現(xiàn)
	// 如果我們?cè)谧远x配置類中沒(méi)有指定 expiredSessionStrategy() 的具體配置,那么就會(huì)使用這個(gè)實(shí)現(xiàn),這個(gè)實(shí)現(xiàn)不做任何業(yè)務(wù)邏輯處理,只負(fù)責(zé)打印響應(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();
		}

	}

}

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

落地實(shí)現(xiàn)

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

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

  1. 平平無(wú)奇的自定義認(rèn)證過(guò)濾器
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. 在配置類中聲明自定義的認(rèn)證過(guò)濾器實(shí)例(代碼與第3步合在了一起)
  2. 為認(rèn)證過(guò)濾器封裝 SessionAuthenticationStrategy,由于 SessionAuthenticationStrategy 是實(shí)例化需要依賴 SessionRegistry,因此也需要聲明該 Bean 實(shí)例
@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();
    }

	// 使用默認(rèn)的 SessionRegistryImpl 實(shí)現(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();
        });
        // ???????????????????????????????????????
        // 為自定義的認(rèn)證過(guò)濾器封裝 SessionAuthenticationStrategy。需要兩個(gè) Strategy 組合使用才能發(fā)揮作用
        // ConcurrentSessionControlAuthenticationStrategy -》 控制并發(fā)數(shù),讓超出的并發(fā)會(huì)話過(guò)期【ConcurrentSessionFilter 會(huì)在過(guò)期會(huì)話再次請(qǐng)求資源時(shí),將過(guò)期會(huì)話進(jìn)行 logout 操作并重定向到登錄頁(yè)面】
        // RegisterSessionAuthenticationStrategy -》注冊(cè)新會(huì)話進(jìn) SessionRegistry 實(shí)例中
        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) 方法,在其中添加上會(huì)話并發(fā)管理的相關(guān)配置,并將自定義的認(rèn)證過(guò)濾器用于替換 UsernamePasswordAuthenticationFilter 位置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	// .......
	// 省略第3步中已經(jīng)貼出來(lái)的配置代碼
	
    @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("未認(rèn)證,請(qǐng)登錄!");
                    response.getWriter().flush();
                })
                // 開(kāi)啟會(huì)話管理,設(shè)置會(huì)話最大并發(fā)數(shù)為 1
                .and().sessionManagement().maximumSessions(1)
                // 控制的是 ConcurrentSessionFilter 的 this.sessionInformationExpiredStrategy 屬性的實(shí)例化賦值對(duì)象
                .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", "當(dāng)前用戶已在其他設(shè)備登錄,請(qǐng)重新登錄!");
                    response.getWriter().println(new ObjectMapper().writeValueAsString(result));
                })
        ;
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

啟動(dòng)測(cè)試

通過(guò) apifox 的桌面版和網(wǎng)頁(yè)版,來(lái)模擬兩個(gè)客戶端去請(qǐng)求我們的系統(tǒng):

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

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

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

再訪問(wèn)受保護(hù)資源,結(jié)果如下:

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

網(wǎng)頁(yè)端訪問(wèn)受保護(hù)資源

桌面版再次訪問(wèn)受保護(hù)資源

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

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

相關(guān)文章

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

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

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

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

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

    Spring Boot與Docker部署詳解

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

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

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

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

    @RepeatSubmit是一個(gè)自定義注解,用于防止表單重復(fù)提交,它通過(guò)AOP和攔截器模式實(shí)現(xiàn),結(jié)合了線程安全和分布式環(huán)境的考慮,注解參數(shù)包括interval(間隔時(shí)間)和message(提示信息),使用時(shí)需要注意并發(fā)處理、用戶體驗(yàn)、性能和安全性等方面,失效原因是多方面的
    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ǎn),部署、調(diào)用區(qū)塊鏈合約的工程案例,本文通過(guò)流程分析給大家介紹的非常詳細(xì),需要的朋友參考下吧
    2022-01-01
  • 使用Java實(shí)現(xiàn)DNS域名解析的簡(jiǎn)單示例

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

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

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

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

    Java中的CopyOnWriteArrayList你了解嗎

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

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

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

最新評(píng)論