一、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。
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;
}
}
<!--配置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>-->
<!--配置自定義的SessionManager對象-->
<bean id="sessionManager" class="org.pc.session.CustomSessionManager">
<property name="sessionDAO" ref="sessionDao"/>
</bean>
<!--配置Shiro的增删改查管理對象-->
<bean id="sessionDao" class="org.pc.session.SessionDao"/>