目錄
1、爲什麼要使用redis管理session
很多公司會使用分佈式來部署項目,使用反向代理功能來分發請求,但是如果依舊採用session的方式來對登錄信息等進行管理,就會限制反向代理服務器的配置(根據ip,hash服務器地址),爲了解決這個問題,使用獨立的session服務器可以完美解決這個問題,如下圖所示
每個服務器都會從相同的redis裏讀取用戶登錄信息,如果用戶登錄認證是在服務器1上面完成的,服務器1會把相應的會話數據保存到redis中,當用戶訪問其他鏈接時,請求被分配到了服務器3,由於服務器是無狀態的,服務器3會到redis中查找該用戶狀態,查詢到已登錄後,處理數據,成功返回!
2、session的工作原理
在使用之前,我們先了解一下session的原理,session是一段會話,由於http是無狀態的,爲了保存會話狀態,纔有了session。session在服務器端是存放在內存中的,如果登錄用戶多會佔用很多內存資源,而session在客戶端時保存在cookie中的一段文本JSESSIONID,而這個id正式鏈接客戶端和服務器內存的憑證。
下圖模擬用戶登錄後修改密碼流程
3、session的生命週期
- session在用戶首次訪問系統時創建(訪問靜態資源不會創建)
- 生命週期可以由服務器手動配置,tomcat默認爲30分鐘
- 由於session對應的信息保存在cookie中,關閉瀏覽器並不會使session失效(除非改寫session)
- 服務器端可以使用 invalidate() 手動失效session
4、 shiro的session
4.1、官方說明
默認 SecurityManager 實現默認使用 DefaultSessionManager 開箱即用。該 DefaultSessionManager 實現提供了應用程序所需的所有企業級會話管理功能,例如會話驗證,孤立清理等。可在任何應用程序中使用。像所管理的所有其他組件一樣 SecurityManager , SessionManager 可以通過 Shiro 的所有默認 SecurityManager 實現( getSessionManager()/ setSessionManager() )上的JavaBeans風格的getter / setter方法來獲取或設置它們。每當創建或更新會話時,其數據都需要保留到存儲位置,以便應用程序稍後可以訪問它。類似地,當會話無效且已被更長時間使用時,需要將其從存儲中刪除,因此會話數據存儲空間不會耗盡。這些SessionManager實現將這些創建/讀取/更新/刪除(CRUD)操作委託給一個內部組件,該組件SessionDAO反映了數據訪問對象(DAO)設計模式。
SessionDAO的功能是您可以實現此接口以與所需的任何數據存儲進行通信。這意味着您的會話數據可以駐留在內存中,文件系統上,關係數據庫或NoSQL數據存儲區中,或您需要的任何其他位置。您可以控制持久性行爲。
您可以將任何SessionDAO實現配置爲默認SessionManager實例上的屬性。
我們可以根據下圖查看到shiro的session具體結構。
4.2、shiro默認session的實現
- 在之前的配置中,我們知道了shiro的核心配置項是 SecurityManager 類,發現這個類是提供 setSessionManager() 方法讓我們自定義session管理器的。
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
securityManager.setRememberMeManager(rememberMeManager());
securityManager.setCacheManager(myEhCacheManager());// 將緩存管理交給ehCache
securityManager.setSessionManager(****);//設置session管理器
return securityManager;
}
- 所有的配置都會注入我們使用的 DefaultWebSecurityManager 中,查看該類發現在構建實例的時候,系統默認設置了 ServletContainerSessionManager() 爲session管理器
class DefaultWebSecurityManager
......
//無參構造
public DefaultWebSecurityManager() {
super();
((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
this.sessionMode = HTTP_SESSION_MODE;
setSubjectFactory(new DefaultWebSubjectFactory());
setRememberMeManager(new CookieRememberMeManager());
setSessionManager(new ServletContainerSessionManager());
}
- 繼續查看 ServletContainerSessionManager 類,在new的時候,會依次執行父類的構造,在 SessionsSecurityManager 中,我們發現瞭如下代碼,設置sessionManager爲 DefaultSessionManager ,
/**
* Default no-arg constructor, internally creates a suitable default {@link SessionManager SessionManager} delegate
* instance.
*/
public SessionsSecurityManager() {
super();
this.sessionManager = new DefaultSessionManager();
applyCacheManagerToSessionManager();
}
- 而在 DefaultSessionManager 的構造中,我們找到了默認提供的sessionDao的實現 MemorySessionDAO 類
class DefaultSessionManager
......
public DefaultSessionManager() {
this.deleteInvalidSessions = true;
this.sessionFactory = new SimpleSessionFactory();
this.sessionDAO = new MemorySessionDAO();
}
- MemorySessionDAO 關係圖,可以看到,他繼承了AbstractSessionDAO抽象類,而頂級接口則是SessionDao層,另外以外的發現了shiro爲我們提供的一個企業級session緩存的實現類 EnterpriseCacheSessionDAO
6.查看了一下 EnterpriseCacheSessionDAO 類發現,這個緩存是利用了shiro提供的 Cache 接口 下的 MapCache 實現類實現的,ConcurrentHashMap作爲value來保證線程安全!
public class EnterpriseCacheSessionDAO extends CachingSessionDAO {
public EnterpriseCacheSessionDAO() {
setCacheManager(new AbstractCacheManager() {
@Override
protected Cache<Serializable, Session> createCache(String name) throws CacheException {
return new MapCache<Serializable, Session>(name, new ConcurrentHashMap<Serializable, Session>());
}
});
}
......
- shiro 提供的Cache接口另外實現類
8.回到 MemorySessionDAO 我們查看這個類的相關代碼
public class MemorySessionDAO extends AbstractSessionDAO {
private static final Logger log = LoggerFactory.getLogger(MemorySessionDAO.class);
//保存session的容器
private ConcurrentMap<Serializable, Session> sessions;
//構造
public MemorySessionDAO() {
this.sessions = new ConcurrentHashMap<Serializable, Session>();
}
//創建session
protected Serializable doCreate(Session session) {
//sessionid生成器
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
storeSession(sessionId, session);
return sessionId;
}
//存儲會話
protected Session storeSession(Serializable id, Session session) {
if (id == null) {
throw new NullPointerException("id argument cannot be null.");
}
//只有在key不存在或者key爲null的時候,value值纔會被覆蓋 jdk 1.8新特性
return sessions.putIfAbsent(id, session);
/**putIfAbsent 源碼展示
default V putIfAbsent(K key, V value) {
V v = get(key);
if (v == null) {
v = put(key, value);
}
return v;
}
*/
}
//根據sessionid獲取session對象
protected Session doReadSession(Serializable sessionId) {
return sessions.get(sessionId);
}
//根據sessionid更新session對象
public void update(Session session) throws UnknownSessionException {
storeSession(session.getId(), session);
}
//刪除session
public void delete(Session session) {
if (session == null) {
throw new NullPointerException("session argument cannot be null.");
}
Serializable id = session.getId();
if (id != null) {
sessions.remove(id);
}
}
//獲取存活的session對象
public Collection<Session> getActiveSessions() {
Collection<Session> values = sessions.values();
if (CollectionUtils.isEmpty(values)) {
return Collections.emptySet();
} else {
return Collections.unmodifiableCollection(values);
}
}
}
可以看到,在默認情況下,session的操作全是有 MemorySessionDAO 類實現的,我們猜想,新增redis操作類,繼承 AbstractSessionDAO ,重寫其中的重要方法,是不是就可以實現redis管理session了?
5、使用redis管理session(配置)
redis的安裝就不介紹了,只講解如何在shiro中使用redis管理session!
5.1 、引入jar包
上面雖然已經發現如何替換session操作類來實現session的redis管理,但是已經有的輪子直接拿來即可,首先我們要引入redis的jar
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
我在引入上面的包後會報錯,引入這個解決,未報錯不用引入,報錯原因未深究
err:Missing artifact com.sun:tools:jar:1.8.0
<!-- tools -->
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
<optional>true</optional>
</dependency>
5.2、新增redis配置
在boot配置文件中新增如下文件,我採用的是yml文件,其他格式需要自行轉換
application.yml
#redis
redis:
#redis機器ip
host: 127.0.0.1
#redis端口
port: 6379
#redis密碼
password: 123456
#默認數據庫
database: 10
#redis超時時間(毫秒),如果不設置,取默認值2000
timeout: 10000
5.3、ShiroConfig配置
- 新增讀取配置項屬性及redis實例和redisdao實例
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private int port;
@Value("${redis.password}")
private String password;
@Value("${redis.database}")
private int database;
@Value("${redis.timeout}")
private int timeout;
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);// 主機地址
redisManager.setPort(port);// 端口
redisManager.setPassword(password);// 訪問密碼
redisManager.setDatabase(database);// 默認數據庫
redisManager.setTimeout(timeout);// 過期時間
return redisManager;
}
/**
* SessionDAO的作用是爲Session提供CRUD並進行持久化的一個shiro組件 MemorySessionDAO 直接在內存中進行會話維護
* EnterpriseCacheSessionDAO
* 提供了緩存功能的會話維護,默認情況下使用MapCache實現,內部使用ConcurrentHashMap保存緩存的會話。
*
* @return
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDao = new RedisSessionDAO();
redisSessionDao.setKeyPrefix("shiro-session");//配置session前綴
redisSessionDao.setSessionIdGenerator(sessionIdGenerator());
redisSessionDao.setRedisManager(redisManager());
// session在redis中的保存時間,最好大於session會話超時時間
redisSessionDao.setExpire(timeout);
return redisSessionDao;
}
- 配置自定義sessionid生成器
/**
* 配置會話ID生成器
*
* @return
*/
@Bean
public SessionIdGenerator sessionIdGenerator() {
return new JavaUuidSessionIdGenerator();
}
- 配置session監聽器
/**
* 配置session監聽
*
* @return
*/
@Bean
public MySessionListener sessionListener() {
MySessionListener sessionListener = new MySessionListener();
return sessionListener;
}
MySessionListener 類
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
/**
*
* @ClassName: MySessionListener
* @Description 統計session數量
* @version
* @author JH
* @date 2019年9月2日 上午11:15:38
*/
public class MySessionListener implements SessionListener {
private final AtomicInteger sessionCount = new AtomicInteger(0);
/**
* 登錄
*/
@Override
public void onStart(Session session) {
sessionCount.incrementAndGet();
System.out.println("登錄,有效session數量:"+sessionCount.get());
}
/**
* 登出
*/
@Override
public void onStop(Session session) {
sessionCount.decrementAndGet();
System.out.println("登出,有效session數量:"+sessionCount.get());
}
/**
* session過期
*/
@Override
public void onExpiration(Session session) {
sessionCount.decrementAndGet();
System.out.println("session過期,有效session數量:"+sessionCount.get());
}
}
- 配置自定義session的cookie,替換JSESSIONID
/**
* 配置保存sessionId的cookie 注意:這裏的cookie 不是上面的記住我 cookie 記住我需要一個cookie session管理
* 也需要自己的cookie 默認爲: JSESSIONID 問題: 與SERVLET容器名衝突,重新定義爲sid
*
* @return
*/
@Bean("sessionIdCookie")
public SimpleCookie sessionIdCookie() {
// 這個參數是cookie的名稱
SimpleCookie simpleCookie = new SimpleCookie("REDIS-SESSION");
// setcookie的httponly屬性如果設爲true的話,會增加對xss防護的安全係數。它有以下特點:
// setcookie()的第七個參數
// 設爲true後,只能通過http訪問,javascript無法訪問
// 防止xss讀取cookie
simpleCookie.setHttpOnly(true);
simpleCookie.setPath("/");
// maxAge=-1表示瀏覽器關閉時失效此Cookie
simpleCookie.setMaxAge(-1);
return simpleCookie;
}
- 配置會話管理器
/**
* 配置會話管理器,設定會話超時及保存
*
* @return
*/
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
Collection<SessionListener> listeners = new ArrayList<SessionListener>();
// 配置監聽
listeners.add(sessionListener());
sessionManager.setSessionListeners(listeners);
sessionManager.setSessionIdCookie(sessionIdCookie());
sessionManager.setSessionDAO(redisSessionDAO());
// 全局會話超時時間(單位毫秒),默認30分鐘 暫時設置爲10秒鐘 用來測試
sessionManager.setGlobalSessionTimeout(1800000);//單位毫秒
// 是否開啓刪除無效的session對象 默認爲true
sessionManager.setDeleteInvalidSessions(true);
// 是否開啓定時調度器進行檢測過期session 默認爲true
sessionManager.setSessionValidationSchedulerEnabled(true);
// 設置session失效的掃描時間, 清理用戶直接關閉瀏覽器造成的孤立會話 默認爲 1個小時
// 設置該屬性 就不需要設置 ExecutorServiceSessionValidationScheduler
// 底層也是默認自動調用ExecutorServiceSessionValidationScheduler
sessionManager.setSessionValidationInterval(3600000);//單位毫秒
// 取消url 後面的 JSESSIONID,設置爲false爲取消
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
- 將會話管理器交給 securityManager 管理
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
securityManager.setRememberMeManager(rememberMeManager());
securityManager.setCacheManager(myEhCacheManager());// 將緩存管理交給ehCache
securityManager.setSessionManager(sessionManager());//將session管理交給reids
return securityManager;
}
至此,shiro的session交給redis管理已經配置完成了,很簡單啊!
6、驗證猜想
- 6.1、在之前的源碼跟蹤中,我們猜想重寫一個類繼承 AbstractSessionDAO* ,替換 MemorySessionDAO ,在引入 shiro-redis jar包後,查看是用的redisSessionDAO關係圖,驗證猜想!
- 6.2、部分 RedisSessionDAO 源碼
@Override
protected Serializable doCreate(Session session) {
if (session == null) {
logger.error("session is null");
throw new UnknownSessionException("session is null");
}
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
this.saveSession(session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if (sessionId == null) {
logger.warn("session id is null");
return null;
}
Session s = getSessionFromThreadLocal(sessionId);
if (s != null) {
return s;
}
logger.debug("read session from redis");
try {
s = (Session) valueSerializer.deserialize(redisManager.get(keySerializer.serialize(getRedisSessionKey(sessionId))));
setSessionToThreadLocal(sessionId, s);
} catch (SerializationException e) {
logger.error("read session error. settionId=" + sessionId);
}
return s;
}
private void setSessionToThreadLocal(Serializable sessionId, Session s) {
Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get();
if (sessionMap == null) {
sessionMap = new HashMap<Serializable, SessionInMemory>();
sessionsInThread.set(sessionMap);
}
SessionInMemory sessionInMemory = new SessionInMemory();
sessionInMemory.setCreateTime(new Date());
sessionInMemory.setSession(s);
sessionMap.put(sessionId, sessionInMemory);
}
- 6.3 redis集羣
在使用shiro-redis工具包的時候,驚訝的發現了它已經爲我們提供了redis集羣的支持,後續會進行測試。
7、測試
- 打開登錄頁面,查看cookie是否被修改(已修改爲我們設置的名稱)
- session監聽器控制檯打印人數
3.redis可視化工具(成功將session寫入redis中)
8、源碼
- 需要源碼可以點擊這裏獲取!