x-blog之并发登陆人数控制简单介绍

前言

并发登陆人数控制就是控制同一账号最大有效会话数。而项目中会话管理交给shiro控制,shiro的sessionId每次是不一样的,也就是说,同一用户登录,在缓存中通过用户名产生的会话id不一样,造成同一用户登录重新登录会变成不同用户而使在线人数不对。
当然,对于这些实时性的统计,我觉得利用redis缓存会话更优,但鉴于个人经验和能力问题,暂时用shiro管理。

过滤器

首先定义一个KickoutSessionFilter过滤器继承shiro的AccessControlFilter,用于拦截登录请求,并且在shiro配置文件中
配置和初始化相应属性,在echache.xml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--并发登录控制-->
<bean id="kickoutSessionFilter" class="cn.slycmiaoxi.filter.KickoutSessionFilter">
<property name="cacheManager" ref="cacheManager"/>
<property name="sessionManager" ref="sessionManager"/>
<property name="kickoutAfter" value="false"/>
<property name="maxSession" value="1"/>
<property name="kickoutUrl" value="/index.jsp"/>
</bean>
<cache name="shiro-kickout-session"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>

一般涉及缓存都需要同步缓存,也就是当有会话失效时。利用一个监听器将该会话同步掉

而过滤器核心思想就是先判断该会话是否有效或者是否已存在,然后放在一个缓存队列中,如果其大小超过该会话数,就踢出头或尾会话,在加入该会话,然后就是这个缓存队列是通过ehcache中命名用户名设置的,然后将其放在shiro拦截器,放在登录请求中,这是如果有会话被提出,但是本地缓存sessionDAO中任然有那个被踢掉的缓存,需要个监听器根据那个用户名得到sessionId然后踢掉,缓存同步

码上有戏

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
<!--并发登录控制-->
public class KickoutSessionFilter extends AccessControlFilter {
private Logger logger = (Logger)LoggerFactory.getLogger(this.getClass().getName());
/**
* 踢出后到的地址
*/
private String kickoutUrl;
/**
* 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
*/
private boolean kickoutAfter = false;
/**
* 同一个帐号最大会话数 默认1
*/
private int maxSession;
private SessionManager sessionManager;
private Cache<String, Deque<Serializable>> cache;
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
throws Exception {
Subject subject = getSubject(request, response);
Session session = subject.getSession();
String nickName = (String)subject.getPrincipal();
Serializable sessionId = session.getId();
if (!subject.isAuthenticated() && !subject.isRemembered()) {
// 如果没有登录,直接进行之后的流程
return true;
}
synchronized (this.cache) {
Deque<Serializable> deque = this.cache.get(nickName);
if (deque == null) {
deque = new ConcurrentLinkedDeque<Serializable>();
}
if (!deque.contains(sessionId) && session.getAttribute("kickOut") == null) {
session.setAttribute("nickName", nickName);
deque.addLast(sessionId);
}
this.logger.debug("deque = " + deque);
if (deque.size() > this.maxSession) {
if (!this.kickoutAfter) {
sessionId = deque.removeFirst();
}
else {
sessionId = deque.removeLast();
}
try {
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(sessionId));
if (kickoutSession != null) {
kickoutSession.stop();
}
}
catch (Exception e) {// ignore exception
}
}
this.cache.put(nickName, deque);
}
// 如果被踢出了,直接退出,重定向到踢出后的地址
if (session.getAttribute("kickout") != null) {
// 会话被踢出了
try {
subject.logout();
}
catch (Exception e) { // ignore
}
saveRequest(request);
WebUtils.issueRedirect(request, response, kickoutUrl);
return false;
}
return true;
}
public void setKickoutUrl(String kickoutUrl) {
this.kickoutUrl = kickoutUrl;
}
public void setKickoutAfter(boolean kickoutAfter) {
this.kickoutAfter = kickoutAfter;
}
public void setMaxSession(int maxSession) {
this.maxSession = maxSession;
}
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public void setCacheManager(CacheManager cacheManager) {
this.cache = cacheManager.getCache(Constants.LOGIN_SESSION);
}
}
public class KickOutSessionListener implements SessionListener {
private Logger logger = (Logger)LoggerFactory.getLogger(this.getClass().getName());
private Cache<String, Deque<Serializable>> cache;
@Autowired
private SessionDAO sessionDAO;
public void setCacheManager(CacheManager cacheManager) {
this.cache = cacheManager.getCache("shiro-kickout-session");
}
private void removeSessionFromCache(Session session) {
String nickName = (String)session.getAttribute("nickName");
if (nickName != null) {
logger.info("remove session: " + session.getId() + " in deque of " + nickName + "@shiro-kickout-session");
synchronized (this.cache) {
Deque<Serializable> deque = this.cache.get(nickName);
deque.remove(session.getId());
sessionDAO.delete(session);
this.cache.put(nickName, deque);
}
}
}
@Override
public void onStop(Session session) {
this.removeSessionFromCache(session);
}
@Override
public void onExpiration(Session session) {
this.removeSessionFromCache(session);
}
@Override
public void onStart(Session session) {
}
}

热评文章