Shiro权限管理框架(四):深入分析Shiro中的Session管理
其实关于Shiro的一些学习笔记很早就该写了,因为懒癌和拖延症晚期一直没有落实,直到今天公司的一个项目碰到了在集群环境的单点登录频繁掉线的问题,为了解决这个问题,Shiro相关的文档和教程没少翻。最后问题解决了,但我觉得我也是时候来做一波Shiro学习笔记了。
本篇是Shiro系列第四篇,Shiro中的过滤器初始化流程和实现原理。Shiro基于URL的权限控制是通过Filter实现的,本篇从我们注入的ShiroFilterFactoryBean开始入手,翻看源码追寻Shiro中的过滤器的实现原理。
首发地址:https://www.guitu18.com/post/2019/08/08/45.html
Session

SessionManager

我们在配置Shiro时配置了一个DefaultWebSecurityManager,先来看下

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
public class ServletContainerSessionManager implements WebSessionManager {
public Session getSession(SessionKey key) throws SessionException {
if (!WebUtils.isHttp(key)) {
String msg = "SessionKey must be an HTTP compatible implementation.";
throw new IllegalArgumentException(msg);
}
HttpServletRequest request = WebUtils.getHttpRequest(key);
Session session = null;
HttpSession httpSession = request.getSession(false);
if (httpSession != null) {
session = createSession(httpSession, request.getRemoteHost());
}
return session;
}
private String getHost(SessionContext context) {
String host = context.getHost();
if (host == null) {
ServletRequest request = WebUtils.getRequest(context);
if (request != null) {
host = request.getRemoteHost();
}
}
return host;
}
protected Session createSession(SessionContext sessionContext) throws AuthorizationException {
if (!WebUtils.isHttp(sessionContext)) {
String msg = "SessionContext must be an HTTP compatible implementation.";
throw new IllegalArgumentException(msg);
}
HttpServletRequest request = WebUtils.getHttpRequest(sessionContext);
HttpSession httpSession = request.getSession();
String host = getHost(sessionContext);
return createSession(httpSession, host);
}
protected Session createSession(HttpSession httpSession, String host) {
return new HttpServletSession(httpSession, host);
}
}
ServletContainerSessionManager本身并不管理会话,它最终操作的还是HttpSession,所以只能在Servlet容器中起作用,它不能支持除使用HTTP协议的之外的任何会话。
所以一般我们配置Shiro都会配置一个DefaultWebSessionManager,它继承了DefaultSessionManager,看看DefaultSessionManager的构造方法:
public DefaultSessionManager() {
this.deleteInvalidSessions = true;
this.sessionFactory = new SimpleSessionFactory();
this.sessionDAO = new MemorySessionDAO();
}
这里的sessionDAO初始化了一个MemorySessionDAO,它其实就是一个Map,在内存中通过键值对管理Session。
public MemorySessionDAO() {
this.sessions = new ConcurrentHashMap<Serializable, Session>();
}
HttpServletSession
public class HttpServletSession implements Session {
public HttpServletSession(HttpSession httpSession, String host) {
if (httpSession == null) {
String msg = "HttpSession constructor argument cannot be null.";
throw new IllegalArgumentException(msg);
}
if (httpSession instanceof ShiroHttpSession) {
String msg = "HttpSession constructor argument cannot be an instance of ShiroHttpSession. This " +
"is enforced to prevent circular dependencies and infinite loops.";
throw new IllegalArgumentException(msg);
}
this.httpSession = httpSession;
if (StringUtils.hasText(host)) {
setHost(host);
}
}
protected void setHost(String host) {
setAttribute(HOST_SESSION_KEY, host);
}
public void setAttribute(Object key, Object value) throws InvalidSessionException {
try {
httpSession.setAttribute(assertString(key), value);
} catch (Exception e) {
throw new InvalidSessionException(e);
}
}
}
Shiro的HttpServletSession只是对javax.servlet.http.HttpSession进行了简单的封装,所以在Web应用中对Session的相关操作最终都是对javax.servlet.http.HttpSession进行的,比如上面代码中的setHost()是将内容以键值对的形式保存在httpSession中。
先来看下这张图:

