单态登陆,或者成为单一登陆,就是一个账号只能在一台机器上登录,如果在其他机器上登陆了,则原来的登陆自动失效。单态登陆的目的是为了防止多台机器同时使用一个账号。
本例使用一个简单的JSP页面来模拟登陆情况。如果session中有userInfo信息,则表示已经登陆,页面将显示登陆后的账号。如果session中没有userInfo信息,则表示没有登录,页面将显示登陆输入框。登陆与注销动作都在在JSP中完成,代码如下,
<%@ page import="model.UserInfo" %>
<%@ page import="java.util.Date" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%
String action = request.getParameter("action");
String account = request.getParameter("account");
UserInfo userInfo = (UserInfo)session.getAttribute("userInfo");
if("login".equals(action)){
//登陆,将userInfo放入session
userInfo = new UserInfo();
userInfo.setAccount(account);
userInfo.setIp(request.getRemoteAddr());
userInfo.setLoginDate(new Date());
session.setAttribute("userInfo",userInfo);
response.sendRedirect(response.encodeRedirectURL(request.getRequestURI()));
}else if("logout".equals(action)){
//注销,将userInfo从session中移除
session.removeAttribute("userInfo");
response.sendRedirect(response.encodeRedirectURL(request.getRequestURI()));
}
%>
<%
if(userInfo!=null)
{
%>
欢迎您:<%=userInfo.getAccount()%><br>
您的登陆IP为:<%=userInfo.getIp()%><br>
登陆时间为:<%=userInfo.getLoginDate()%><br>
<a href="<%=request.getRequestURI()%>?action=logout">退出</a>
<script>setTimeout("location=location;",5000)</script>
<%
}
else
{
%>
<p style="color: red"><%=session.getAttribute("msg")%></p>
<form action="<%=request.getRequestURI()%>?action=login" method="post">
账号<input type="text" name="account" >
<input type="submit" value="登陆">
</form>
<%
}
%>
</body>
</html>
为了保证信息的 实时性,登陆后的代码中添加了一句JavaScript代码,每隔5秒刷新一次页面。这样,如果账号在别处登陆,5秒之内就会显示被迫下线的信息。userInfo为简单的JavaBean,用于记录登录者信息,代码如下
package model;
import java.io.Serializable;
import java.util.Date;
public class UserInfo implements Serializable {
private String account;
private String ip;
private Date loginDate;
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public Date getLoginDate() {
return loginDate;
}
public void setLoginDate(Date loginDate) {
this.loginDate = loginDate;
}
}
单态登陆的代码写在Listener中,通过监听Session和userInfo属性的添加与删除实现。如果新添加了一个userInfo属性,则认为是新登录用户,Listener中查看该用户的账号是否在其他机器上登录过。如果已经登陆,则让旧的登陆信息失效。Listener中使用一个Map把所有包含userInfo信息的Session收集起来。代码如下
package listener;
import model.UserInfo;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import java.util.HashMap;
import java.util.Map;
@WebListener()
public class LoginSessionListener implements HttpSessionAttributeListener {
Log log = LogFactory.getLog(this.getClass());
//用于保存全部session的map
Map<String,HttpSession> map = new HashMap<>();
@Override
public void attributeAdded(HttpSessionBindingEvent httpSessionBindingEvent) {
String name = httpSessionBindingEvent.getName();
//登陆
if(name.equals("userInfo")){
UserInfo userInfo = (UserInfo)httpSessionBindingEvent.getValue();
if(map.get(userInfo.getAccount())!=null){
//map中有记录,表明在其他机器中登陆过,将以前的登陆失效
HttpSession session = map.get(userInfo.getAccount());
UserInfo oldInfo = (UserInfo)session.getAttribute("userInfo");
log.info("账号:"+oldInfo.getAccount()+"在"+oldInfo.getIp()+"已经登陆,该登陆将被迫下线");
session.removeAttribute("userInfo");
session.setAttribute("msg","您的账号在其他机器上登录,您被迫下线");
}
//将Session以用户名为索引,放入map中
map.put(userInfo.getAccount(),httpSessionBindingEvent.getSession());
log.info("账号:"+userInfo.getAccount()+"在"+userInfo.getIp()+"登陆");
}
}
@Override
public void attributeRemoved(HttpSessionBindingEvent httpSessionBindingEvent) {
String name = httpSessionBindingEvent.getName();
//注销
if(name.equals("userInfo")){
//将该session从map移除
UserInfo userInfo = (UserInfo)httpSessionBindingEvent.getValue();
map.remove(userInfo.getAccount());
log.info("账号:"+userInfo.getAccount()+"注销");
}
}
@Override
public void attributeReplaced(HttpSessionBindingEvent httpSessionBindingEvent) {
String name = httpSessionBindingEvent.getName();
//没有注销的情况下用另一个账号登陆
if(name.equals("userInfo")){
//移除旧的登陆信息
UserInfo oldInfo = (UserInfo)httpSessionBindingEvent.getValue();
map.remove(oldInfo.getAccount());
//新的登陆信息
UserInfo userInfo = (UserInfo)httpSessionBindingEvent.getSession().getAttribute("userInfo");
//也要检查新登录的账号是否在别的机器上登陆过
if(map.get(userInfo.getAccount())!=null){
//map中有记录,表明在其他机器中登陆过,将以前的登陆失效
HttpSession session = map.get(userInfo.getAccount());
session.removeAttribute("userInfo");
session.setAttribute("msg","您的账号在其他机器上登录,您被迫下线");
}
//将Session以用户名为索引,放入map中
map.put(userInfo.getAccount(),httpSessionBindingEvent.getSession());
log.info("账号:"+userInfo.getAccount()+"在"+userInfo.getIp()+"登陆");
}
}
}
在web.xml中配置Listener,代码如下
<listener>
<listener-class>listener.LoginSessionListener</listener-class>
</listener>
最后一张为账号在其他浏览器登陆后的效果。