前言
前面我們利用springboot結(jié)合shiro在項目中解決了多數(shù)據(jù)源認(rèn)證的問題,接下來我們來看看如何在之前的框架基礎(chǔ)上實現(xiàn)分布式session管理.在一般情況下web服務(wù)器與客戶端采用http協(xié)議進(jìn)行通訊,大家都知道http協(xié)議本身是一種無狀態(tài)的協(xié)議,即每次請求之間都是相互獨立的,服務(wù)器無法記住當(dāng)前請求的用戶是誰,可想而知在絕大部分業(yè)務(wù)場景下,對于用戶來講這種體驗是非常糟糕的,而session便是一種讓web服務(wù)器與客戶端之間進(jìn)行狀態(tài)保持的解決方案.
session與cookie
session即會話,會話是存儲在web服務(wù)器端內(nèi)存中的,類似于map這樣的一種數(shù)據(jù)結(jié)構(gòu),session與cookie之間的關(guān)系簡單點說就是用戶在第一次通過瀏覽器訪問服務(wù)端時,服務(wù)端會為當(dāng)前用戶創(chuàng)建一個session對象并將生成的唯一標(biāo)識sessionid返回給客戶端瀏覽器存儲到cookie中,當(dāng)用戶登錄之后,服務(wù)端通過瀏覽器請求頭中傳遞過來的sessionid在web容器中找到對應(yīng)session并將用戶的個人信息與登錄狀態(tài)和其綁定起來,這樣子以后每次請求,服務(wù)端都能與客戶端建立起有效的狀態(tài)保持了.
什么是session共享
通常在單體應(yīng)用下是無需考慮session的共享問題的,因為這種架構(gòu)一般是集中式部署的,即所有的代碼都部署到一臺web服務(wù)器上,代碼分層也是很經(jīng)典的MVC三層架構(gòu).

