redis插件连接集群 shiro_Shiro经过Redis管理会话实现集群(转载)
寫(xiě)在前面
1.在上一篇帖子?Shiro一些補(bǔ)充?中提到過(guò)Shiro可以使用Shiro自己的Session或者自定義的Session來(lái)代替HttpSession
2.Redis/Jedis參考我寫(xiě)的?http://sgq0085.iteye.com/category/317384 一系列內(nèi)容
一. SessionDao
配置在sessionManager中,可選項(xiàng),如果不修改默認(rèn)使用MemorySessionDAO,即在本機(jī)內(nèi)存中操作。
如果想通過(guò)Redis管理Session,從這里入手。只需要實(shí)現(xiàn)類(lèi)似DAO接口的CRUD即可。
經(jīng)過(guò)1:最開(kāi)始通過(guò)繼承AbstractSessionDAO實(shí)現(xiàn),發(fā)現(xiàn)doReadSession方法調(diào)用過(guò)于頻繁,所以改為通過(guò)集成CachingSessionDAO來(lái)實(shí)現(xiàn)。
注意,本地緩存通過(guò)EhCache實(shí)現(xiàn),失效時(shí)間一定要遠(yuǎn)小于Redis失效時(shí)間,這樣本地失效后,會(huì)訪問(wèn)Redis讀取,并重新設(shè)置Redis上會(huì)話數(shù)據(jù)的過(guò)期時(shí)間。
因?yàn)镴edis API KEY和Value相同,同為String或同為byte[]為了方便擴(kuò)展下面的方法
package com.gqshao.authentication.utils;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.session.Session;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
public class SerializeUtils extends SerializationUtils {
public static String serializeToString(Serializable obj) {
try {
byte[] value = serialize(obj);
return Base64.encodeToString(value);
} catch (Exception e) {
throw new RuntimeException("serialize session error", e);
}
}
public static Session deserializeFromString(String base64) {
try {
byte[] objectData = Base64.decode(base64);
return deserialize(objectData);
} catch (Exception e) {
throw new RuntimeException("deserialize session error", e);
}
}
public static Collection deserializeFromStringController(Collection base64s) {
try {
List list = Lists.newLinkedList();
for (String base64 : base64s) {
byte[] objectData = Base64.decode(base64);
T t = deserialize(objectData);
list.add(t);
}
return list;
} catch (Exception e) {
throw new RuntimeException("deserialize session error", e);
}
}
}
我的Dao實(shí)現(xiàn),ShiroSession是我自己實(shí)現(xiàn)的,原因在后面說(shuō)明,默認(rèn)使用的是SimpleSession
package com.gqshao.authentication.dao;
import com.gqshao.authentication.session.ShiroSession;
import com.gqshao.authentication.utils.SerializeUtils;
import com.gqshao.redis.component.JedisUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.ValidatingSession;
import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.apache.shiro.util.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* 針對(duì)自定義的ShiroSession的Redis CRUD操作,通過(guò)isChanged標(biāo)識(shí)符,確定是否需要調(diào)用Update方法
* 通過(guò)配置securityManager在屬性cacheManager查找從緩存中查找Session是否存在,如果找不到才調(diào)用下面方法
* Shiro內(nèi)部相應(yīng)的組件(DefaultSecurityManager)會(huì)自動(dòng)檢測(cè)相應(yīng)的對(duì)象(如Realm)是否實(shí)現(xiàn)了CacheManagerAware并自動(dòng)注入相應(yīng)的CacheManager。
*/
public class CachingShiroSessionDao extends CachingSessionDAO {
private static final Logger logger = LoggerFactory.getLogger(CachingShiroSessionDao.class);
// 保存到Redis中key的前綴 prefix+sessionId
private String prefix = "";
// 設(shè)置會(huì)話的過(guò)期時(shí)間
private int seconds = 0;
@Autowired
private JedisUtils jedisUtils;
/**
* 重寫(xiě)CachingSessionDAO中readSession方法,如果Session中沒(méi)有登陸信息就調(diào)用doReadSession方法從Redis中重讀
*/
@Override
public Session readSession(Serializable sessionId) throws UnknownSessionException {
Session session = getCachedSession(sessionId);
if (session == null
|| session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {
session = this.doReadSession(sessionId);
if (session == null) {
throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
} else {
// 緩存
cache(session, session.getId());
}
}
return session;
}
/**
* 根據(jù)會(huì)話ID獲取會(huì)話
*
* @param sessionId 會(huì)話ID
* @return ShiroSession
*/
@Override
protected Session doReadSession(Serializable sessionId) {
Session session = null;
Jedis jedis = null;
try {
jedis = jedisUtils.getResource();
String key = prefix + sessionId;
String value = jedis.get(key);
if (StringUtils.isNotBlank(value)) {
session = SerializeUtils.deserializeFromString(value);
logger.info("sessionId {} ttl {}: ", sessionId, jedis.ttl(key));
// 重置Redis中緩存過(guò)期時(shí)間
jedis.expire(key, seconds);
logger.info("sessionId {} name {} 被讀取", sessionId, session.getClass().getName());
}
} catch (Exception e) {
logger.warn("讀取Session失敗", e);
} finally {
jedisUtils.returnResource(jedis);
}
return session;
}
public Session doReadSessionWithoutExpire(Serializable sessionId) {
Session session = null;
Jedis jedis = null;
try {
jedis = jedisUtils.getResource();
String key = prefix + sessionId;
String value = jedis.get(key);
if (StringUtils.isNotBlank(value)) {
session = SerializeUtils.deserializeFromString(value);
}
} catch (Exception e) {
logger.warn("讀取Session失敗", e);
} finally {
jedisUtils.returnResource(jedis);
}
return session;
}
/**
* 如DefaultSessionManager在創(chuàng)建完session后會(huì)調(diào)用該方法;
* 如保存到關(guān)系數(shù)據(jù)庫(kù)/文件系統(tǒng)/NoSQL數(shù)據(jù)庫(kù);即可以實(shí)現(xiàn)會(huì)話的持久化;
* 返回會(huì)話ID;主要此處返回的ID.equals(session.getId());
*/
@Override
protected Serializable doCreate(Session session) {
// 創(chuàng)建一個(gè)Id并設(shè)置給Session
Serializable sessionId = this.generateSessionId(session);
assignSessionId(session, sessionId);
Jedis jedis = null;
try {
jedis = jedisUtils.getResource();
// session由Redis緩存失效決定,這里只是簡(jiǎn)單標(biāo)識(shí)
session.setTimeout(seconds);
jedis.setex(prefix + sessionId, seconds, SerializeUtils.serializeToString((ShiroSession) session));
logger.info("sessionId {} name {} 被創(chuàng)建", sessionId, session.getClass().getName());
} catch (Exception e) {
logger.warn("創(chuàng)建Session失敗", e);
} finally {
jedisUtils.returnResource(jedis);
}
return sessionId;
}
/**
* 更新會(huì)話;如更新會(huì)話最后訪問(wèn)時(shí)間/停止會(huì)話/設(shè)置超時(shí)時(shí)間/設(shè)置移除屬性等會(huì)調(diào)用
*/
@Override
protected void doUpdate(Session session) {
//如果會(huì)話過(guò)期/停止 沒(méi)必要再更新了
try {
if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
return;
}
} catch (Exception e) {
logger.error("ValidatingSession error");
}
Jedis jedis = null;
try {
if (session instanceof ShiroSession) {
// 如果沒(méi)有主要字段(除lastAccessTime以外其他字段)發(fā)生改變
ShiroSession ss = (ShiroSession) session;
if (!ss.isChanged()) {
return;
}
Transaction tx = null;
try {
jedis = jedisUtils.getResource();
// 開(kāi)啟事務(wù)
tx = jedis.multi();
ss.setChanged(false);
tx.setex(prefix + session.getId(), seconds, SerializeUtils.serializeToString(ss));
logger.info("sessionId {} name {} 被更新", session.getId(), session.getClass().getName());
// 執(zhí)行事務(wù)
tx.exec();
} catch (Exception e) {
if (tx != null) {
// 取消執(zhí)行事務(wù)
tx.discard();
}
throw e;
}
} else if (session instanceof Serializable) {
jedis = jedisUtils.getResource();
jedis.setex(prefix + session.getId(), seconds, SerializeUtils.serializeToString((Serializable) session));
logger.info("sessionId {} name {} 作為非ShiroSession對(duì)象被更新, ", session.getId(), session.getClass().getName());
} else {
logger.warn("sessionId {} name {} 不能被序列化 更新失敗", session.getId(), session.getClass().getName());
}
} catch (Exception e) {
logger.warn("更新Session失敗", e);
} finally {
jedisUtils.returnResource(jedis);
}
}
/**
* 刪除會(huì)話;當(dāng)會(huì)話過(guò)期/會(huì)話停止(如用戶(hù)退出時(shí))會(huì)調(diào)用
*/
@Override
protected void doDelete(Session session) {
Jedis jedis = null;
try {
jedis = jedisUtils.getResource();
jedis.del(prefix + session.getId());
logger.debug("Session {} 被刪除", session.getId());
} catch (Exception e) {
logger.warn("修改Session失敗", e);
} finally {
jedisUtils.returnResource(jedis);
}
}
/**
* 刪除cache中緩存的Session
*/
public void uncache(Serializable sessionId) {
Session session = this.readSession(sessionId);
super.uncache(session);
logger.info("取消session {} 的緩存", sessionId);
}
/**
* 獲取當(dāng)前所有活躍用戶(hù),如果用戶(hù)量多此方法影響性能
*/
@Override
public Collection getActiveSessions() {
Jedis jedis = null;
try {
jedis = jedisUtils.getResource();
Set keys = jedis.keys(prefix + "*");
if (CollectionUtils.isEmpty(keys)) {
return null;
}
List valueList = jedis.mget(keys.toArray(new String[0]));
return SerializeUtils.deserializeFromStringController(valueList);
} catch (Exception e) {
logger.warn("統(tǒng)計(jì)Session信息失敗", e);
} finally {
jedisUtils.returnResource(jedis);
}
return null;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public void setSeconds(int seconds) {
this.seconds = seconds;
}
}
二.Session和SessionFactory
步驟2:經(jīng)過(guò)上面的開(kāi)發(fā)已經(jīng)可以使用的,但發(fā)現(xiàn)每次訪問(wèn)都會(huì)多次調(diào)用SessionDAO的doUpdate方法,來(lái)更新Redis上數(shù)據(jù),過(guò)來(lái)發(fā)現(xiàn)更新的字段只有LastAccessTime(最后一次訪問(wèn)時(shí)間),由于會(huì)話失效是由Redis數(shù)據(jù)過(guò)期實(shí)現(xiàn)的,這個(gè)字段意義不大,為了減少對(duì)Redis的訪問(wèn),降低網(wǎng)絡(luò)壓力,實(shí)現(xiàn)自己的Session,在SimpleSession上套一層,增加一個(gè)標(biāo)識(shí)位,如果Session除lastAccessTime意外其它字段修改,就標(biāo)識(shí)一下,只有標(biāo)識(shí)為修改的才可以通過(guò)doUpdate訪問(wèn)Redis,否則直接返回。這也是上面SessionDao中doUpdate中邏輯判斷的意義
package com.gqshao.authentication.session;
import org.apache.shiro.session.mgt.SimpleSession;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;
/**
* 由于SimpleSession lastAccessTime更改后也會(huì)調(diào)用SessionDao update方法,
* 增加標(biāo)識(shí)位,如果只是更新lastAccessTime SessionDao update方法直接返回
*/
public class ShiroSession extends SimpleSession implements Serializable {
// 除lastAccessTime以外其他字段發(fā)生改變時(shí)為true
private boolean isChanged;
public ShiroSession() {
super();
this.setChanged(true);
}
public ShiroSession(String host) {
super(host);
this.setChanged(true);
}
@Override
public void setId(Serializable id) {
super.setId(id);
this.setChanged(true);
}
@Override
public void setStopTimestamp(Date stopTimestamp) {
super.setStopTimestamp(stopTimestamp);
this.setChanged(true);
}
@Override
public void setExpired(boolean expired) {
super.setExpired(expired);
this.setChanged(true);
}
@Override
public void setTimeout(long timeout) {
super.setTimeout(timeout);
this.setChanged(true);
}
@Override
public void setHost(String host) {
super.setHost(host);
this.setChanged(true);
}
@Override
public void setAttributes(Map attributes) {
super.setAttributes(attributes);
this.setChanged(true);
}
@Override
public void setAttribute(Object key, Object value) {
super.setAttribute(key, value);
this.setChanged(true);
}
@Override
public Object removeAttribute(Object key) {
this.setChanged(true);
return super.removeAttribute(key);
}
/**
* 停止
*/
@Override
public void stop() {
super.stop();
this.setChanged(true);
}
/**
* 設(shè)置過(guò)期
*/
@Override
protected void expire() {
this.stop();
this.setExpired(true);
}
public boolean isChanged() {
return isChanged;
}
public void setChanged(boolean isChanged) {
this.isChanged = isChanged;
}
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
@Override
protected boolean onEquals(SimpleSession ss) {
return super.onEquals(ss);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public String toString() {
return super.toString();
}
}
package com.gqshao.authentication.session;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionContext;
import org.apache.shiro.session.mgt.SessionFactory;
public class ShiroSessionFactory implements SessionFactory {
@Override
public Session createSession(SessionContext initData) {
ShiroSession session = new ShiroSession();
return session;
}
}
三.SessionListener
步驟3:發(fā)現(xiàn)用戶(hù)推出后,Session沒(méi)有從Redis中銷(xiāo)毀,雖然當(dāng)前重新new了一個(gè),但會(huì)對(duì)統(tǒng)計(jì)帶來(lái)干擾,通過(guò)SessionListener解決這個(gè)問(wèn)題
package com.gqshao.authentication.listener;
import com.gqshao.authentication.dao.CachingShiroSessionDao;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
public class ShiroSessionListener implements SessionListener {
private static final Logger logger = LoggerFactory.getLogger(ShiroSessionListener.class);
@Autowired
private CachingShiroSessionDao sessionDao;
@Override
public void onStart(Session session) {
// 會(huì)話創(chuàng)建時(shí)觸發(fā)
logger.info("ShiroSessionListener session {} 被創(chuàng)建", session.getId());
}
@Override
public void onStop(Session session) {
sessionDao.delete(session);
// 會(huì)話被停止時(shí)觸發(fā)
logger.info("ShiroSessionListener session {} 被銷(xiāo)毀", session.getId());
}
@Override
public void onExpiration(Session session) {
sessionDao.delete(session);
//會(huì)話過(guò)期時(shí)觸發(fā)
logger.info("ShiroSessionListener session {} 過(guò)期", session.getId());
}
}
四.將賬號(hào)信息放到Session中
修改realm中AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)方法,在返回AuthenticationInfo之前添加下面的代碼,把用戶(hù)信息放到Session中
// 把賬號(hào)信息放到Session中,并更新緩存,用于會(huì)話管理
Subject subject = SecurityUtils.getSubject();
Serializable sessionId = subject.getSession().getId();
ShiroSession session = (ShiroSession) sessionDao.doReadSessionWithoutExpire(sessionId);
session.setAttribute("userId", su.getId());
session.setAttribute("loginName", su.getLoginName());
sessionDao.update(session);
五.?配置文件
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd">
Shiro安全配置
/login = authc
/logout = logout
/static/** = anon
/** = user
depends-on="lifecycleBeanPostProcessor">
maxElementsInMemory="10000"
eternal="false"
timeToLiveSeconds="60"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="10"
/>
六.測(cè)試會(huì)話管理
package com.gqshao.authentication.controller;
import com.gqshao.authentication.dao.CachingShiroSessionDao;
import org.apache.shiro.session.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.Serializable;
import java.util.Collection;
@Controller
@RequestMapping("/session")
public class SessionController {
@Autowired
private CachingShiroSessionDao sessionDao;
@RequestMapping("/active")
@ResponseBody
public Collection getActiveSessions() {
return sessionDao.getActiveSessions();
}
@RequestMapping("/read")
@ResponseBody
public Session readSession(Serializable sessionId) {
return sessionDao.doReadSessionWithoutExpire(sessionId);
}
}
七.集群情況下的改造
1.問(wèn)題上面啟用了Redis中央緩存、EhCache本地JVM緩存,AuthorizingRealm的doGetAuthenticationInfo登陸認(rèn)證方法返回的AuthenticationInfo,默認(rèn)情況下會(huì)被保存到Session的Attribute下面兩個(gè)字段中
org.apache.shiro.subject.support.DefaultSubjectContext.PRINCIPALS_SESSION_KEY 保存 principal
org.apache.shiro.subject.support.DefaultSubjectContext.AUTHENTICATED_SESSION_KEY 保存 boolean是否登陸
然后在每次請(qǐng)求過(guò)程中,在ShiroFilter中組裝Subject時(shí),讀取Session中這兩個(gè)字段
現(xiàn)在的問(wèn)題是Session被緩存到本地JVM堆中,也就是說(shuō)服務(wù)器A登陸,無(wú)法修改服務(wù)器B的EhCache中Session屬性,導(dǎo)致服務(wù)器B沒(méi)有登陸。
處理方法有很多思路,比如重寫(xiě)CachingSessionDAO,readSession如果沒(méi)有這兩個(gè)屬性就不緩存(沒(méi)登陸就不緩存),或者cache的session沒(méi)有這兩個(gè)屬性就調(diào)用自己實(shí)現(xiàn)的doReadSession方法從Redis中重讀一下。
/**
* 重寫(xiě)CachingSessionDAO中readSession方法,如果Session中沒(méi)有登陸信息就調(diào)用doReadSession方法從Redis中重讀
*/
@Override
public Session readSession(Serializable sessionId) throws UnknownSessionException {
Session session = getCachedSession(sessionId);
if (session == null
|| session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {
session = this.doReadSession(sessionId);
if (session == null) {
throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
} else {
// 緩存
cache(session, session.getId());
}
}
return session;
}
2.如果需要保持各個(gè)服務(wù)器Session是完全同步的,可以通過(guò)Redis消息訂閱/發(fā)布功能,再調(diào)用SessionDao中實(shí)現(xiàn)了刪除Session本地緩存的方法
總結(jié)
以上是生活随笔為你收集整理的redis插件连接集群 shiro_Shiro经过Redis管理会话实现集群(转载)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 麦冬泡酒的功效与作用、禁忌和食用方法
- 下一篇: curl 请求没反应_理解Redis的反