基于redis的session共享
由 Redis负责 session 数据的存储,而我们自己实现的 session manager 将负责 session 生命周期的管理。
一般的系统架构:
此架构存在着当redis master故障时, 虽然可以有一到多个备用slave,但是redis不会主动的进行master切换,这时session服务中断。
为了做到redis的高可用,引入了zookper或者haproxy或者keepalived来解决redis master slave的切换问题。即:
此体系结构中, redis master出现故障时, 通过haproxy设置redis slave为临时master, redis master重新恢复后,
再切换回去. 此方案中, redis-master 与redis-slave 是双向同步的, 解决目前redis单点问题. 这样保证了session信息
在redis中的高可用。
Shiro有默认的Session管理机制,通过MemorySessionDAO 进行Session的管理和维护。通过查看 org.apache.shiro.web.session.mgt.DefaultWebSessionManager 类继承的DefaultSessionManager源码,我们可以看到:
public DefaultSessionManager() {
this.deleteInvalidSessions = true;
this.sessionFactory = new SimpleSessionFactory();
this.sessionDAO = new MemorySessionDAO();
}
由于我们采用redis来管理Session,所以需要配置自己的SessionDao,配置如下:
<!-- session管理器 -->
<bean id="redisSessionDAO" class="com.shiro.session.RedisSessionDAO" />
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="sessionDAO" ref="redisSessionDAO"></property>
<!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->
<property name="sessionIdCookie" ref="sharesession" />
<!-- 定时检查失效的session -->
<property name="sessionValidationSchedulerEnabled" value="true" />
</bean>
<!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->
<bean id="sharesession" class="org.apache.shiro.web.servlet.SimpleCookie">
<!-- cookie的name,对应的默认是 JSESSIONID -->
<constructor-arg name="name" value="SHAREJSESSIONID" />
<!-- jsessionId的path为 / 用于多个系统共享jsessionId -->
<property name="path" value="/" />
<property name="httpOnly" value="true"/>
</bean>
RedisSessionDao
public class RedisSessionDAO extends AbstractSessionDAO {
private static Logger log = LoggerFactory.getLogger(RedisSessionDAO.class);
@Autowired
private RedisCache redisCache;
private String keyPrefix = "shiro_redis_session:";
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
storeSession(sessionId,session);
log.debug("generate session id:"+sessionId);
return sessionId;
}
private void storeSession(Serializable id, Session session) {
if(id == null){
throw new NullPointerException("id argument cannot be null.");
}
byte[] key = getByteKey(session.getId());
byte[] value = SerializationUtils.serialize(session);
int seconds = 1800;
redisCache.addCache(key,value,seconds);
}
@Override
protected Session doReadSession(Serializable sessionId) {
if(sessionId == null){
throw new NullPointerException("id argument cannot be null.");
}
Session session = (Session)SerializationUtils.deserialize(redisCache.get(getByteKey(sessionId)));
return session;
}
@Override
public void update(Session session) throws UnknownSessionException {
this.storeSession(session.getId(),session);
}
@Override
public void delete(Session session) {
if(session == null || session.getId() == null){
log.error("session or session id is null");
return;
}
byte[] key = getByteKey(session.getId());
redisCache.del(key);
}
@Override
public Collection<Session> getActiveSessions() {
Set<Session> sessions = new HashSet<Session>();
Set<byte[]> keys = redisCache.keys(this.keyPrefix + "*");
if(keys != null && keys.size()>0){
for(byte[] key:keys){
Session s = (Session)SerializationUtils.deserialize(redisCache.get(key));
sessions.add(s);
}
}
return sessions;
}
/**
* 获得byte[]型的key
* @return
*/
private byte[] getByteKey(Serializable sessionId){
String preKey = this.keyPrefix + sessionId;
return preKey.getBytes();
}
}
RedisCache是关于Redis 服务器的一些基本操作(增删改查),可以自己找一个关于这方面的操作类。
有一个关键问题是在 updateSession的时候, RedisCache中set的时候,无论是否这个key存在, 都需要set进去;如果redis中已经存在key ,在updateSession时候没有覆盖原来的session值,会发生登录失败 的问题;而我翻看shiro自带的SessionDao 是采用 MemorySessionDAO来管理Session的 ,查看源码发现 MemorySessionDAO在updateSession的时候采用的如下的方式:
private ConcurrentMap<Serializable, Session> sessions;
public void update(Session session) throws UnknownSessionException {
storeSession(session.getId(), session);
}
protected Session storeSession(Serializable id, Session session) {
if (id == null) {
throw new NullPointerException("id argument cannot be null.");
}
return sessions.putIfAbsent(id, session);
}
注意红色部分,MemorySessionDAO 管理session采用线程安全的ConcurrentMap管理,在更新的时候采用的是 putIfAbsent 方式,意思是:如果key不存在的情况下才put session,存在则不会更新 Session,但是可以登录成功,研究了半天没想明白。这个问题暂时隔着,不过不影响我们用redis管理Session。