1. 程式人生 > >使用session儲存使用者登入狀態(實現單點登入)

使用session儲存使用者登入狀態(實現單點登入)

由於是軟體公司,專案使用者量很小,而且是傳統專案,所以用session來儲存使用者的登入狀態。前端是移動端,我為session物件寫了一個工具類,供自己用,記錄一下,說不定以後還會用到。

先上session工具的程式碼:

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);
			}
		}
	}
}
工具使用單例與工廠模式,單例用的懶漢式,因為懶漢式存線上程安全問題,所有用classloader機制,用內部類方式建立單例物件來保證執行緒安全。

session設定的有效時間是-1,即無過期時間,完全用程式碼來控制session的過期。這裡有個問題,就是session的過期優先順序是程式碼>web.xml配置>容器配置(我們容器用的tomcat),但是這只是一個優先順序,程式碼雖然設定了不過期,但是如果web.xml或者tomcat的配置檔案中設定了有效期的話,還是會過期。所以需要去掉web.xml和tomcat裡關於session過期的配置。

然後,我們session有效時間是一天,每次取session新增使用者的時候,都清理一遍超時的session。(這一機制建立在使用者量少的的基礎上,如果像電商專案一時間有幾千幾萬使用者登入,那就不可取了,不過那種專案一般使用redis來儲存使用者登入狀態,也不用自己寫session工具了不是?)

還有一個清理session的方法用於實現單點登入,傳入使用者存在session中的資訊,如果一致,說明該使用者又登入了,清除掉他以前的登入資訊即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);
我session物件中存的是使用者的openid(微信的使用者id)和使用者名稱,key是userInfo,每次登入時,先清除舊的session,然後通過
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配置的問題,這個很重要。