有时候我们想禁止一个账号在多个地方登录,以防止账号被滥用的情况,那么如果实现这种效果呢?一般有两种实现效果,一种是如果账号已登录,则后续不能再登录,除非此账号已超时或注销登录;第二种是在登录时提示该账号已在其他地方登录,是否继续登录。
第一种实现不太友好,如果这个账号一直不退出,则其他人一直无法登录,下面展示第2种的实现方式。
– 实现原理:
通过实现HttpSessionListener的HttpSessionAttributeListener接口和HttpSessionAttributeListener的attributeAdded接口来更新在线用户列表,然后登录时检查此在线用户列表,如果列表中有当前登录用户则提示“该账号已在其他地方登录,是否继续”。
实现逻辑:
-
通过监听器维护一个在线用户列表容器,容器存放用户的LoginName, HttpSession键值对。
-
通过实现HttpSessionAttributeListener的attributeAdded接口来更新在线用户列表,只要向Session中添加属性便会调用此方法。
一般在登录时会向Session中存放一个用户信息,在attributeAdded中判断如果存放到session中的属性为用户登录的属性名,则将该session放入在线用户列表容器中。
此处不使用HttpSessionListener的sessionCreated的原因是,浏览器打开时会产生一个session,但是当用户登录时如果创建session不加true参数不会产生一个新的session,此时不关闭浏览器,注销当前用户再登录下一个用户,不会产生一个新的session,因为这时不会调用sessionCreated方法。此处和用户的登录代码有一定关系。 -
通过sessionDestroyed方法实现在线用户列表的移除,即当用户注销或者session超时便会调用此方法。
-
编写检查登录账号是否已经在在线用户列表容器中的接口,注意此处要先验证账号密码是否正确,如果账号密码不正确则继续走接下去的登录逻辑,不提示账号密码错误,因为此处验证账号密码只是做为是否提示账号在他处登录的条件。
-
在前端登录时调用判断账号是否可以多点登录的接口。
PS:此代码为单机版实现,如果为集群环境,请将在线用户列表放入Redis或其他地方进行维护。
1、编写在线用户列表容器类
package com.gsafety.iams.listener;
import java.util.Hashtable;
import java.util.Map;
import javax.servlet.http.HttpSession;
public class OnlineUserList {
private static Map<String, HttpSession> onlineUsers = new Hashtable<String, HttpSession>();
private OnlineUserList() {
super();
}
public static synchronized void put(String loginName, HttpSession session) {
onlineUsers.put(loginName, session);
}
public static synchronized HttpSession get(String loginName) {
return onlineUsers.get(loginName);
}
public static synchronized boolean containsKey(String loginName) {
return onlineUsers.containsKey(loginName);
}
public static synchronized void remove(String loginName) {
onlineUsers.remove(loginName);
}
}
2.、 实现HttpSessionListener和HttpSessionAttributeListener
package com.gsafety.iams.listener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.apache.log4j.Logger;
import com.gsafety.cloudframework.config.po.Configuration;
import com.gsafety.cloudframework.config.util.ConfigCacheUtil;
import com.gsafety.cloudframework.runtime.vo.SessionBean;
public class UserLoginSessionListener implements HttpSessionListener, HttpSessionAttributeListener {
private static final Logger LOG = Logger.getLogger(UserLoginSessionListener.class);
private static final String SESSION_BEAN = "session_bean";
@Override
public void sessionCreated(HttpSessionEvent e) {
}
@Override
public void sessionDestroyed(HttpSessionEvent e) {
HttpSession session = e.getSession();
String loginName = getLoginName(session);
OnlineUserList.remove(loginName);
LOG.info(String.format("【%s】已注销..", loginName));
}
private String getLoginName(HttpSession session) {
SessionBean sessionBean = (SessionBean) session.getAttribute("session_bean");
if (sessionBean == null)
return null;
return sessionBean.getUser().getLoginName();
}
/**
* 通过session属性的变化来判断用户是否已在其他地方登录
*/
@Override
public void attributeAdded(HttpSessionBindingEvent e) {
String attrName = e.getName();
if (SESSION_BEAN.equals(attrName)) {
HttpSession session = e.getSession();
String loginName = getLoginName(session);
//限制多点登录,并且用户已在其他设备登录
if (OnlineUserList.containsKey(loginName) && isSessionLimit()) {
HttpSession ss = OnlineUserList.get(loginName);
//将session置为失效
ss.invalidate();
}
OnlineUserList.put(loginName, session);
LOG.info(String.format("登录成功,用户:【%s】", loginName));
}
}
private boolean isSessionLimit() {
Boolean sessionLimit = false;
Configuration config = ConfigCacheUtil.getConf("system.session.limit");
if (config != null) {
sessionLimit = Boolean.valueOf(config.getValue());
}
return sessionLimit;
}
@Override
public void attributeRemoved(HttpSessionBindingEvent e) {
}
@Override
public void attributeReplaced(HttpSessionBindingEvent e) {
}
}
3、 编写action
package com.gsafety.iams.actions;
import org.apache.commons.lang.StringUtils;
import org.apache.struts2.convention.annotation.Namespace;
import org.apache.struts2.convention.annotation.ParentPackage;
import org.apache.struts2.convention.annotation.Result;
import org.apache.struts2.convention.annotation.Results;
import com.gsafety.cloudframework.common.base.util.ActionUtil;
import com.gsafety.cloudframework.common.base.util.encrypt.DESCoder;
import com.gsafety.cloudframework.common.base.util.encrypt.MD5Digester;
import com.gsafety.cloudframework.config.po.Configuration;
import com.gsafety.cloudframework.config.util.ConfigCacheUtil;
import com.gsafety.cloudframework.user.facade.EmsUserFacade;
import com.gsafety.cloudframework.user.po.EmsUser;
import com.gsafety.iams.listener.OnlineUserList;
import com.opensymphony.xwork2.ActionSupport;
@ParentPackage("json-default")
@Namespace("/sys/user")
@Results(value = { @Result(name = "checkCanMultiLogin", type = "json", params= {"root", "canMultiLogin"}) })
public class LoginControlAction extends ActionSupport {
private static final long serialVersionUID = 1L;
private String loginName; //账号
private String password; //密码
private boolean canMultiLogin;
/**
* 是否允许多处登录
* @return
*/
public String checkCanMultiLogin() {
canMultiLogin = true; //默认为允许
if (checkUserPassword()) {
//已存在登录用户,并且系统禁止多点登录
if (OnlineUserList.containsKey(loginName) && isSessionLimit()) {
canMultiLogin = false;;
}
}
return "checkCanMultiLogin";
}
/**
* 是否禁止session登录,从配置中读取。
* @return
*/
private boolean isSessionLimit() {
Boolean sessionLimit = false;
//此处从配置中读取是否控制多点登录,true为控制,false不控制
//......代码省略,自己实现
return sessionLimit;
}
/**
* 验证用户账号密码
* @return
*/
private boolean checkUserPassword() {
//验证账号密码是否正确,代码省略
//......
return false;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public boolean isCanMultiLogin() {
return canMultiLogin;
}
public void setCanMultiLogin(boolean canMultiLogin) {
this.canMultiLogin = canMultiLogin;
}
}
4、 在web.xml中配置监听器
<listener>
<listener-class>com.gsafety.iams.listener.UserLoginSessionListener</listener-class>
</listener>
5、 修改前端登录,下面是JS调用Action判断是否允许多点登录的方法
function checkCanMultiLogin(loginName, finalPass) {
var canMultiLogin = false;
jQuery.ajax({
type: "POST",
url: "${base}/sys/user/login-control!checkCanMultiLogin.do",
data: {'loginName':loginName,'password':finalPass},
async: false,
success: function(data) {
canMultiLogin = data;
}
});
return canMultiLogin;
}
6.、在登录之前添加如下判断
//检查是否允许多点同时登录
var canMultiLogin = checkCanMultiLogin(loginName, finalPass);
if (!canMultiLogin || canMultiLogin == 'false') {
if(!confirm("当前账号已在其他地方登录,是否继续")) {
return false;
}
}