单态登陆
单态登陆,或者称为单一登陆,就是一个账号只能在一台机器上登陆,如果在其他机器上登陆了,则原来的登陆失效。单态登陆的目的是防止多台机器登陆同一账号。
本例使用一个简单的JSP页面来模拟登陆状况。如果session中有personInfo信息,则表示已经登陆,页面将显示登陆后的账号。如果session中没有personInfo信息,则表示没有登陆,页面将显示登陆输入框。登陆与注销都在JSP页面中完成。
singleton.jsp
<%@page import="logindemo.PersonInfo"%>
<%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@ page language="java" import="java.util.*" contentType="text/html; charset=utf-8"%>
<%
String action = request.getParameter("action");
String account = request.getParameter("account");
if("login".equals(action) && account.trim().length()>0){
//如果为登陆动作
PersonInfo personInfo = new PersonInfo();
personInfo.setAccount(account.trim().toLowerCase());
personInfo.setIp(request.getRemoteAddr());
personInfo.setLoginDate(new Date());
session.setAttribute("personInfo", personInfo);
response.sendRedirect(response.encodeRedirectUrl(request.getRequestURI()));
return;
}else if("logout".equals(action)){
session.removeAttribute("personInfo");
response.sendRedirect(response.encodeRedirectUrl(request.getRequestURI()));
return;
}
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTMLper 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<style type="text/css">
body {
font-size:12px;
}
</style>
</head>
<body>
<c:choose>
<c:when test="${ personInfo != null }">
<!-- 已经登录,将显示帐号信息 -->
欢迎您,${ personInfo.account }。<br/>
您的登录IP为${ personInfo.ip },<br/>
登录时间为<fmt:formatDate value="${ personInfo.loginDate }" pattern="yyyy-MM-dd HH:mm"/>。
<a href="${ pageContext.request.requestURI }?action=logout">退出</a>
<!-- 每5秒钟刷新一次页面 -->
<script>setTimeout("location=location; ", 5000); </script>
</c:when>
<c:otherwise>
<!-- 没有登录,将显示登录页面 -->
${ msg }
<c:remove var="msg" scope="session" />
<form action="${ pageContext.request.requestURI }?action=login" method="post">
帐号:
<input name="account" />
<input type="submit" value="登录">
</form>
</c:otherwise>
</c:choose>
</body>
</html>
为了信息的实时性,登陆后的代码中添加了js代码,每隔5秒刷新一次页面。这样,如果账号在别处登陆,5秒之内就会强制下线。
PersonInfo.java
package logindemo;
import java.io.Serializable;
import java.util.Date;
public class PersonInfo implements Serializable {
private static final long serialVersionUID = 4063123342511223123L;
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;
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof PersonInfo)) {
return false;
}
return account.equals(((PersonInfo) obj).getAccount());
}
}
JSP代码中没有任何实现单态登陆的代码。单态登陆的代码写在listener中,通过监听Session的personInfo属性的添加删除实现的。如果新添加了一个personInfo属性,则认为是新登录用户,Listener中查看该用户账号是否已经在其他机器上登陆,如果已经登陆过了,则把旧的登陆信息失效。
该Listener可能需要修改其他用户的session,而正常情况下其他用户的session是对当前登陆用户不可见的。因此Listener中使用一个Map把所有包含personInfo信息的session收集起来,以便索引或修改相关session。
LoginSessionListener.java
package logindemo;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
public class LoginSessionListener implements HttpSessionAttributeListener {
Map<String, HttpSession> map = new HashMap<String, HttpSession>();
public void attributeAdded(HttpSessionBindingEvent event) {
String name = event.getName();
// 登录
if (name.equals("personInfo")) {
PersonInfo personInfo = (PersonInfo) event.getValue();
if (map.get(personInfo.getAccount()) != null) {
// map 中有记录,表明该帐号在其他机器上登录过,将以前的登录失效
HttpSession session = map.get(personInfo.getAccount());
PersonInfo oldPersonInfo = (PersonInfo) session
.getAttribute("personInfo");
session.removeAttribute("personInfo");
session.setAttribute("msg", "您的帐号已经在其他机器上登录,您被迫下线。");
}
// 将session以用户名为索引,放入map中
map.put(personInfo.getAccount(), event.getSession());
// System.out.println("添加用户成功,现在登陆人数" + map.size());
}
}
public void attributeRemoved(HttpSessionBindingEvent event) {
String name = event.getName();
// 注销
if (name.equals("personInfo")) {
// 将该session从map中移除
PersonInfo personInfo = (PersonInfo) event.getValue();
map.remove(personInfo.getAccount());
}
// System.out.println("删除用户成功,现在登陆人数" + map.size());
}
public void attributeReplaced(HttpSessionBindingEvent event) {
String name = event.getName();
// 没有注销的情况下,用另一个帐号登录
if (name.equals("personInfo")) {
// 移除旧的的登录信息
PersonInfo oldPersonInfo = (PersonInfo) event.getValue();
map.remove(oldPersonInfo.getAccount());
// 新的登录信息
PersonInfo personInfo = (PersonInfo) event.getSession()
.getAttribute("personInfo");
// 也要检查新登录的帐号是否在别的机器上登录过
if (map.get(personInfo.getAccount()) != null) {
// map 中有记录,表明该帐号在其他机器上登录过,将以前的登录失效
HttpSession session = map.get(personInfo.getAccount());
session.removeAttribute("personInfo");
session.setAttribute("msg", "您的帐号已经在其他机器上登录,您被迫下线。");
}
map.put("personInfo", event.getSession());
}
// System.out.println("修改用户成功,现在登陆人数" + map.size());
}
}
web.xml
<listener>
<listener-class>logindemo.LoginSessionListener</listener-class>
</listener>
显示在线用户
本例将使用L
istener记录服务器的信息,包括启动时间、累计访问人数、最大同时在线数以及发生的时间、当前用户数以及当前登陆用户数等,并列出所有的在线用户信息,包括账号、第一次访问时间、最后一次访问时间、访问次数及ip地址等。运行效果见下图。
为了简单起见,将所有的数据放到ApplicationConstants类的静态属性中。
ApplicationConstants.java
package serverinfodemo;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpSession;
public class ApplicationConstants {
// 所有的 Session
public static Map<String, HttpSession> SESSION_MAP = new HashMap<String, HttpSession>();
// 当前登录的用户总数
public static int CURRENT_LOGIN_COUNT = 0;
// 历史访客总数
public static int TOTAL_HISTORY_COUNT = 0;
// 服务器启动时间
public static Date START_DATE = new Date();
// 最高在线时间
public static Date MAX_ONLINE_COUNT_DATE = new Date();
// 最高在线人数
public static int MAX_ONLINE_COUNT = 0;
}
该例中使用4中listener:ServletContextListener、HttpSessionListener、HttpSessionAttributeListener以及ServletRequestListener。
使用ServletContextListener来监听服务器的启动与关闭,记录服务器的启动时间等。
MyContextListener.java
package serverinfodemo;
import java.util.Date;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class MyContextListener implements ServletContextListener {
public void contextDestroyed(ServletContextEvent arg0) {
ApplicationConstants.START_DATE = null;
ApplicationConstants.MAX_ONLINE_COUNT_DATE = null;
}
public void contextInitialized(ServletContextEvent arg0) {
ApplicationConstants.START_DATE = new Date();
}
}
对session的监听比较复杂,需要维护在线用户列表、总访问人数等。为简单起见,本例仍使用Map来索引所有session。session创建的时候,将session放入map中;session销毁的时候,从map中将session移除,以保证所有在线用户的session都在map中。当session的personInfo属性发生变化时,维护用户的登陆与注销。
MySessionListener.java
package serverinfodemo;
import java.util.Date;
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;
public class MySessionListener implements HttpSessionListener,
HttpSessionAttributeListener {
/**
* 添加属性时调用
*/
public void attributeAdded(HttpSessionBindingEvent se) {
if (se.getName().equals("personInfo")) {
ApplicationConstants.CURRENT_LOGIN_COUNT++; // 当前登陆用户++
HttpSession session = se.getSession();
// 查找该账号有没有在其他机器上登陆
for (HttpSession sess : ApplicationConstants.SESSION_MAP.values()) {
// 如果该账号已经在其他机器上登陆,则以前的登陆失败
if (se.getValue().equals(sess.getAttribute("personInfo"))
&& session.getId() != sess.getId()) {
sess.invalidate();
ApplicationConstants.CURRENT_LOGIN_COUNT--;
}
}
}
}
/**
* 移除属性时调用
*/
public void attributeRemoved(HttpSessionBindingEvent se) {
if (se.getName().equals("personInfo")) {
ApplicationConstants.CURRENT_LOGIN_COUNT--; // 当前登陆用户--
}
}
public void attributeReplaced(HttpSessionBindingEvent se) {
if (se.getName().equals("personInfo")) { // 重新登陆
HttpSession session = se.getSession();
// 查找该账号有没有在其他机器上登陆
for (HttpSession sess : ApplicationConstants.SESSION_MAP.values()) {
// 如果该账号已经在其他机器上登陆,则以前的登陆失败
if (se.getValue().equals(sess.getAttribute("personInfo"))
&& session.getId() != sess.getId()) {
sess.invalidate();
ApplicationConstants.CURRENT_LOGIN_COUNT--;
}
}
}
}
/**
* session创建时被调用
*/
public void sessionCreated(HttpSessionEvent arg0) {
HttpSession session = (HttpSession) arg0.getSession();
ApplicationConstants.SESSION_MAP.put(session.getId(), session);
ApplicationConstants.TOTAL_HISTORY_COUNT++; // 总访问人数++
if (ApplicationConstants.SESSION_MAP.size() > ApplicationConstants.MAX_ONLINE_COUNT) {
ApplicationConstants.MAX_ONLINE_COUNT = ApplicationConstants.SESSION_MAP
.size();
ApplicationConstants.MAX_ONLINE_COUNT_DATE = new Date();
}
}
/**
* session销毁时调用
*/
public void sessionDestroyed(HttpSessionEvent arg0) {
HttpSession session = (HttpSession) arg0.getSession();
ApplicationConstants.SESSION_MAP.remove(session.getId());
}
}
监听request主要是记录客户的ip地址、访问次数等,也可以记录用户访问的URI。
MyRequestListener.java
package serverinfodemo;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
public class MyRequestListener implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {
}
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre
.getServletRequest();
HttpSession session = request.getSession(true);
session.setAttribute("ip", request.getRemoteAddr()); // 记录ip地址
String uri = request.getRequestURI(); // 访问的URI
String[] suffix = { ".html", ".do", ".jsp", ".action" }; // 指定后缀
for (int i = 0; i < suffix.length; i++) {
if (uri.endsWith(suffix[i])) {
// 如果是指定后缀,程序继续运行
break;
}
if (i == suffix.length - 1) {
// 否则返回
return;
}
}
Integer activeTimes = (Integer) session.getAttribute("activeTimes"); // 获取访问次数
if (activeTimes == null) {
activeTimes = 0;
}
session.setAttribute("activeTimes", activeTimes + 1); // 更新访问次数
}
}
web.xml
<listener>
<listener-class>serverinfodemo.MyContextListener</listener-class>
</listener>
<listener>
<listener-class>serverinfodemo.MySessionListener</listener-class>
</listener>
<listener>
<listener-class>serverinfodemo.MyRequestListener</listener-class>
</listener>
online.jsp
<%@page import="java.text.SimpleDateFormat"%>
<%@page import="logindemo.PersonInfo"%>
<%@page import="serverinfodemo.ApplicationConstants"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<jsp:directive.page import="java.util.Date" />
<jsp:directive.page import="java.text.DateFormat" />
<style>
body, td {font-size: 12px; }
</style>
服务器启动时间:<%=new SimpleDateFormat("yyyy-MM-dd HH:mm").format(ApplicationConstants.START_DATE)%>,
累计共接待过 <%= ApplicationConstants.TOTAL_HISTORY_COUNT %> 访客。<br/>
同时在线人数最多为 <%= ApplicationConstants.MAX_ONLINE_COUNT %> 人,
发生在 <%=DateFormat.getDateTimeInstance().format(ApplicationConstants.MAX_ONLINE_COUNT_DATE)%>。 <br/>
目前在线总数:<%= ApplicationConstants.SESSION_MAP.size() %>,登录用户:<%=ApplicationConstants.CURRENT_LOGIN_COUNT%>。<br/>
<table border=1>
<tr>
<th>jsessionid</th>
<th>account</th>
<th>creationTime</th>
<th>lastAccessedTime</th>
<th>new</th>
<th>activeTimes</th>
<th>ip</th>
</tr>
<%
for (String id : ApplicationConstants.SESSION_MAP.keySet()) {
HttpSession sess = ApplicationConstants.SESSION_MAP.get(id);
PersonInfo personInfo = (PersonInfo)sess.getAttribute("personInfo");
%>
<tr <%= session == sess ? "bgcolor=#DDDDDD" : "" %>>
<td><%=id%></td>
<td><%=personInfo==null ? " " : personInfo.getAccount()%></td>
<td><%=DateFormat.getDateTimeInstance().format(sess.getCreationTime())%></td>
<td><%=DateFormat.getDateTimeInstance().format(
new Date(sess.getLastAccessedTime()))%></td>
<td><%=sess.isNew()%></td>
<td><%=sess.getAttribute("activeTimes")%></td>
<td><%=sess.getAttribute("ip") %></td>
</tr>
<%
}
%>
</table>