在上一篇文章中,介绍了使用session监听+spring MVC拦截器禁止用户重复登录 。但随着用户数量的增大,需要采用多服务器构建负载均衡,以分担大量用户访问对系统造成的压力。此时为了禁止用户重复登录,使用session监听+Spring MVC拦截器的方式存在一定问题,因为用户登录后路由到哪台服务器具有不确定性,比如用户第一次登录后被路由到服务器A,不能保证用户1小时后重复登录后被路由到服务器A。因此通过服务器缓存的sesson集合判断用户是否已经登录过不可行。
而通过数据库+Spring MVC拦截器的的基本思路是:建立一张用户在线表(UserOnLine),用户每次登录时,记录当前sessionID,或者用户第几次登录version(本文记录version)。在Spring MVC拦截器中校验数据库中version是否和当前session中的version相等,不相等强制session过期,并提示用户重复登录,强制退出到登录界面。
具体如下:
1、UserOnLine表实体(项目中使用hibernate注解方式)
package com.cnpc.base.user.model;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
import org.hibernate.annotations.GenericGenerator;
@Entity
@Table(name = "USER_ONLINE")
@JsonIgnoreProperties(value = { "hibernateLazyInitializer", "handler","fieldHandler" })
public class UserOnLine implements Serializable {
/**
*
*/
private static final long serialVersionUID = 5030026318119472029L;
@Id
@Column(name = "id", length = 36)
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
@JsonProperty("id")
private String id;
/** 用户ID**/
@Column(name = "userid", length = 36)
private String userid;
/** 登录时间 **/
@Column(name = "loginTime")
private Date loginTime;
/**
* 用户登录次数
*/
@Column(name = "version")
private int version;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserid() {
return userid;
}
public void setUserid(String userid) {
this.userid = userid;
}
public Date getLoginTime() {
return loginTime;
}
public void setLoginTime(Date loginTime) {
this.loginTime = loginTime;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
}
2、用户成功登录后,更新user_online表
public void sessionHandlerByTable(HttpSession session){
String userid=session.getAttribute("userid").toString();
UserOnLine user=userService.getUserOnLineByUserId(userid);
if(user==null){
user=new UserOnLine();
user.setLoginTime(new Date());
user.setUserid(userid);
user.setVersion(1);
user=(UserOnLine)userService.save(user);
}
else{
user.setLoginTime(new Date());
user.setVersion(user.getVersion()+1);
user=(UserOnLine)userService.update(user);
}
session.setAttribute("version", user.getVersion());//session中保存用户登录次数
}
3、Spring MVC拦截器authIntercepter(拦截器的配置见上篇文章)
package com.cnpc.framework.interceptor;
import java.io.PrintWriter;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.cnpc.base.user.service.UserService;
import com.cnpc.framework.common.SessionContainer;
@Component("SpringMVCInterceptor")
public class AuthInterceptor extends HandlerInterceptorAdapter {
@Resource
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
// 后台session控制
String[] noFilters = new String[] { "/auth/login", "/auth/logout" };
String uri = request.getRequestURI();
boolean beFilter = true;
for (String s : noFilters) {
if (uri.indexOf(s) != -1) {
beFilter = false;
break;
}
}
SessionContainer sessionContainer = (SessionContainer) request.getSession().getAttribute("SessionContainer");
if (beFilter) {
if (null == sessionContainer) {
if (request.getHeader("x-requested-with") != null
&& request.getHeader("x-requested-with").equalsIgnoreCase("XMLHttpRequest"))// 如果是ajax请求响应头会有,x-requested-with;
{
response.setHeader("sessionstatus", "timeout");// 在响应头设置session状态
return false;
}
// 未登录
PrintWriter out = response.getWriter();
StringBuilder builder = new StringBuilder();
builder.append("<script type=\"text/javascript\" charset=\"UTF-8\">");
builder.append("alert(\"页面过期,请重新登录\");");
builder.append("window.top.location.href='/auth/logout';");
builder.append("</script>");
out.print(builder.toString());
out.close();
return false;
} else {
int version=userService.getUserOnLineByUserId(request.getSession().getAttribute("userid").toString()).getVersion();
if(version!=Integer.parseInt(request.getSession().getAttribute("version").toString())){
//强制session超时
request.getSession().invalidate();
if (request.getHeader("x-requested-with") != null
&& request.getHeader("x-requested-with").equalsIgnoreCase("XMLHttpRequest"))// 如果是ajax请求响应头会有,x-requested-with;
{
response.setHeader("sessionstatus", "repeatlogin");// 在响应头设置session状态
return false;
}
PrintWriter out = response.getWriter();
StringBuilder builder = new StringBuilder();
builder.append("<script type=\"text/javascript\" charset=\"UTF-8\">");
builder.append("alert(\"您的帐号已在其他机器登录,请重新登录\");");
builder.append("window.top.location.href='/auth/logout';");
builder.append("</script>");
out.print(builder.toString());
out.close();
return false;
}
// 添加系统日志
// -----------------------------------
// -----------------------------------
}
}
Map paramsMap = request.getParameterMap();
return super.preHandle(request, response, handler);
}
}
在客户端以ajax方式同服务器交互时,客户端还要处理session过期后的跳转,其代码如下(放入公共js文件中)
$.ajaxSetup({
contentType:"application/x-www-form-urlencoded;charset=utf-8",
complete:function(XMLHttpRequest,textStatus){
var sessionstatus=XMLHttpRequest.getResponseHeader("sessionstatus"); // 通过XMLHttpRequest取得响应头,sessionstatus,
if(sessionstatus=="timeout"){
// 如果超时就处理 ,指定要跳转的页面
alert("页面过期,请重新登录");
window.top.location.href="/auth/logout";
}
if(sessionstatus=="repeatlogin"){
alert("您的帐号已在其他机器登录,请重新登录");
window.top.location.href="/auth/logout";
}
}
}
);
以上方式实现了负载均衡下,禁止用户重复登录的功能。当然也可用在单机服务器上。唯一的不足是每次要从数据库中取出该用户的version同session中的version比对,会损失一部分性能。