此時應(yīng)用雖然不存在單點問題,但是也由于服務(wù)器是多個節(jié)點同時向外提供服務(wù)的,如果此時前端負(fù)載均衡器將客戶端請求分發(fā)到不同的服務(wù)器節(jié)點上面,就會因為其中某些節(jié)點上沒有用戶session而導(dǎo)致請求失效.所以在單體架構(gòu)中依賴于容器自身所提供的session管理方案已經(jīng)無法滿足分布式或集群場景下的需求,于是我們需要有一套機(jī)制來保證當(dāng)服務(wù)器在多節(jié)點的情況下session數(shù)據(jù)可以共享.
session一致性解決方案
接下來我們來看看主要有哪些常見的session共享方案.
**1.session復(fù)制 **
這種方案主要依賴于tomcat等web服務(wù)器,可在多個服務(wù)器之間自動實時復(fù)制session數(shù)據(jù),如利用Terracotta來實現(xiàn)tomcat間session共享,配置對原來的應(yīng)用完全透明,原有程序幾乎不用做任何修改,而且Terracotta本身支持HA或者使用tomcat自帶的cluster也可以,但是這些方案效率較低,用戶量大并發(fā)量大時會大量占用網(wǎng)絡(luò)帶寬而且可能有延遲,整體上來說非常消耗系統(tǒng)資源.
**2.nginx配置ip的hash路由策略 **
利用nginx的基于訪問ip的hash路由策略,其原理就是同一個ip的所有請求都會被nginx進(jìn)行ip_hash進(jìn)行計算,通過結(jié)果定位到指定的后臺服務(wù)器即一個用戶如果ip不變,那么每次請求的都是同一后臺服務(wù)器.但是最外層的代理要保證源ip在請求的過程不會被修改,如果你的架構(gòu)里在最外層不單單是nginx服務(wù),而是類似于請求分發(fā)的服務(wù)那么一個用戶的請求可能被定位到不同的服務(wù)器上或者說一個局域網(wǎng)有許多用戶同時登錄系統(tǒng)的話,那么ip_hash就沒有什么用了.
**3.存儲在cookie中 **
session也可存儲于客戶端cookie中,但數(shù)據(jù)大小有限制,且用戶有可能禁用cookie,存在安全隱患.
**4.Spring Session **
spring提供的一整套支持分布式session管理的方案,默認(rèn)采用外置的redis來存儲數(shù)據(jù),以此來解決會話共享的問題。
**5.實現(xiàn)獨立的會話中心 **
利用如數(shù)據(jù)庫、redis或者memcache等第三方存儲介質(zhì)來保存會話信息,負(fù)責(zé)session數(shù)據(jù)共享,并實現(xiàn)獨立的會話中心來管理session的生命周期,讓其不再與web容器耦合在一起.
在項目中我們采取了自建會話中心的方式來支持session共享.
shiro如何結(jié)合redis實現(xiàn)session共享
首先shiro提供了可擴(kuò)展會話管理的頂層接口AbstractSessionDAO,我們可以自定義自己的RedisSessionDao類繼承自AbstractSessionDAO,在此實現(xiàn)類中分別覆寫以下方法:
doCreate():用戶第一次訪問系統(tǒng)時創(chuàng)建會話信息
doReadSession():讀取會話信息
update():更新用戶會話信息
delete():刪除用戶會話信息
getActiveSessions():獲取所有的在線會話信息
這幾個方法即提供了對會話的基本管理crud以及session超時策略的控制,主要代碼如下:
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = SessionCons.TOKEN_PREFIX
+ UUID.randomUUID().toString();
assignSessionId(session, sessionId);
redisTemplate.opsForValue().set(sessionId, session);
redisTemplate.expire(sessionId, PC_EXPIRE_TIME, TimeUnit.SECONDS);
if (logger.isDebugEnabled()) {
logger.debug("create shiro session ,sessionId is :{}",
sessionId.toString());
}
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
Session session = redisTemplate.opsForValue().get(sessionId);
if (null != session) {
String deviceType = (String) session
.getAttribute(SessionCons.DEVICE_TYPE);
if (StringHelpUtils.isNotBlank(deviceType)) {
if (deviceType.equals(DeviceType.PC.toString())) {
// PC會話信息
session.setTimeout(PC_EXPIRE_TIME * 1000);
if (logger.isDebugEnabled()) {
logger.debug("read pc session ,sessionId is :{}",
sessionId.toString());
}
} else {
// APP會話信息
session.setTimeout(APP_EXPIRE_TIME * 1000);
if (logger.isDebugEnabled()) {
logger.debug("read app session ,sessionId is :{}",
sessionId.toString());
}
}
}
}
return session;
}
@Override
public void update(Session session) throws UnknownSessionException {
if (null != session && null != session.getId()) {
String deviceType = (String) session
.getAttribute(SessionCons.DEVICE_TYPE);
if (StringHelpUtils.isBlank(deviceType))
deviceType = DeviceType.PC.toString();
redisTemplate.opsForValue().set(session.getId(), session);
if (deviceType.equals(DeviceType.PC.toString())) {
// PC會話信息
session.setTimeout(PC_EXPIRE_TIME * 1000);
redisTemplate.expire(session.getId(), PC_EXPIRE_TIME,
TimeUnit.SECONDS);
if (logger.isDebugEnabled()) {
logger.debug("update pc session ,sessionId is :{}", session
.getId().toString());
}
} else {
// APP會話信息
session.setTimeout(APP_EXPIRE_TIME * 1000);
redisTemplate.expire(session.getId(), APP_EXPIRE_TIME,
TimeUnit.SECONDS);
if (logger.isDebugEnabled()) {
logger.debug("update app session ,sessionId is :{}",
session.getId().toString());
}
}
}
}
@Override
public void delete(Session session) {
if (logger.isDebugEnabled()) {
logger.debug("delete shiro session ,sessionId is :{}", session
.getId().toString());
}
redisTemplate.opsForValue().getOperations().delete(session.getId());
}
@Override
public Collection<Session> getActiveSessions() {
Set<Serializable> keys = redisTemplate
.keys(SessionCons.TOKEN_PREFIX_KEY);
if (keys.size() == 0) {
return Collections.emptySet();
}
List<Session> sessions = redisTemplate.opsForValue().multiGet(keys);
return Collections.unmodifiableCollection(sessions);
}
然后在shiro的配置類中配置自定義的sessionDAO.
@Bean
public RedisSessionDao redisSessionDAO() {
return new RedisSessionDao();
}
設(shè)置shiro的會話管理器.
@Bean
public CustomerWebSessionManager sessionManager() {
CustomerWebSessionManager sessionManager = new CustomerWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
......
return sessionManager;
}
接著定義一個會話常量類SessionCons,內(nèi)部包含會話key前綴等系統(tǒng)常量.
String LOGIN_USER_PERMISSIONS = "session_login_user_permissions";
String LOGIN_USER_SESSION = "session_login_user";
String TOKEN_PREFIX = "web_session_key-";
String TOKEN_PREFIX_KEY = "web_session_key-*";
String DEVICE_TYPE = "device_type";
......
最后在用戶登錄之后的時候就可以把相關(guān)的用戶session信息和權(quán)限數(shù)據(jù)存儲到redis中了.
subject.getSession().setAttribute(SessionCons.LOGIN_USER_SESSION,loginAccinfo);
subject.getSession().setAttribute(SessionCons.DEVICE_TYPE,DeviceTypePC.toString());
subject.getSession().setAttribute(SessionCons.LOGIN_USER_PERMISSIONS, permissions);
......
到此我們已經(jīng)在之前的框架基礎(chǔ)上實現(xiàn)了分布式session管理了.