場景
由於在原有的系統開發前期只注重功能,而沒有考慮系統後期的架構,一個賬戶可以在多臺設備上登錄,且使用的用戶對此毫無感知,一旦發生風險後果可想而知.現在在不改動系統原有的架構情況下,實現用戶在一臺設備登錄後,在別的設備登錄時,會把上一個用戶擠掉,並且在一定時間沒有操作之後,再次操作時需要重新登錄
現有的架構中,我們使用了SpringSession+redis,自定義一個攔截器,監聽相關請求即可實現.
用戶登錄成功後,我們將用戶ID生成對應的一個token保存到緩存中,永久token則直接保存到數據庫即可,退出登錄時清除該用戶的token形成一個閉環操作
攔截器的實現方法
package com.zoomshare.controller.interceptor;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.github.pagehelper.util.StringUtil;
import com.zoomshare.baseframework.core.Constans;
import com.zoomshare.baseframework.util.BaseService;
import com.zoomshare.service.sys.ISysconfigService;
/**
* 後臺Session攔截器
* PS:免費POS機辦理VX:18670040141
* @author ShinerZhou
* @Create 2020-03-31
*/
public class WebSessionInterceptor extends BaseService implements HandlerInterceptor{
private static final org.slf4j.Logger Logger = org.slf4j.LoggerFactory.getLogger(WebSessionInterceptor.class);
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ISysconfigService sysconfigService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
//無論訪問的地址是不是正確的,都進行登錄驗證,登錄成功後的訪問再進行分發,404的訪問自然會進入到錯誤控制器中
HttpSession session = request.getSession();
String message = null;
if (session != null && session.getAttribute(Constans.USER_SESSION_ID) != null) {
try{
//驗證當前請求的session是否是已登錄的session
String loginSessionId = redisTemplate.opsForValue().get(Constans.AUTO_USER_TOKEN + session.getAttribute(Constans.USER_SESSION_ID));
if (loginSessionId != null && loginSessionId.equals(session.getId())) {
String redisTimeOut = sysconfigService.getSysconfig(Constans.TOKEN_TIME_OUT);
if (StringUtil.isEmpty(redisTimeOut)) {
redisTimeOut = "30";
}
Long timeOut = Long.parseLong(redisTimeOut) * 60;
redisTemplate.opsForValue().set(Constans.AUTO_USER_TOKEN + session.getAttribute(Constans.USER_SESSION_ID), loginSessionId, timeOut, TimeUnit.SECONDS);
return true;
}
if(loginSessionId != null && !loginSessionId.equals(session.getId())) {
message = "當前賬號已在別處被登錄";
}
if(session == null || StringUtils.isEmpty(loginSessionId)) {
message = "賬號異常,請重新登錄";
}
} catch (Exception e) {
e.printStackTrace();
}
}
Logger.info("攔截到非法Session:"+session.getId());
response401(response, message);
return false;
}
private void response401(HttpServletResponse response, String message) {
try {
String errorMessage = URLEncoder.encode(message, "UTF-8");
response.sendRedirect("/login.jsp?errorMessage="+errorMessage);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception
{
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception
{
}
}
在請求執行之前,該攔截器會獲取request請求中Headers中的用戶ID和token,拿到token後和數據庫中的token比較,一樣則放行請求,否則重定向到請求頁面重新登錄
**
preHandle方法,在請求發生前執行(攔截請求,校驗數據)。 postHandle方法,在請求完成後執行(處理業務邏輯後更新相關數據)。 afterCompletion方法將在整個請求完成之後,也就是DispatcherServlet渲染了視圖執行(請求執行成功後清理相關資源)。
**
如果其中有一個返回false則整個請求就結束,根據不同的業務場景重寫對應的方法即可,但是這裏的攔截器是針對所有的請求,我們實際中可能有些接口不需要攔截
對應如下方式過濾不需要攔截的接口
package com.zoomshare.controller.interceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebSecurityConfig implements WebMvcConfigurer {
@Bean
public WebSessionInterceptor getWebSessionInterceptor(){
return new WebSessionInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry)
{
//所有已api開頭的訪問都要進入RedisSessionInterceptor攔截器進行登錄驗證,並排除login接口(全路徑)。必須寫成鏈式,分別設置的話會創建多個攔截器。
//必須寫成getSessionInterceptor(),否則SessionInterceptor中的@Autowired會無效
registry.addInterceptor(getWebSessionInterceptor()).addPathPatterns("/**").excludePathPatterns("/system/login")
.excludePathPatterns("/storeCost/getStoreCost")
.excludePathPatterns("/api/callBack/sendResult")
.excludePathPatterns("/api/**").excludePathPatterns("/static/**");
}
}
實現原理:用戶登錄時,會生成改用戶對應的唯一有效token,之後的每次請求都會被攔截器攔截,拿到請求token後和數據庫token比較,一樣則會放行.如果用戶在操作過程中,另一個用戶使用該賬號登錄,token則會發生改變,之前的用戶再請求時就會跳轉到登錄頁面重新登錄,總之系統中的賬號就只允許在一個終端上登錄