shiro源码篇 - shiro的session共享,你值得拥有
前言
开心一刻
老师对小明说:"乳就是小的意思,比如乳猪就是小猪,乳名就是小名,请你用乳字造个句"
小明:"我家很穷,只能住在40平米的乳房"
老师:"..., 这个不行,换一个"
小明:"我每天上学都要跳过我家门口的一条乳沟"
老师:"......, 这个也不行,再换一个"
小明:"老师,我想不出来了,把我的乳头都想破了!"
路漫漫其修远兮,吾将上下而求索!
github:
码云(gitee):
前情回顾
与中,shiro对session的操作基本都讲到了,但还缺一个session共享没有讲解;session共享的原理其实在一文已经讲过了,本文不讲原理,只看看shiro的session共享的实现。
为何需要session共享
如果是单机应用,那么谈不上session共享,session放哪都无所谓,不在乎放到默认的servlet容器中,还是抽出来放到单独的地方;
也就是说session共享是针对集群(或分布式、或分布式集群)的;如果不做session共享,仍然采用默认的方式(session存放到默认的servlet容器),当我们的应用是以集群的方式发布的时候,同个用户的请求会被分发到不同的集群节点(分发依赖具体的负载均衡规则),那么每个处理同个用户请求的节点都会重新生成该用户的session,这些session之间是毫无关联的。那么同个用户的请求会被当成多个不同用户的请求,这肯定是不行的。
如何实现session共享
实现方式其实有很多,甚至可以不做session共享,具体有哪些,大家自行去查资料。本文提供一种方式:redis实现session共享,就是将session从servlet容器抽出来,放到redis中存储,所有集群节点都从redis中对session进行操作。
sessiondao
sessiondao其实是用于session持久化的,但里面有缓存部分,具体细节我们往下看
shiro已有sessiondao的实现如下
sessiondao接口提供的方法如下
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.session.session; import org.apache.shiro.session.unknownsessionexception; import java.io.serializable; import java.util.collection; /** * 从eis操作session的规范(eis:例如关系型数据库, 文件系统, 持久化缓存等等, 具体依赖dao实现) * 提供了典型的crud的方法:create, readsession, update, delete */ public interface sessiondao { /** * 插入一个新的sesion记录到eis */ serializable create(session session); /** * 根据会话id获取会话 */ session readsession(serializable sessionid) throws unknownsessionexception; /** * 更新session; 如更新session最后访问时间/停止会话/设置超时时间/设置移除属性等会调用 */ void update(session session) throws unknownsessionexception; /** * 删除session; 当会话过期/会话停止(如用户退出时)会调用 */ void delete(session session); /** * 获取当前所有活跃session, 所有状态不是stopped/expired的session * 如果用户量多此方法影响性能 */ collection<session> getactivesessions(); }
sessiondao给出了从持久层(一般而言是关系型数据库)操作session的标准。
abstractsessiondao提供了sessiondao的基本实现,如下
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.session.session; import org.apache.shiro.session.unknownsessionexception; import org.apache.shiro.session.mgt.simplesession; import java.io.serializable; /** * sessiondao的抽象实现, 在会话创建和读取时做一些健全性检查,并在需要时允许可插入的会话id生成策略. * sessiondao的update和delete则留给子类来实现 * eis需要子类自己实现 */ public abstract class abstractsessiondao implements sessiondao { /** * sessionid生成器 */ private sessionidgenerator sessionidgenerator; public abstractsessiondao() { this.sessionidgenerator = new javauuidsessionidgenerator(); // 指定javauuidsessionidgenerator为默认sessionid生成器 } /** * 获取sessionid生成器 */ public sessionidgenerator getsessionidgenerator() { return sessionidgenerator; } /** * 设置sessionid生成器 */ public void setsessionidgenerator(sessionidgenerator sessionidgenerator) { this.sessionidgenerator = sessionidgenerator; } /** * 生成一个新的sessionid, 并将它应用到session实例 */ protected serializable generatesessionid(session session) { if (this.sessionidgenerator == null) { string msg = "sessionidgenerator attribute has not been configured."; throw new illegalstateexception(msg); } return this.sessionidgenerator.generateid(session); } /** * sessiondao中create实现; 将创建的sesion保存到eis. * 子类docreate方法的代理,具体的细节委托给了子类的docreate方法 */ public serializable create(session session) { serializable sessionid = docreate(session); verifysessionid(sessionid); return sessionid; } /** * 保证从docreate返回的sessionid不是null,并且不是已经存在的. * 目前只实现了null校验,是否已存在是没有校验的,可能shiro的开发者会在后续补上吧. */ private void verifysessionid(serializable sessionid) { if (sessionid == null) { string msg = "sessionid returned from docreate implementation is null. please verify the implementation."; throw new illegalstateexception(msg); } } /** * 分配sessionid给session实例 */ protected void assignsessionid(session session, serializable sessionid) { ((simplesession) session).setid(sessionid); } /** * 子类通过实现此方法来持久化session实例到eis. */ protected abstract serializable docreate(session session); /** * sessiondao中readsession实现; 通过sessionid从eis获取session对象. * 子类doreadsession方法的代理,具体的获取细节委托给了子类的doreadsession方法. */ public session readsession(serializable sessionid) throws unknownsessionexception { session s = doreadsession(sessionid); if (s == null) { throw new unknownsessionexception("there is no session with id [" + sessionid + "]"); } return s; } /** * 子类通过实现此方法从eis获取session实例 */ protected abstract session doreadsession(serializable sessionid); }
sessiondao的基本实现,实现了sessiondao的create、readsession(具体还是依赖abstractsessiondao子类的docreate、doreadsession实现);同时加入了自己的sessionid生成器,负责sessionid的操作。
cachingsessiondao提供了session缓存的功能,如下
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.cache.cache; import org.apache.shiro.cache.cachemanager; import org.apache.shiro.cache.cachemanageraware; import org.apache.shiro.session.session; import org.apache.shiro.session.unknownsessionexception; import org.apache.shiro.session.mgt.validatingsession; import java.io.serializable; import java.util.collection; import java.util.collections; /** * 应用层与持久层(eis,如关系型数据库、文件系统、nosql)之间的缓存层实现 * 缓存着所有激活状态的session * 实现了cachemanageraware,会在shiro加载的过程中调用此对象的setcachemanager方法 */ public abstract class cachingsessiondao extends abstractsessiondao implements cachemanageraware { /** * 激活状态的sesion的默认缓存名 */ public static final string active_session_cache_name = "shiro-activesessioncache"; /** * 缓存管理器,用来获取session缓存 */ private cachemanager cachemanager; /** * 用来缓存session的缓存实例 */ private cache<serializable, session> activesessions; /** * session缓存名, 默认是active_session_cache_name. */ private string activesessionscachename = active_session_cache_name; public cachingsessiondao() { } /** * 设置缓存管理器 */ public void setcachemanager(cachemanager cachemanager) { this.cachemanager = cachemanager; } /** * 获取缓存管理器 */ public cachemanager getcachemanager() { return cachemanager; } /** * 获取缓存实例的名称,也就是获取activesessionscachename的值 */ public string getactivesessionscachename() { return activesessionscachename; } /** * 设置缓存实例的名称,也就是设置activesessionscachename的值 */ public void setactivesessionscachename(string activesessionscachename) { this.activesessionscachename = activesessionscachename; } /** * 获取缓存实例 */ public cache<serializable, session> getactivesessionscache() { return this.activesessions; } /** * 设置缓存实例 */ public void setactivesessionscache(cache<serializable, session> cache) { this.activesessions = cache; } /** * 获取缓存实例 * 注意:不会返回non-null值 * * @return the active sessions cache instance. */ private cache<serializable, session> getactivesessionscachelazy() { if (this.activesessions == null) { this.activesessions = createactivesessionscache(); } return activesessions; } /** * 创建缓存实例 */ protected cache<serializable, session> createactivesessionscache() { cache<serializable, session> cache = null; cachemanager mgr = getcachemanager(); if (mgr != null) { string name = getactivesessionscachename(); cache = mgr.getcache(name); } return cache; } /** * abstractsessiondao中create的重写 * 调用父类(abstractsessiondao)的create方法, 然后将session缓存起来 * 返回sessionid */ public serializable create(session session) { serializable sessionid = super.create(session); // 调用父类的create方法 cache(session, sessionid); // 以sessionid作为key缓存session return sessionid; } /** * 从缓存中获取session; 若sessionid为null,则返回null */ protected session getcachedsession(serializable sessionid) { session cached = null; if (sessionid != null) { cache<serializable, session> cache = getactivesessionscachelazy(); if (cache != null) { cached = getcachedsession(sessionid, cache); } } return cached; } /** * 从缓存中获取session */ protected session getcachedsession(serializable sessionid, cache<serializable, session> cache) { return cache.get(sessionid); } /** * 缓存session,以sessionid作为key */ protected void cache(session session, serializable sessionid) { if (session == null || sessionid == null) { return; } cache<serializable, session> cache = getactivesessionscachelazy(); if (cache == null) { return; } cache(session, sessionid, cache); } protected void cache(session session, serializable sessionid, cache<serializable, session> cache) { cache.put(sessionid, session); } /** * abstractsessiondao中readsession的重写 * 先从缓存中获取,若没有则调用父类的readsession方法获取session */ public session readsession(serializable sessionid) throws unknownsessionexception { session s = getcachedsession(sessionid); // 从缓存中获取 if (s == null) { s = super.readsession(sessionid); // 调用父类readsession方法获取 } return s; } /** * sessiondao中update的实现 * 更新session的状态 */ public void update(session session) throws unknownsessionexception { doupdate(session); // 更新eis中的session if (session instanceof validatingsession) { if (((validatingsession) session).isvalid()) { cache(session, session.getid()); // 更新缓存中的session } else { uncache(session); // 移除缓存中的sesson } } else { cache(session, session.getid()); } } /** * 由子类去实现,持久化session到eis */ protected abstract void doupdate(session session); /** * sessiondao中delete的实现 * 删除session */ public void delete(session session) { uncache(session); // 从缓存中移除 dodelete(session); // 从eis中删除 } /** * 由子类去实现,从eis中删除session */ protected abstract void dodelete(session session); /** * 从缓存中移除指定的session */ protected void uncache(session session) { if (session == null) { return; } serializable id = session.getid(); if (id == null) { return; } cache<serializable, session> cache = getactivesessionscachelazy(); if (cache != null) { cache.remove(id); } } /** * sessiondao中getactivesessions的实现 * 获取所有的存活的session */ public collection<session> getactivesessions() { cache<serializable, session> cache = getactivesessionscachelazy(); if (cache != null) { return cache.values(); } else { return collections.emptyset(); } } }
是应用层与持久化层之间的缓存层,不用频繁请求持久化层以提升效率。重写了abstractsessiondao中的create、readsession方法,实现了sessiondao中的update、delete、getactivesessions方法,预留doupdate和dodelele给子类去实现(doxxx方法操作的是持久层)
memorysessiondao,sessiondao的简单内存实现,如下
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.session.session; import org.apache.shiro.session.unknownsessionexception; import org.apache.shiro.util.collectionutils; import org.slf4j.logger; import org.slf4j.loggerfactory; import java.io.serializable; import java.util.collection; import java.util.collections; import java.util.concurrent.concurrenthashmap; import java.util.concurrent.concurrentmap; /** * 基于内存的sessiondao的简单实现,所有的session存在concurrentmap中 * defaultsessionmanager默认用的memorysessiondao */ public class memorysessiondao extends abstractsessiondao { private static final logger log = loggerfactory.getlogger(memorysessiondao.class); private concurrentmap<serializable, session> sessions; // 存放session的容器 public memorysessiondao() { this.sessions = new concurrenthashmap<serializable, session>(); } // abstractsessiondao 中docreate的重写; 将session存入sessions protected serializable docreate(session session) { serializable sessionid = generatesessionid(session); // 生成sessionid assignsessionid(session, sessionid); // 将sessionid赋值到session storesession(sessionid, session); // 存储session到sessions return sessionid; } // 存储session到sessions protected session storesession(serializable id, session session) { if (id == null) { throw new nullpointerexception("id argument cannot be null."); } return sessions.putifabsent(id, session); } // abstractsessiondao 中doreadsession的重写; 从sessions中获取session protected session doreadsession(serializable sessionid) { return sessions.get(sessionid); } // sessiondao中update的实现; 更新sessions中指定的session public void update(session session) throws unknownsessionexception { storesession(session.getid(), session); } // sessiondao中delete的实现; 从sessions中移除指定的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); } } // sessiondao中sessiondao中delete的实现的实现; 获取sessions中全部session public collection<session> sessiondao中delete的实现() { collection<session> values = sessions.values(); if (collectionutils.isempty(values)) { return collections.emptyset(); } else { return collections.unmodifiablecollection(values); } } }
将session保存在内存中,存储结构是concurrenthashmap;项目中基本不用,即使我们不实现自己的sessiondao,一般用的也是enterprisecachesessiondao。
enterprisecachesessiondao,提供了缓存功能的session维护,如下
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.cache.abstractcachemanager; import org.apache.shiro.cache.cache; import org.apache.shiro.cache.cacheexception; import org.apache.shiro.cache.mapcache; import org.apache.shiro.session.session; import java.io.serializable; import java.util.concurrent.concurrenthashmap; public class enterprisecachesessiondao extends cachingsessiondao { public enterprisecachesessiondao() { // 设置默认缓存器,并实例化mapcache作为cache实例 setcachemanager(new abstractcachemanager() { @override protected cache<serializable, session> createcache(string name) throws cacheexception { return new mapcache<serializable, session>(name, new concurrenthashmap<serializable, session>()); } }); } // abstractsessiondao中docreate的重写; protected serializable docreate(session session) { serializable sessionid = generatesessionid(session); assignsessionid(session, sessionid); return sessionid; } // abstractsessiondao中doreadsession的重写 protected session doreadsession(serializable sessionid) { return null; //should never execute because this implementation relies on parent class to access cache, which //is where all sessions reside - it is the cache implementation that determines if the //cache is memory only or disk-persistent, etc. } // cachingsessiondao中doupdate的重写 protected void doupdate(session session) { //does nothing - parent class persists to cache. } // cachingsessiondao中dodelete的重写 protected void dodelete(session session) { //does nothing - parent class removes from cache. } }
设置了默认的缓存管理器(abstractcachemanager)和默认的缓存实例(mapcache),实现了缓存效果。从父类继承的持久化操作方法(doxxx)都是空实现,也就说enterprisecachesessiondao是没有实现持久化操作的,仅仅只是简单的提供了缓存实现。当然我们可以继承enterprisecachesessiondao,重写doxxx方法来实现持久化操作。
总结下:sessiondao定义了从持久层操作session的标准;abstractsessiondao提供了sessiondao的基础实现,如生成会话id等;cachingsessiondao提供了对开发者透明的session缓存的功能,只需要设置相应的 cachemanager 即可;memorysessiondao直接在内存中进行session维护;而enterprisecachesessiondao提供了缓存功能的session维护,默认情况下使用 mapcache 实现,内部使用concurrenthashmap保存缓存的会话。因为shiro不知道我们需要将session持久化到哪里(关系型数据库,还是文件系统),所以只提供了memorysessiondao持久化到内存(听起来怪怪的,内存中能说成持久层吗)
shiro session共享
共享实现
shiro的session共享其实是比较简单的,重写cachemanager,将其操作指向我们的redis,然后实现我们自己的cachingsessiondao定制缓存操作和缓存持久化。
自定义cachemanager
shirorediscachemanager
package com.lee.shiro.config; import org.apache.shiro.cache.cache; import org.apache.shiro.cache.cacheexception; import org.apache.shiro.cache.cachemanager; import org.springframework.beans.factory.annotation.autowired; import org.springframework.stereotype.component; @component public class shirorediscachemanager implements cachemanager { @autowired private cache shirorediscache; @override public <k, v> cache<k, v> getcache(string s) throws cacheexception { return shirorediscache; } }
shirorediscache
package com.lee.shiro.config; import org.apache.shiro.cache.cache; import org.apache.shiro.cache.cacheexception; import org.springframework.beans.factory.annotation.autowired; import org.springframework.beans.factory.annotation.value; import org.springframework.data.redis.core.redistemplate; import org.springframework.stereotype.component; import java.util.collection; import java.util.set; import java.util.concurrent.timeunit; @component public class shirorediscache<k,v> implements cache<k,v>{ @autowired private redistemplate<k,v> redistemplate; @value("${spring.redis.expiretime}") private long expiretime; @override public v get(k k) throws cacheexception { return redistemplate.opsforvalue().get(k); } @override public v put(k k, v v) throws cacheexception { redistemplate.opsforvalue().set(k,v,expiretime, timeunit.seconds); return null; } @override public v remove(k k) throws cacheexception { v v = redistemplate.opsforvalue().get(k); redistemplate.opsforvalue().getoperations().delete(k); return v; } @override public void clear() throws cacheexception { } @override public int size() { return 0; } @override public set<k> keys() { return null; } @override public collection<v> values() { return null; } }
自定义cachingsessiondao
继承enterprisecachesessiondao,然后重新设置其cachemanager(替换掉默认的内存缓存器),这样也可以实现我们的自定义cachingsessiondao,但是这是优选吗;如若我们实现持久化,继承enterprisecachesessiondao是优选,但如果只是实现session缓存,那么cachingsessiondao是优选,自定义更灵活。那么我们还是继承cachingsessiondao来实现我们的自定义cachingsessiondao
shirosessiondao
package com.lee.shiro.config; import org.apache.shiro.session.session; import org.apache.shiro.session.mgt.eis.cachingsessiondao; import org.springframework.stereotype.component; import java.io.serializable; @component public class shirosessiondao extends cachingsessiondao { @override protected void doupdate(session session) { } @override protected void dodelete(session session) { } @override protected serializable docreate(session session) { // 这里绑定sessionid到session,必须要有 serializable sessionid = generatesessionid(session); assignsessionid(session, sessionid); return sessionid; } @override protected session doreadsession(serializable sessionid) { return null; } }
最后将shirosessiondao实例赋值给sessionmanager实例,再讲sessionmanager实例赋值给securitymanager实例即可
具体代码请参考
源码解析
底层还是利用filter + httpservletrequestwrapper将对session的操作接入到自己的实现中来,而不走默认的servlet容器,这样对session的操作完全由我们自己掌握。
中其实讲到了shiro中对session操作的基本流程,这里不再赘述,没看的朋友可以先去看看再回过头来看这篇。本文只讲shiro中,如何将一个请求的session接入到自己的实现中来的;shiro中有很多默认的filter,我会单独开一篇来讲shiro的filter,这篇我们先不纠结这些filter。
onceperrequestfilter中dofilter方法如下
public final void dofilter(servletrequest request, servletresponse response, filterchain filterchain) throws servletexception, ioexception { string alreadyfilteredattributename = getalreadyfilteredattributename(); if ( request.getattribute(alreadyfilteredattributename) != null ) { // 当前filter已经执行过了,进行下一个filter log.trace("filter '{}' already executed. proceeding without invoking this filter.", getname()); filterchain.dofilter(request, response); } else //noinspection deprecation if (/* added in 1.2: */ !isenabled(request, response) || /* retain backwards compatibility: */ shouldnotfilter(request) ) { // 当前filter未被启用或忽略此filter,则进行下一个filter;shouldnotfilter已经被废弃了 log.debug("filter '{}' is not enabled for the current request. proceeding without invoking this filter.", getname()); filterchain.dofilter(request, response); } else { // do invoke this filter... log.trace("filter '{}' not yet executed. executing now.", getname()); request.setattribute(alreadyfilteredattributename, boolean.true); try { // 执行当前filter dofilterinternal(request, response, filterchain); } finally { // 一旦请求完成,我们清除当前filter的"已经过滤"的状态 request.removeattribute(alreadyfilteredattributename); } } }
上图中,我可以看到abstractshirofilter的dofilterinternal放中将request封装成了shiro自定义的shirohttpservletrequest,将response也封装成了shiro自定义的shirohttpservletresponse。既然filter中将request封装了shirohttpservletrequest,那么到我们应用的request就是shirohttpservletrequest类型,也就是说我们对session的操作最终都是由shiro完成的,而不是默认的servlet容器。
另外补充一点,shiro的session创建不是懒创建的。servlet容器中的session创建是第一次请求session(第一调用request.getsession())时才创建。shiro的session创建如下图
此时,还没登录,但是subject、session已经创建了,只是subject的认证状态为false,说明还没进行登录认证的。至于session创建过程已经保存到redis的流程需要大家自行去跟,或者阅读我之前的博文
总结
1、当以集群方式对外提供服务的时候,不做session共享也是可以的
可以通过ip_hash的机制将同个ip的请求定向到同一台后端,这样保证用户的请求始终是同一台服务处理,与单机应用基本一致了;但这有很多方面的缺陷(具体就不详说了),不推荐使用。
2、servlet容器之间做session同步也是可以实现session共享的
一个servlet容器生成session,其他节点的servlet容器从此servlet容器进行session同步,以达到session信息一致。这个也不推荐,某个时间点会有session不一致的问题,毕竟同步过程受到各方面的影响,不能保证session实时一致。
3、session共享实现的原理其实都是一样的,都是filter + httpservletrequestwrapper,只是实现细节会有所区别;有兴趣的可以看下spring-session的实现细节。
4、如果我们采用的spring集成shiro,其实可以将缓存管理器交由spring管理,相当于由spring统一管理缓存。
5、shiro的cachemanager不只是管理session缓存,还管理着身份认证缓存、授权缓存,shiro的缓存都是cachemanager管理。但是身份认证缓存默认是关闭的,个人也不推荐开启。
6、shiro的session创建时机是在登录认证之前,而不是第一次调用getsession()时。
参考
《跟我学shiro》
上一篇: 写Bug时,需要注意的几点 02
下一篇: Redis数据备份和重启恢复
推荐阅读
-
shiro源码篇 - shiro的filter,你值得拥有
-
shiro源码篇 - shiro的session管理,你值得拥有
-
shiro源码篇 - shiro的session共享,你值得拥有
-
shiro源码篇 - shiro认证与授权,你值得拥有
-
shiro源码篇 - shiro的session的查询、刷新、过期与删除,你值得拥有
-
shiro源码篇 - shiro的session管理,你值得拥有
-
shiro源码篇 - shiro的session共享,你值得拥有
-
shiro源码篇 - shiro认证与授权,你值得拥有
-
shiro源码篇 - shiro的session的查询、刷新、过期与删除,你值得拥有