由于在原有的系统开发前期只注重功能,而没有考虑系统后期的架构,一个账户可以在多台设备上登录,且使用的用户对此毫无感知,一旦发生风险后果可想而知.现在在不改动系统原有的架构情况下,实现用户在一台设备登录后,在别的设备登录时,会把上一个用户挤掉,并且在一定时间没有操作之后,再次操作时需要重新登录
现有的架构中,我们使用了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比较,一样则放行请求,否则重定向到请求页面重新登录
**
**
如果其中有一个返回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则会发生改变,之前的用户再请求时就会跳转到登录页面重新登录,总之系统中的账号就只允许在一个终端上登录