SpringBoot2.0通過Session實現用戶後臺唯一登錄

場景

由於在原有的系統開發前期只注重功能,而沒有考慮系統後期的架構,一個賬戶可以在多臺設備上登錄,且使用的用戶對此毫無感知,一旦發生風險後果可想而知.現在在不改動系統原有的架構情況下,實現用戶在一臺設備登錄後,在別的設備登錄時,會把上一個用戶擠掉,並且在一定時間沒有操作之後,再次操作時需要重新登錄
現有的架構中,我們使用了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則會發生改變,之前的用戶再請求時就會跳轉到登錄頁面重新登錄,總之系統中的賬號就只允許在一個終端上登錄

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章