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则会发生改变,之前的用户再请求时就会跳转到登录页面重新登录,总之系统中的账号就只允许在一个终端上登录

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