先了解上面这几个类的关系和作用,然后我们想管理Shiro中的一些数据就非常方便了。
SessionDao是Session管理的顶层接口,定义了Session的增删改查相关方法。
public interface SessionDAO {
Serializable create(Session session);
Session readSession(Serializable sessionId) throws UnknownSessionException;
void update(Session session) throws UnknownSessionException;
void delete(Session session);
Collection<Session> getActiveSessions();
}
AbstractSessionDao是一个抽象类,在它的构造方法中定义了JavaUuidSessionIdGenerator作为SessionIdGenerator用于生成SessionId。它虽然实现了create()和readSession()两个方法,但具体的流程调用的是它的两个抽象方法doCreate()和doReadSession(),需要它的子类去干活。
public abstract class AbstractSessionDAO implements SessionDAO {
private SessionIdGenerator sessionIdGenerator;
public AbstractSessionDAO() {
this.sessionIdGenerator = new JavaUuidSessionIdGenerator();
}
public Serializable create(Session session) {
Serializable sessionId = doCreate(session);
verifySessionId(sessionId);
return sessionId;
}
protected abstract Serializable doCreate(Session session);
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;
}
protected abstract Session doReadSession(Serializable sessionId);
}
看上面那张类图AbstractSessionDao的子类有三个,查看源码发现CachingSessionDAO是一个抽象类,它并没有实现这两个方法。在它的子类EnterpriseCacheSessionDAO中实现了doCreate()和doReadSession(),但doReadSession()是一个空实现直接返回null。
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>());
}
});
}
EnterpriseCacheSessionDAO依赖于它的父级CachingSessionDAO,在他的构造方法中向父类注入了一个AbstractCacheManager的匿名实现,它是一个基于内存的SessionDao,它所创建的MapCache就是一个Map。
我们在Shiro配置类里通过 sessionManager.setSessionDAO(new EnterpriseCacheSessionDAO()); 来使用它。然后在CachingSessionDAO.getCachedSession() 打个断点测试一下,可以看到cache就是一个ConcurrentHashMap,在内存中以Key-Value的形式保存着JSESSIONID和Session的映射关系。

