- 所用知識點羅列:
cookie 、session、serverlet過濾器、serverlet監聽器,前提環境是基於Session實現的登錄功能
(Session中存放了登錄者的ip,userName等信息作已登錄標記)
- 需要理解的基本概念
Session是基於cookie的,請求會話中,通過瀏覽器cookie攜帶的JsessionId
的sessionId號來找到服務器中相應的Session的.
如果沒有設置cookie的有效時間,一旦瀏覽器關閉,cookie就消失了,
其攜帶的sessionId也就丟失了,
這時即使服務器中的當前用戶的Session還未過期失效,依然存在,也無法找到了,基本等於是銷燬了.
- 問題關鍵所在
正常使用Session實現的登錄時把登陸標記存放在Session中,在一次會話中有效。
Session是以會話爲隔離的,(其他瀏覽器或其他電腦也可以在打開一個會話),不同會話就可以創建同一用戶的不同session。
也就造成了服務器端可以有任意多個SessionId不同,但Session內容卻相同的Session,
也即是同一個用戶可以在多個瀏覽器登錄,無論是否是在同一臺電腦(ip)、同一個地區。
這也是我們要實現多ip登錄踢人下線功能要解決的問題。
- 解決方案思路
1. 自己實現MySessionContext類搞一個map靜態成員變量以<SessionId,Session>的方式裝所有的Session,放服務器的運行內存中待用.
(其實使用serverletContext作爲容器也可以替代Session和這個自己實現的SessionContext)
2. 搞一個Session監聽器,監聽Session的創建和銷燬,在新的session創建時將其加入到上面自己創建的
MySessionContext的靜態成員變量map中,Session銷燬時(或者用戶註銷登錄時)把他的Session移除出map,
並用Session.invalidate()銷燬.
3. 用一個過濾器攔截過濾登錄請求,獲取登錄用戶的登錄標記,然後遍歷裝有Session的map,
對照是否有當前登錄用戶的Session存在,如果沒有就放行通過;
如果有,取出找到的session(也即是前一個登錄者生成的Session)移除出MySessionContext的map容器,
並銷燬這個Session(調用invalid()方法).此時前一個登錄者再刷新頁面時發現Session已經不存在了,配合先前做的Session過期過濾處理,就會和Session過期有一樣的效果——下線.
- 參考代碼
登錄操作:
//獲取登錄ip地址
String ip = request.getRemoteAddr();
//將登錄者的ip放入到session中
request.getSession().setAttribute(ESessionKey.LoginIP.key, ip);
request.getSession().setAttribute(ESessionKey.UserId.key, user.getUserId());// 將用戶id存放到session中,作爲已登錄標記
MySessionContext實現
public class MySessionContext {
private static HashMap<String,HttpSession> mymap = new HashMap<String,HttpSession>();
public static synchronized void AddSession(HttpSession session) {
if (session != null) {
mymap.put(session.getId(), session);
}
}
public static synchronized void DelSession(HttpSession session) {
if (session != null) {
HttpSession session2 = mymap.remove(session.getId());//移出session
if(session2!=null){
session2.invalidate();//將從sessionContext中移出的Session失效 --相當於清除當前Session對應的登錄用戶
}
}
}
public static synchronized HttpSession getSession(String session_id) {
if (session_id == null)
return null;
return (HttpSession)mymap.get(session_id);
}
public static HashMap<String, HttpSession> getMymap() {
return mymap;
}
}
Session監聽器
public class SessionCounter implements HttpSessionListener {
private static int activeSessions = 0;
public void sessionCreated(HttpSessionEvent se) {
MySessionContext.AddSession(se.getSession());
activeSessions++;
System.out.println("++++++++玩家上線了++++++++");
}
public void sessionDestroyed(HttpSessionEvent se) {
if(activeSessions > 0)
activeSessions--;
HttpSession session = se.getSession();
MySessionContext.DelSession(session);
}
public static int getActiveSessions() {
return activeSessions;
}
}
踢人下線過濾器核心代碼
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)resp;
String localIp = request.getRemoteAddr();//獲取本地ip
HttpSession session = null;
String user_id = (String)request.getParameter("userId"); //登錄請求時填寫的
if(StringUtils.isNotBlank(user_id))
{
session = getLoginedUserSession(user_id);
}
String loginedIp = null;
if(session!=null)
{
loginedIp = (String)session.getAttribute(ESessionKey.LoginIP.key);//獲取已登錄者ip(如果有)
}
if(StringUtils.isNotBlank(loginedIp) && !localIp.equals(loginedIp)){
MySessionContext.DelSession(session);//踢人--找到並銷燬Session
request.setAttribute("msg", "您的賬號在其它ip登錄,您被踢下線了!");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}else{
chain.doFilter(request, response);//放行
}
}
Session過期過濾器 核心代碼
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
System.out.println("過濾請求...");
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)resp;
//session中獲取用戶名信息
String userId = (String)request.getSession().getAttribute("userId");
String admin = (String)request.getSession().getAttribute("admin");
//普通用戶登錄過濾,如果用戶名爲空說明帶有登錄標記的Session過期了
if (userId==null||"".equals(userId.toString()) ) {
//超級管理員過濾
if(admin==null||"".equals(admin.toString())){
response.sendRedirect(request.getContextPath()+ADMIN_URL);
return ;
}
//如果普通用戶和超級管理員都沒有登陸內容,說明登錄過期
System.out.println("登錄過期,請重新登錄!");
response.sendRedirect(request.getContextPath()+LOGIN_URL);
PrintWriter printWriter = response.getWriter();
String relogin = "登錄過期,請重新登錄!";
printWriter.write(relogin);
printWriter.flush();
printWriter.close();
return ;
}
//過濾通過,放行
chain.doFilter(request, response);
System.out.println("過濾響應!");
}
web.xml配置
<!-- 登錄踢人過濾器 -->
<filter>
<filter-name>TickFronterFilter</filter-name>
<filter-class>com.fengyun.web.filter.TickFronterFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>TickFronterFilter</filter-name>
<url-pattern>/login.html</url-pattern>
</filter-mapping>
<!-- session監聽器 -->
<listener>
<listener-class>
com.fengyun.web.filter.SessionCounter
</listener-class>
</listener>
<!-- session過期過濾器 -->
<filter>
<filter-name>Loginfilter</filter-name>
<filter-class>com.fengyun.web.filter.LoginOverdueFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>Loginfilter</filter-name>
<url-pattern>/material/*</url-pattern>
...等等需要過濾的url地址...當然可以使用通配方式寫(此處不詳述)
<url-pattern>/operate_editeCompact.html</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>