在 Java 中怎么获取多少用户同时在线,怎么只允许一个用户在线,怎么强行把一个用户T下线?现在常用的几种框架好像没有提供相应API,所以自己使用 Servlet
和 Listener
做一个
想法
一般有这样的需求时马上会想到下面两点:
- HTTP是无状态的,怎么记录在线人数,下线了怎么办?
- HTTP只能客户端向服务端发请求,不能服务器推信息给客户端,怎么T用户下线?
理论上肯定是不可行的。好吧,办法目前能想到的只有利用session了。在web.xml里面可以有这样的配置
<session-config>
<session-timeout>5</session-timeout>
</session-config>
这里设置的是Session超过5分钟就会过期。Tomcat默认是30分钟,用来统计用户数的话,越小越精确,但如果是一个需要正常登录才能访问而且信息都在Session里面,也要考虑用户忍耐度-_-!
用户每创建一个Session都会有产生唯一的SessionId,如果没有过期,后续与服务器的交互SessionId是不会变的,就利用SessionId来实现这个需求。
监听器
首先,在web.xml里面加入监听器,监听器的作用是维护一个全局的在线用户列表
配置
<listener>
<listener-class>com.xdnote.safelogin.SafeLoginLinsener</listener-class>
</listener>
代码
监听器Linsener
package com.xdnote.safelogin;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
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 SafeLoginLinsener implements HttpSessionAttributeListener,HttpSessionListener,ServletContextListener{
//Session中有attribute被添加时
public void attributeAdded(HttpSessionBindingEvent event) {
System.out.println("HttpSessionAttributeListener:attributeAdded");
}
//Session中有attribute被删除时
public void attributeRemoved(HttpSessionBindingEvent event) {
System.out.println("HttpSessionAttributeListener:attributeRemoved");
}
//Session中有attribute被替换时
public void attributeReplaced(HttpSessionBindingEvent event) {
System.out.println("HttpSessionAttributeListener:attributeReplaced");
}
//Session被创建时
public void sessionCreated(HttpSessionEvent event) {
HttpSession session=event.getSession();
System.out.println("HttpSessionListener:sessionCreated:"+session.getId());
}
//Session被销毁时
public void sessionDestroyed(HttpSessionEvent event) {
HttpSession session=event.getSession();
System.out.println("HttpSessionListener:sessionDestroyed:"+session.getId());
}
//Servlet被创建时
public void contextInitialized(ServletContextEvent event) {
System.out.println(event.getServletContext().getInitParameter("application"));
System.out.println("ServletContextListener:contextInitialized");
}
//Servlet被销毁时
public void contextDestroyed(ServletContextEvent event) {
System.out.println("ServletContextListener:contextDestroyed");
}
}
过滤器
所有请求经过过滤器处理,这里可以把这个过滤器想像成一个功能扩展后的登录过滤器,除了判断用户是否登录,还判断用户是否在“白名单中”。
配置
<filter>
<description>filter for safe login </description>
<display-name>SafeLogin</display-name>
<filter-name>SafeLoginFilter</filter-name>
<filter-class>com.xdnote.safelogin.SafeLoginFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>SafeLoginFilter</filter-name>
<url-pattern>/*.jsp</url-pattern>
</filter-mapping>
代码
package com.xdnote.safelogin;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
public class SafeLoginFilter implements Filter{
boolean allowMultiple=true;
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
}
//Filter 初始化时
public void init(FilterConfig arg0) throws ServletException {
allowMultiple=Boolean.valueOf(arg0.getInitParameter("allowMultiple")).booleanValue();
System.out.println("Filter:init");
}
//Filter 销毁时
public void destroy() {
System.out.println("Filter:destroy");
}
}
全局缓存
最基本的监听器和过滤器出来了,现在需要一个缓存,也先把方法写在这,后续再补充。
package com.xdnote.safelogin;
public class CacheUtil {
public static boolean addCache(String key,String value){
return false;
}
public static boolean delCache(String key){
return false;
}
public static String getCache(String key){
String value="";
return value;
}
}
如果是集群部署,可以考虑MemCached.不是的话,就存在内存里面可要小心超出内存或被回收什么的。
登录流程
现在手画一下Session逻辑。通常看上去很复杂的东西,自己去想一遍,想清楚了就会发现没有那么复杂。这里假设allowMultiple为true.即允许多个帐号多个终端登录。
假设帐号为,1,2. Session 为a-z,则具体操作流程可以如下
操作 | 1,2登录 | 1在另一处登录 | 其中1个1超时 | 冻结1帐号 | 解冻1帐号 |
---|---|---|---|---|---|
登录用户 | {1=a,2=b} | {1=a,c,2=b} | {1=c,2=b} | {2=b} | {2=b} |
黑名单 | {} | {} | >{} | {1=date+1} | {} |
- 登录中在登录用户数组里面产生一条数据
- 如果一个帐号多处登录,则一个数据里面有多个ID,用逗号分开
- 配置Session time out自动登出,登出后自动干掉相应记录
- 用户发现自己帐号异常可以冻结,冻结后所有操作失效。(加入黑名单一天)
- 用户冻结时,若进行操作的用户在黑名单内,则直接返回失败并给用户登出
- 冻结时间到后,解冻,将黑名单内删除
逻辑大致就是这样,开始填充代码,这里先写CacheUtil,由于只是演示下,就使用java内置的serletContext为Cache源吧,实际项目中不推荐。按照之前流程,梳理一下代码大致如下:
CacheUtil.java
package com.xdnote.safelogin;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
public class CacheUtil {
//登录用户前缀
public static String LOGIN_PREFIX="LOGIN_";
//黑名单用户前缀
public static String BLACK_PREFIX="LOGIN_";
//黑名单用户屏蔽时间 单位毫秒
public static int BLOCK_TIME = 60*60*24*1000;
//添加登录用户数据缓存
public static void addLogin(String key,String value,ServletContext sc){
String storekey=LOGIN_PREFIX+key;
Object logins=sc.getAttribute(storekey);
//如果取不到就添加,如果取的到,就以逗后相隔开,append到登录上去
if(null==logins){
sc.setAttribute(storekey, value);
}else{
String[] sids=logins.toString().split(",");
for(int i=0,j=sids.length;i<j;i++){
if(sids[i].equals(value)){
return;
}
}
sc.setAttribute(storekey, logins+","+value);
}
}
//删除数据缓存
public static void delLogin(String key,String value,ServletContext sc){
String storekey=LOGIN_PREFIX+key;
Object logins=sc.getAttribute(storekey);
if(null==logins){
return;
}else{
//如果有多个用户多处登录,只删除其中登出的一处
StringBuffer sb=new StringBuffer();
String[] sids=logins.toString().split(",");
for(int i=0,j=sids.length;i<j;i++){
if(!sids[i].equals(value)){
sb.append(sids[i]).append(",");
}
}
String rtn=sb.toString();
if(rtn.length()==0){
sc.removeAttribute(storekey);
}else{
sc.setAttribute(storekey, sb.substring(0, rtn.length()-1));
}
}
}
//增加黑名单
public static void addBlackList(String key,ServletContext sc){
String storekey=BLACK_PREFIX+key;
Object logins=sc.getAttribute(storekey);
//增加到黑名列内
if(logins==null){
long now=System.currentTimeMillis();
sc.setAttribute(storekey, String.valueOf(now+BLOCK_TIME));
}
}
public static void main(String[] args){
System.out.println(System.currentTimeMillis());
}
//查询是否黑名单
public static boolean isBlack(String account,ServletContext sc){
String storekey=BLACK_PREFIX+account;
Object logins=sc.getAttribute(storekey);
if(logins!=null){
//如果在黑名单列但已经过了冻结期,则从黑名单里面删除
long now=System.currentTimeMillis();
long added=Long.parseLong(logins.toString());
if((now-added)>BLOCK_TIME){
sc.removeAttribute(storekey);
}else{
return true;
}
}
return false;
}
//判断是否可以浏览,判断当前登录用户是否在黑名单内,而且正处于冻结期
public static boolean canView(HttpSession session,ServletContext sc){
String sessionkey=sc.getInitParameter("accountField")!=null?sc.getInitParameter("accountField"):"account";
if(session.getAttribute(sessionkey)!=null){
String account=session.getAttribute(sessionkey).toString();
System.out.println(account);
if(isBlack(account,sc)){
return false;
}
}
return true;
}
}
SafeLoginLinsener.java
package com.xdnote.safelogin;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
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 SafeLoginLinsener implements HttpSessionAttributeListener,HttpSessionListener,ServletContextListener{
private String ACCOUNT="account";
//Session中有attribute被添加时
public void attributeAdded(HttpSessionBindingEvent event) {
//如果session
if(event.getName().equals(ACCOUNT)){
HttpSession session=event.getSession();
CacheUtil.addLogin(session.getAttribute(ACCOUNT).toString(),session.getId(),session.getServletContext());
System.out.println(session.getAttribute(ACCOUNT).toString()+":");
}
}
//Session被销毁时
public void sessionDestroyed(HttpSessionEvent event) {
HttpSession session=event.getSession();
if(session.getAttribute(ACCOUNT)!=null){
CacheUtil.delLogin(session.getAttribute(ACCOUNT).toString(),session.getId(),session.getServletContext());
}
}
//Servlet被创建时
public void contextInitialized(ServletContextEvent event) {
this.ACCOUNT=event.getServletContext().getInitParameter("accountField");
}
//Servlet被销毁时
public void contextDestroyed(ServletContextEvent event) {}
//Session中有attribute被删除时
public void attributeRemoved(HttpSessionBindingEvent event) {}
//Session中有attribute被替换时
public void attributeReplaced(HttpSessionBindingEvent event) {}
//Session被创建时
public void sessionCreated(HttpSessionEvent event) {}
}
SafeLoginFilter.java
package com.xdnote.safelogin;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
public class SafeLoginFilter implements Filter{
//过滤
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req=(HttpServletRequest) request;
HttpSession session=req.getSession();
if(CacheUtil.canView(session, session.getServletContext())){
chain.doFilter(request, response);
}else{
PrintWriter pw=response.getWriter();
pw.write("<script>alert('帐号已被冻结');window.location.href='logout.jsp';</script>");
}
}
//Filter 初始化时
public void init(FilterConfig arg0) throws ServletException {}
//Filter 销毁时
public void destroy() {}
}
其它
OK,一个基本的过滤器功能也应该可以了,但没有冻结什么都白干,好吧,再加一个servlet,假设有开放短信,用户绑定手机发个短信就可以冻结,或是管理员想冻就冻,想解冻就解冻,用servlet做个manage再合适不过。
加入了Servlet的完整web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<filter>
<description>filter for safe login </description>
<display-name>SafeLogin</display-name>
<filter-name>SafeLoginFilter</filter-name>
<filter-class>com.xdnote.safelogin.SafeLoginFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>SafeLoginFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>com.xdnote.safelogin.SafeLoginLinsener</listener-class>
</listener>
<context-param>
<param-name>accountField</param-name>
<param-value>account</param-value>
</context-param>
<session-config>
<session-timeout>5</session-timeout>
</session-config>
<servlet>
<servlet-name>ManageBlackList</servlet-name>
<servlet-class>com.xdnote.safelogin.SafeLoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ManageBlackList</servlet-name>
<url-pattern>/ManageBlackList</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
</web-app>
然后是SafeLoginServlet.java
package com.xdnote.safelogin;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class SafeLoginServlet extends HttpServlet {
private static final long serialVersionUID = -145600453825908280L;
/**
* 重写servlet的get,post
*/
public void doGet(HttpServletRequest request, HttpServletResponse response)
{
this.doPost(request, response);
}
/**
* 黑名单管理
*/
public void doPost(HttpServletRequest request, HttpServletResponse response)
{
String action = request.getParameter("action");
String account= request.getParameter("account");
if(action.equals("add")){
CacheUtil.addBlackList(account, request.getSession().getServletContext());
}
}
}
到这里一个最最最简单的T人下线的功能就基本这样了。再加几个页面
<%@ page language="java" pageEncoding="utf-8"%>
login on
login.jsp
<%@ page language="java" pageEncoding="utf-8"%>
<%
if(request.getParameter("account")!=null){
session.setAttribute("account",request.getParameter("account"));
}
%>
login
log in success
logout.jsp
<%@ page language="java" pageEncoding="utf-8"%>
<%
request.getSession().invalidate();
response.sendRedirect("/");
%>
代码后来又改过,运行不正常,只是提供基本思想而已,可以根据实际情况扩展,比如只允许一个用户同时登录一个帐号,记录当前用户有多少游客和多少VIP等,后续有空完善了再做成JAR包下载,嘻嘻