再来看AbstractSessionDao的第三个实现MemorySessionDAO,它就是一个基于内存的SessionDao,简单直接,构造方法直接new了一个ConcurrentHashMap。
public MemorySessionDAO() {
this.sessions = new ConcurrentHashMap<Serializable, Session>();
}
那么它和EnterpriseCacheSessionDAO有啥区别,其实EnterpriseCacheSessionDAO只是CachingSessionDAO的一个默认实现,在CachingSessionDAO中cacheManager是没有默认值的,在EnterpriseCacheSessionDAO的构造方法将其初始化为一个ConcurrentHashMap。
如果我们直接用EnterpriseCacheSessionDAO其实和MemorySessionDAO其实没有什么区别,都是基于Map的内存型SessionDao。而CachingSessionDAO的目的是为了方便扩展的,用户可以继承CachingSessionDAO并注入自己的Cache实现,比如以Redis缓存Session的RedisCache。
其实在业务上如果需要Redis来管理Session,那么直接继承AbstractSessionDao更好,有Redis支撑中间的Cache就是多余的,这样还可以做分布式或集群环境的Session共享(分布式或集群环境如果中间还有一层Cache那么还要考虑同步问题,所以集群环境千万不要继承EnterpriseCacheSessionDAO来实现Session共享)。
回到开篇提到的我们公司项目集群环境下单点登录频繁掉线的问题,其实就是中间那层Cache造成的。我们的业务代码中RedisSessionDao是继承自EnterpriseCacheSessionDAO的,这样一来那在Redis之上还有一个基于内存的Cache层。此时用户的Session如果发生变更,虽然Redis中的Session是同步的,Cache层没有同步,导致的现象就是用户在一台服务器的Session是有效的,另一台服务器Cache中的Session还是旧的,然后用户就被迫下线了。
最后,关于Shiro中Session管理(持久化/共享)的一些总结:
如果只是在单机环境下需要做Session持久化(服务器重启保持Session在线),那么最好继承EnterpriseCacheSessionDAO来增加本地Session缓存以减少I/O开销,否则大量的
doReadSession()调用会造成I/O甚至网络压力(单机环境下能省一点是一点嘛,集群就没办法省了);如果是在集群环境下做Session共享,千万不要继承EnterpriseCacheSessionDAO,会产生服务器间的本地Session缓存不同步问题,直接继承AbstractSessionDAO即可;
关于集群环境下Session共享详细配置可以看这篇:Shiro权限管理框架(二):Shiro结合Redis实现分布式或集群环境下的Session共享
Shiro权限管理框架(四):深入分析Shiro中的Session管理的更多相关文章
- Shiro权限管理框架(三):Shiro中权限过滤器的初始化流程和实现原理
本篇是Shiro系列第三篇,Shiro中的过滤器初始化流程和实现原理.Shiro基于URL的权限控制是通过Filter实现的,本篇从我们注入的ShiroFilterFactoryBean开始入手,翻看 ...
- Shiro权限管理框架(一):Shiro的基本使用
首发地址:https://www.guitu18.com/post/2019/07/26/43.html 核心概念 Apache Shiro是一个强大且易用的Java安全框架,执行身份验证.授权.密码 ...
- Shiro权限管理框架(二):Shiro结合Redis实现分布式环境下的Session共享
首发地址:https://www.guitu18.com/post/2019/07/28/44.html 本篇是Shiro系列第二篇,使用Shiro基于Redis实现分布式环境下的Session共享. ...
- Shiro权限管理框架(五):自定义Filter实现及其问题排查记录
明确需求 在使用Shiro的时候,鉴权失败一般都是返回一个错误页或者登录页给前端,特别是后台系统,这种模式用的特别多.但是现在的项目越来越多的趋向于使用前后端分离的方式开发,这时候就需要响应Json数 ...
- Shiro权限管理框架
一.Shiro介绍 Apache Shiro 是Java 的一个安全框架.Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在JavaEE 环境.Shiro 可以 ...
- Shiro权限管理框架详解
1 权限管理1.1 什么是权限管理 基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被 ...
- shiro权限管理框架与springmvc整合
shiro是apache下的一个项目,和spring security类似,用于用户权限的管理‘ 但从易用性和学习成本上考虑,shiro更具优势,同时shiro支持和很多接口集成 用户及权限管理是众多 ...
- Apache Shiro权限框架在SpringMVC+Hibernate中的应用
在做网站开发中,用户权限必须要考虑的,权限这个东西很重要,它规定了用户在使用中能进行哪 些操作,和不能进行哪些操作:我们完全可以使用过滤器来进行权限的操作,但是有了权限框架之后,使用起来会非常的方便, ...
- Shiro 权限管理框架
一.什么是Shiro Apache Shiro是一个强大易用的java安全框架,提供认证.授权.加密和会话管理等功能 · 认证:用户身份识别,俗称“登录”: · 授权:访问控制 · 密码加密:保护或隐 ...
随机推荐
- [vue] vue服务端渲染nuxt.js
初始化 使用脚手架工具 create-nuxt-app 快速创建 npx create-nuxt-app <项目名> npx create-nuxt-app 执行一些选择 在集成的服务器端 ...
- 欢迎加入强哥的 Android 开发交流群
最近建了一个 Android 开发交流群,但不限于交流移动端.前端和后端等相关技术. 本群的宗旨:让所有学习的群友都有进步的机会. 1. 经验交流 在我们学习时遇到困境,或者开发过程中遇到难题,都可以 ...
- SpringBoot定时任务,总有一款适合你
title: SpringBoot定时任务,总有一款适合你 date: 2019-09-28 16:19:10 tags: - springboot - 定时任务 categories: java - ...
- CentOS 8 正式发布
转载请注明:文章转载自 OSCHINA 社区 [http://www.oschina.net] 本文地址:https://www.oschina.net/news/110111/centos-8-re ...
- Servlet与Tomcat运行示例
Servlet与Tomcat运行示例 本文将写一个servlet,然后将其部署到Tomcat的全过程.本文参考<深入拆解Tomcat_Jetty>内容. 一.基于web.xml开发步骤 下 ...
- ajax跨域问题以及解决方案
转:https://blog.csdn.net/csdn_ds/article/category/6937392/3 在工作中,大家应该都遇到过ajax跨域问题,浏览器的错误如下: XMLHttpRe ...
- idea 环境变量设置编码
1.打开Run/Debug Configuration,选择你的tomcat 2.然后在 Server > VM options 设置为 -Dfile.encoding=UTF-8
- 一台机器上搭建多个redis实例的配置文件修改部分
1.单个redis服务搭建请参考:redis服务搭建 2.一台Redis服务器,分成多个节点,每个节点分配一个端口(6380,6381…),默认端口是6379. 每个节点对应一个Redis配置文件,如 ...
- Zookeeper学习笔记之 Zab协议(Zookeeper Atomic Broadcast)
Zab协议(Zookeeper Atomic Broadcast): 广播模式: Leader将所有更新(称为proposal),顺序发送给Follower 当Leader收到半数以上的Followe ...
- Nginx internal 指令限制访问图片资源文件
Nginx 的 internal 指令可以用来限制 Web 公共目录下的图片等资源文件被任意用户直接访问.一个明显的使用场景是,对于用户上传的认证图片,属于个人隐私资源,不应该让所有用户都能访问得到, ...