文章目录
  1. 1. 想法
  2. 2. 监听器
    1. 2.1. 配置
    2. 2.2. 代码
  3. 3. 过滤器
    1. 3.1. 配置
    2. 3.2. 代码
  4. 4. 全局缓存
  5. 5. 登录流程
  6. 6. 其它

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

想法

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

  1. HTTP是无状态的,怎么记录在线人数,下线了怎么办?
  2. HTTP只能客户端向服务端发请求,不能服务器推信息给客户端,怎么T用户下线?

理论上肯定是不可行的。好吧,办法目前能想到的只有利用session了。在web.xml里面可以有这样的配置

1
2
3
<session-config>
<session-timeout>5</session-timeout>
</session-config>

这里设置的是Session超过5分钟就会过期。Tomcat默认是30分钟,用来统计用户数的话,越小越精确,但如果是一个需要正常登录才能访问而且信息都在Session里面,也要考虑用户忍耐度-_-!

用户每创建一个Session都会有产生唯一的SessionId,如果没有过期,后续与服务器的交互SessionId是不会变的,就利用SessionId来实现这个需求。

监听器

首先,在web.xml里面加入监听器,监听器的作用是维护一个全局的在线用户列表

配置

1
2
3
4

<listener>
<listener-class>com.xdnote.safelogin.SafeLoginLinsener</listener-class>
</listener>

代码

监听器Linsener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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");
}
}

过滤器

所有请求经过过滤器处理,这里可以把这个过滤器想像成一个功能扩展后的登录过滤器,除了判断用户是否登录,还判断用户是否在“白名单中”。

配置

1
2
3
4
5
6
7
8
9
10
<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>

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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");
}
}

全局缓存

最基本的监听器和过滤器出来了,现在需要一个缓存,也先把方法写在这,后续再补充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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人下线的功能就基本这样了。再加几个页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<%@ page language="java"  pageEncoding="utf-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>login on</title>
</head>
<body>
<form action="login.jsp">
<label for="account">account</label><input type="text" name="account">
<input type="submit" value="submit">
</form>
</body>
</html>
login.jsp
<%@ page language="java" pageEncoding="utf-8"%>
<%
if(request.getParameter("account")!=null){
session.setAttribute("account",request.getParameter("account"));
}
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>login</title>
</head>
<body>
log in success
</body>
</html>

logout.jsp
<%@ page language="java" pageEncoding="utf-8"%>
<%
request.getSession().invalidate();
response.sendRedirect("/");
%>

代码后来又改过,可能运行不正常,只是提供基本思想而已,可以根据实际情况扩展,比如只允许一个用户同时登录一个帐号,记录当前用户有多少游客和多少VIP等,后续有空完善了再做成JAR包下载,嘻嘻

文章目录
  1. 1. 想法
  2. 2. 监听器
    1. 2.1. 配置
    2. 2.2. 代码
  3. 3. 过滤器
    1. 3.1. 配置
    2. 3.2. 代码
  4. 4. 全局缓存
  5. 5. 登录流程
  6. 6. 其它