一、Shiro会话管理之Session共享

(一)为什么要Session共享
  这里的Session共享是在分布式情境下的,若是单机应用,就没有Session共享这一说法。

  Session是由处理请求的服务器创建、持有、销毁的,如果是多台服务器,即分布式,如果同一用户的第一次请求被a服务器处理,session则在a服务器哪里,如果第二次请求被分配到b服务器,b服务器则拿不到session。

  而为了解决上述问题,所以采用session共享。这里的session共享是通过存储在redis中实现的,当a服务器创建好session后,保存进redis中,这样b服务器也能从redis中拿到session。

(二)session共享的代码实现

1、添加POM依赖

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>

2、创建​​SessionDao​​​,实现将​​Session​​​存入​​redis​​​或删除
(1)创建​​​SessionDao​

package org.pc.session;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.pc.util.JedisUtil;
import org.springframework.util.CollectionUtils;
import org.springframework.util.SerializationUtils;

import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

/**
* @author 咸鱼
* @date 2018/9/9 17:38
* AbstractSessionDAO就是Shiro自己的session
*/
public class SessionDao extends AbstractSessionDAO {

@Resource
private JedisUtil jedisUtil;

private static final String SHIRO_SESSION_PREFIX = "redis-session";
private byte[] getKey(String key){
return (SHIRO_SESSION_PREFIX + key).getBytes();
}

/**
* 创建session,存入redis
* @param session 传入的session对象
* @return 返回序列化后的sesssionId
*/
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
//父类方法,将sessionId与session进行捆绑
assignSessionId(session, sessionId);
saveSessionInRedis(session);
return sessionId;
}

/**
* 保存session进redis,实现session共享
* @param session 待保存对象
*/
private void saveSessionInRedis(Session session) {
if (session != null && session.getId() != null){
//session的ID为key
byte[] key = getKey(session.getId().toString());
//经过序列化的session对象为value(序列化:将对象转换成二进制数组)
byte[] value = SerializationUtils.serialize(session);
//将session存入缓存中(redis中)
jedisUtil.set(key, value);
//设置过期时间,600秒
jedisUtil.expire(key, 600);
}
}

/**
* 从redis中获取session
* @param sessionId session的ID
* @return session对象
*/
@Override
protected Session doReadSession(Serializable sessionId) {
if (sessionId == null){
return null;
}
byte[] key = getKey(sessionId.toString());
byte[] value = jedisUtil.get(key);
//只有对象进行序列化时才需要序列化和反序列化,而像String等类型对象,可直接转换成字节数组
return (Session) SerializationUtils.deserialize(value);
}

/**
* 更新session
* @param session 待更新session
* @throws UnknownSessionException 未知异常
*/
@Override
public void update(Session session) throws UnknownSessionException {
saveSessionInRedis(session);
}

@Override
public void delete(Session session) {
if (session != null && session.getId() != null){
byte[] key = getKey(session.getId().toString());
jedisUtil.delete(key);
}
}

/**
* 获取所有存活的session
* @return session集合
*/
@Override
public Collection<Session> getActiveSessions() {
Set<byte[]> keys = jedisUtil.keys(SHIRO_SESSION_PREFIX + "*");
Set<Session> sessions = new HashSet<Session>();
if (!CollectionUtils.isEmpty(keys)){
for (byte[] key : keys){
sessions.add((Session) SerializationUtils.deserialize(jedisUtil.get(key)));
}
}
return sessions;
}
}

(2)要想正常使用SessionDao,还需要个与redis交互的工具类JedisUtil

package org.pc.util;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import javax.annotation.Resource;
import java.util.Set;

/**
* @author 咸鱼
* @date 2018/9/9 17:42
*/
public class JedisUtil {
@Resource
private JedisPool jedisPool;

/**
* 从redis连接池中获取redis连接
*/
private Jedis getResource(){
return jedisPool.getResource();
}

/**
* 关闭redis连接,将资源返还给redis连接池
*/
private void closeJedis(Jedis jedis){
if (jedis != null){
jedis.close();
}
}


public void set(byte[] key, byte[] value) {
Jedis jedis = getResource();
try {
jedis.set(key, value);
} catch (Exception e) {
e.printStackTrace();
} finally {
closeJedis(jedis);
}
}

public void expire(byte[] key, int timeout) {
Jedis jedis = getResource();
try {
jedis.expire(key, timeout);
} catch (Exception e) {
e.printStackTrace();
} finally {
closeJedis(jedis);
}
}

public byte[] get(byte[] key) {
Jedis jedis = getResource();
try {
return jedis.get(key);
} catch (Exception e) {
e.printStackTrace();
} finally {
closeJedis(jedis);
}
return null;
}

public void delete(byte[] key) {
Jedis jedis = getResource();
try {
jedis.del(key);
} catch (Exception e) {
e.printStackTrace();
} finally {
closeJedis(jedis);
}
}

public Set<byte[]> keys(String s) {
Jedis jedis = getResource();
try {
//key的类型是什么,返回的类型就是什么
return jedis.keys(s.getBytes());
} catch (Exception e) {
e.printStackTrace();
} finally {
closeJedis(jedis);
}
return null;
}
}

