X.d 笔记

小Web,大世界

0%

java里面监听在线人数,T用户下线

在 Java 中怎么获取多少用户同时在线,怎么只允许一个用户在线,怎么强行把一个用户T下线?现在常用的几种框架好像没有提供相应API,所以自己使用 ServletListener 做一个

想法

一般有这样的需求时马上会想到下面两点:

  1. HTTP是无状态的,怎么记录在线人数,下线了怎么办?
  2. 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} {}
  1. 登录中在登录用户数组里面产生一条数据
  2. 如果一个帐号多处登录,则一个数据里面有多个ID,用逗号分开
  3. 配置Session time out自动登出,登出后自动干掉相应记录
  4. 用户发现自己帐号异常可以冻结,冻结后所有操作失效。(加入黑名单一天)
  5. 用户冻结时,若进行操作的用户在黑名单内,则直接返回失败并给用户登出
  6. 冻结时间到后,解冻,将黑名单内删除

逻辑大致就是这样,开始填充代码,这里先写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(&#039;帐号已被冻结&#039;);window.location.href=&#039;logout.jsp&#039;;</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包下载,嘻嘻