开心一刻html
老师对小明说:"乳就是小的意思,好比乳猪就是小猪,乳名就是小名,请你用乳字造个句"
小明:"我家很穷,只能住在40平米的乳房"
老师:"..., 这个不行,换一个"
小明:"我天天上学都要跳过我家门口的一条乳沟"
老师:"......, 这个也不行,再换一个"
小明:"老师,我想不出来了,把个人乳头都想破了!"java
路漫漫其修远兮,吾将上下而求索!git
github:https://github.com/youzhibinggithub
码云(gitee):https://gitee.com/youzhibingredis
shiro的session建立与session的查询、更新、过时、删除中,shiro对session的操做基本都讲到了,但还缺一个session共享没有讲解;session共享的原理其实在自定义session管理一文已经讲过了,本文不讲原理,只看看shiro的session共享的实现。spring
若是是单机应用,那么谈不上session共享,session放哪都无所谓,不在意放到默认的servlet容器中,仍是抽出来放到单独的地方;数据库
也就是说session共享是针对集群(或分布式、或分布式集群)的;若是不作session共享,仍然采用默认的方式(session存放到默认的servlet容器),当咱们的应用是以集群的方式发布的时候,同个用户的请求会被分发到不一样的集群节点(分发依赖具体的负载均衡规则),那么每一个处理同个用户请求的节点都会从新生成该用户的session,这些session之间是毫无关联的。那么同个用户的请求会被当成多个不一样用户的请求,这确定是不行的。apache
实现方式其实有不少,甚至能够不作session共享,具体有哪些,你们自行去查资料。本文提供一种方式:redis实现session共享,就是将session从servlet容器抽出来,放到redis中存储,全部集群节点都从redis中对session进行操做。后端
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共享实际上是比较简单的,重写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实例便可
具体代码请参考spring-boot-shiro
底层仍是利用Filter + HttpServletRequestWrapper将对session的操做接入到本身的实现中来,而不走默认的servlet容器,这样对session的操做彻底由咱们本身掌握。
shiro的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的流程须要你们自行去跟,或者阅读我以前的博文
一、当以集群方式对外提供服务的时候,不作session共享也是能够的
能够经过ip_hash的机制将同个ip的请求定向到同一台后端,这样保证用户的请求始终是同一台服务处理,与单机应用基本一致了;但这有不少方面的缺陷(具体就不详说了),不推荐使用。
二、servlet容器之间作session同步也是能够实现session共享的
一个servlet容器生成session,其余节点的servlet容器今后servlet容器进行session同步,以达到session信息一致。这个也不推荐,某个时间点会有session不一致的问题,毕竟同步过程受到各方面的影响,不能保证session实时一致。
三、session共享实现的原理其实都是同样的,都是filter + HttpServletRequestWrapper,只是实现细节会有所区别;有兴趣的能够看下spring-session的实现细节。
四、若是咱们采用的spring集成shiro,其实能够将缓存管理器交由spring管理,至关于由spring统一管理缓存。
五、shiro的CacheManager不仅是管理session缓存,还管理着身份认证缓存、受权缓存,shiro的缓存都是CacheManager管理。可是身份认证缓存默认是关闭的,我的也不推荐开启。
六、shiro的session建立时机是在登陆认证以前,而不是第一次调用getSession()时。
《跟我学shiro》