使用session儲存使用者登入狀態(實現單點登入)
由於是軟體公司,專案使用者量很小,而且是傳統專案,所以用session來儲存使用者的登入狀態。前端是移動端,我為session物件寫了一個工具類,供自己用,記錄一下,說不定以後還會用到。
先上session工具的程式碼:
工具使用單例與工廠模式,單例用的懶漢式,因為懶漢式存線上程安全問題,所有用classloader機制,用內部類方式建立單例物件來保證執行緒安全。package com.xxxx.utils; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import javax.servlet.http.HttpSession; import org.apache.commons.lang3.StringUtils; public class SessionUtil { private static final Long sessionMaxAge = 86400L; private SessionUtil() { } private static class SessionUtilInner { private static final SessionUtil SESSION_UTIL = new SessionUtil(); } public static SessionUtil getSessionUtil() { return SessionUtilInner.SESSION_UTIL; } private static ConcurrentHashMap<String, HttpSession> sessionMap; static { sessionMap = new ConcurrentHashMap<>(); } public static ConcurrentHashMap<String, HttpSession> getSessionMap() { sessionMap = cleanMap(sessionMap); return sessionMap; } private static synchronized ConcurrentHashMap<String, HttpSession> cleanMap( ConcurrentHashMap<String, HttpSession> map) { if (map.size() < 1) { return map; } Set<Entry<String, HttpSession>> entrySet = map.entrySet(); List<String> list = new ArrayList<>(); for (Entry<String, HttpSession> entry : entrySet) { // 如果session過期了,就清除掉這個session long max_age = sessionMaxAge * 1000L; long time = new Date().getTime(); long creationTime = entry.getValue().getCreationTime(); long sessionAge = time - creationTime; if (sessionAge > max_age) { list.add(entry.getKey()); entry.getValue().setMaxInactiveInterval(1); } } if (list != null && list.size() > 0) { for (int i = 0; i < list.size(); i++) { map.remove(list.get(i)); } } list = null; return map; } public static synchronized void cleanOldSession(String userInfo) { if (sessionMap != null && sessionMap.size() > 0) { Set<Entry<String, HttpSession>> entrySet = sessionMap.entrySet(); String sessionKey = ""; for (Entry<String, HttpSession> entry : entrySet) { if (StringUtils.isNotBlank(userInfo) && userInfo.equals((String) entry.getValue().getAttribute("userInfo"))) { sessionKey = entry.getKey(); entry.getValue().setMaxInactiveInterval(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } break; } } if (StringUtils.isNotBlank(sessionKey)) { sessionMap.remove(sessionKey); } } } }
session設定的有效時間是-1,即無過期時間,完全用程式碼來控制session的過期。這裡有個問題,就是session的過期優先順序是程式碼>web.xml配置>容器配置(我們容器用的tomcat),但是這只是一個優先順序,程式碼雖然設定了不過期,但是如果web.xml或者tomcat的配置檔案中設定了有效期的話,還是會過期。所以需要去掉web.xml和tomcat裡關於session過期的配置。
然後,我們session有效時間是一天,每次取session新增使用者的時候,都清理一遍超時的session。(這一機制建立在使用者量少的的基礎上,如果像電商專案一時間有幾千幾萬使用者登入,那就不可取了,不過那種專案一般使用redis來儲存使用者登入狀態,也不用自己寫session工具了不是?)
還有一個清理session的方法用於實現單點登入,傳入使用者存在session中的資訊,如果一致,說明該使用者又登入了,清除掉他以前的登入資訊即session物件,保證每個使用者只有一個登入物件。
工具類說完了,說一下使用;
我session物件中存的是使用者的openid(微信的使用者id)和使用者名稱,key是userInfo,每次登入時,先清除舊的session,然後通過String sessionInfo = appUser.getOpenid() + "|" + appUser.getUserName(); SessionUtil.cleanOldSession(sessionInfo); HttpSession session = request.getSession(true); session.setAttribute("userInfo", sessionInfo); session.setMaxInactiveInterval(Integer.parseInt(sessionInvalidTime.trim())); // 設定session String sessionKey = System.currentTimeMillis() + ""; SessionUtil.getSessionMap().put(sessionKey, session); appUser.setSessinKey(sessionKey);
request.getSession(true)
建立一個新的session物件,存入資訊,我的sessionkey用的是當前時間毫秒數(老生常談,如果使用者多可以用其他方法保證唯一,我們不需要暫時,需要的時候最簡單的方法就是用uuid去掉_後返回),存到session工具類的map裡面,把sessionkey返回給前端,所有需要登入的才能訪問的介面,都要帶著sessionkey來,才能訪問。
然後,編寫一個攔截器:
package com.xxxx.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.xxxx.utils.SessionUtil;
import com.xxxx.RegisterInfo;
import com.xxxx.Constant;
import com.xxxx.JsonUtil;
public class VisitIntercepeter extends HandlerInterceptorAdapter {
@Override
public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
throws Exception {
// TODO 自動生成的方法存根
}
@Override
public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3)
throws Exception {
// TODO 自動生成的方法存根
}
@SuppressWarnings("rawtypes")
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
HttpSession session = null;
String sessionID = "";
if (StringUtils.isNotBlank(request.getParameter("sessinKey"))) {
sessionID = request.getParameter("sessinKey");
} else if (StringUtils.isNotBlank(request.getHeader("sessinKey"))) {
sessionID = request.getHeader("sessinKey");
}
if (sessionID != null) {
session = (HttpSession) SessionUtil.getSessionMap().get(sessionID);
if (session != null) {
String sessionValue = (String) session.getAttribute("userInfo");
if (StringUtils.isNotBlank(sessionValue)) {
return true;
}
}
}
RegisterInfo registerInfo = new RegisterInfo<>();
registerInfo.setCode(Constant.MESSAGE_ERROR_CODE);
registerInfo.setMessage(Constant.MESSAGE_SESSION_OVERDUE);
registerInfo.setResult(false);
String json = JsonUtil.parseBeanToJson(registerInfo);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(json);
return false;
}
}
我們的sessionkey存在head和引數中都是可以的,用sessionkey來攔截沒有登入的使用者。
這樣就大功告成了,專案部署的時候記得前面講到的關於容易對session配置的問題,這個很重要。