(3)配置该工具类(applicationContext-redis.xml)
  备注:若jedisUtil总是为null,那就用注解来注入该对象

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd ">
<!--配置jedis-->

<bean id="jedisUtil" class="org.pc.util.JedisUtil"/>

<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
<constructor-arg name="poolConfig" ref="poolConfig"/>
<constructor-arg name="host" value="192.168.10.130"/>
<constructor-arg name="port" value="6379"/>
</bean>

<bean id="poolConfig" class="org.apache.commons.pool2.impl.GenericObjectPoolConfig"/>
</beans>

(4)配置SessionDao及其它相关配置

<!--配置securityManager,注意在Spring中使用的是DefaultWebSecurityManager,在非web环境下,使用DefaultSecurityManager-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--配置数据源-->
<property name="realm" ref="realm"/>
<!--配置Shiro的SessionManager对象-->
<property name="sessionManager" ref="sessionManager"/>
</bean>
<!--配置Shiro的SessionManager对象-->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="sessionDAO" ref="sessionDao"/>
</bean>
<!--配置Shiro的增删改查管理对象-->
<bean id="sessionDao" class="org.pc.session.SessionDao"/>

3、出现问题及其改进

(1)问题

  经过测试,在我们将session放入redis中后,我们在进行一次页面跳转时,会反复多次从redis中读取session,这样带来的问题就是redis压力过大。

  我们需要对默认的读取session方法进行改进,保证一次页面跳转只读取一次session。

(2)改进
自定义​​​CustomSessionManager​

public class CustomSessionManager extends DefaultWebSessionManager {
/**
* 只需要重写这个调用session的方法
* 备注:这个原生方法是调用SessionDao来获取session,而SessionDao是与redis交互来获取session的,所以为了
* 减轻redis的压力,要改造原生方法,让其从request请求中获取session
* @param sessionKey 该对象存储访问请求对象(request)----关键
* @return 返回session对象
*/
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
//1.调用父类方法获取sessionId
Serializable sessionId = getSessionId(sessionKey);

ServletRequest request = null;
//2、将sessionKey对象强转为WebSessionKey,从而获取到ServletRequest对象
if (sessionKey instanceof WebSessionKey){
request = ((WebSessionKey) sessionKey).getServletRequest();
}

//3、获取session并返回
Session session = null;
if (request != null && sessionId != null){
//3.1 从request中取出session
session = (Session) request.getAttribute(sessionId.toString());
if (session == null){
//3.2 若request中取出的session为null,则需要从redis中获取session,所以调用父类的获取session方法
session = super.retrieveSession(sessionKey);
//3.3获取到session以后,放到request中
request.setAttribute(sessionId.toString(), session);
}
}
return session;
}
}

配置该对象,取代​​Shiro​​​原生的​​DefaultWebSessionManager​

<!--配置securityManager,注意在Spring中使用的是DefaultWebSecurityManager,在非web环境下,使用DefaultSecurityManager-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--配置数据源-->
<property name="realm" ref="realm"/>
<!--配置Shiro的SessionManager对象-->
<property name="sessionManager" ref="sessionManager"/>
</bean>
<!--<!&ndash;配置Shiro的SessionManager对象&ndash;>-->
<!--<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">-->
<!--<property name="sessionDAO" ref="sessionDao"/>-->
<!--</bean>-->
<!--配置自定义的SessionManager对象-->
<bean id="sessionManager" class="org.pc.session.CustomSessionManager">
<property name="sessionDAO" ref="sessionDao"/>
</bean>
<!--配置Shiro的增删改查管理对象-->
<bean id="sessionDao" class="org.pc.session.SessionDao"/>