在同一個系統中,咱們可能只容許一個用戶在一個終端上登陸,通常來講這多是出於安全方面的考慮,可是也有一些狀況是出於業務上的考慮,鬆哥以前遇到的需求就是業務緣由要求一個用戶只能在一個設備上登陸。html
要實現一個用戶不能夠同時在兩臺設備上登陸,咱們有兩種思路:瀏覽器
這種思路都能實現這個功能,具體使用哪個,還要看咱們具體的需求。安全
在 Spring Security 中,這兩種都很好實現,一個配置就能夠搞定。session
想要用新的登陸踢掉舊的登陸,咱們只須要將最大會話數設置爲 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,這樣後面的登陸就會自動踢掉前面的登陸。這裏其餘的配置都是咱們前面文章講過的,我就再也不重複介紹,文末能夠下載案例完整代碼。ide
配置完成後,分別用 Chrome 和 Firefox 兩個瀏覽器進行測試(或者使用 Chrome 中的多用戶功能)。測試
This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).
能夠看到,這裏說這個 session 已通過期,緣由則是因爲使用同一個用戶進行併發登陸。ui
若是相同的用戶已經登陸了,你不想踢掉他,而是想禁止新的登陸操做,那也好辦,配置方式以下:this
@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 配置便可。此時一個瀏覽器登陸成功後,另一個瀏覽器就登陸不了了。spa
是否是很簡單?
不過還沒完,咱們還須要再提供一個 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,雖然多了一個配置,可是依然很簡單!
上面這個功能,在 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();
}
}
}
這段核心代碼我來給你們稍微解釋下:
如此,兩行簡單的配置就實現了 Spring Security 中 session 的併發管理。是否是很簡單?
轉自:https://mp.weixin.qq.com/s/9f2e4Ua2_fxEd-S9Y7DDtA