其实关于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管理的更多相关文章

  1. Shiro权限管理框架(三):Shiro中权限过滤器的初始化流程和实现原理

    本篇是Shiro系列第三篇,Shiro中的过滤器初始化流程和实现原理.Shiro基于URL的权限控制是通过Filter实现的,本篇从我们注入的ShiroFilterFactoryBean开始入手,翻看 ...

  2. Shiro权限管理框架(一):Shiro的基本使用

    首发地址:https://www.guitu18.com/post/2019/07/26/43.html 核心概念 Apache Shiro是一个强大且易用的Java安全框架,执行身份验证.授权.密码 ...

  3. Shiro权限管理框架(二):Shiro结合Redis实现分布式环境下的Session共享

    首发地址:https://www.guitu18.com/post/2019/07/28/44.html 本篇是Shiro系列第二篇,使用Shiro基于Redis实现分布式环境下的Session共享. ...

  4. Shiro权限管理框架(五):自定义Filter实现及其问题排查记录

    明确需求 在使用Shiro的时候,鉴权失败一般都是返回一个错误页或者登录页给前端,特别是后台系统,这种模式用的特别多.但是现在的项目越来越多的趋向于使用前后端分离的方式开发,这时候就需要响应Json数 ...

  5. Shiro权限管理框架

    一.Shiro介绍 Apache Shiro 是Java 的一个安全框架.Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在JavaEE 环境.Shiro 可以 ...

  6. Shiro权限管理框架详解

    1 权限管理1.1 什么是权限管理 基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被 ...

  7. shiro权限管理框架与springmvc整合

    shiro是apache下的一个项目,和spring security类似,用于用户权限的管理‘ 但从易用性和学习成本上考虑,shiro更具优势,同时shiro支持和很多接口集成 用户及权限管理是众多 ...

  8. Apache Shiro权限框架在SpringMVC+Hibernate中的应用

    在做网站开发中,用户权限必须要考虑的,权限这个东西很重要,它规定了用户在使用中能进行哪 些操作,和不能进行哪些操作:我们完全可以使用过滤器来进行权限的操作,但是有了权限框架之后,使用起来会非常的方便, ...

  9. Shiro 权限管理框架

    一.什么是Shiro Apache Shiro是一个强大易用的java安全框架,提供认证.授权.加密和会话管理等功能 · 认证:用户身份识别,俗称“登录”: · 授权:访问控制 · 密码加密:保护或隐 ...

随机推荐

  1. 新手学习Git之在本地使用Git

    每个开发人员应该都会一个版本管理工具,在Git和SVN中,我选择以Git,以下是我的一些心得 什么是 Git Git是目前世界上最先进的分布式版本控制系统(没有之一). 一.Git安装 1).linu ...

  2. 安装centos8

    一.     镜像下载 国内源下载镜像:(推荐) http://mirrors.aliyun.com/centos/8.0.1905/isos/x86_64/CentOS-8-x86_64-1905- ...

  3. playframe 项目搭建

  4. Python常用端口扫描

    from socket import * import sys host=sys.argv[1] service={':'HTTP', ':'SQL_Server', ':'Remote_Destop ...

  5. grep、正则表达式

    1.grep :文本搜索工具 -i:忽略大小写--color:匹配到的内容高亮显示-v:显示没有被模式匹配到的行-o:只显示被模式匹配到的字符串-n:显示匹配到行的行号-E:使用扩展正则表达式-A:后 ...

  6. Java面向对象程序设计第6章1-12

    1.this和super各有几种用法? this this 用法有三种,主要应用场合: 表示当前对象引用,常用于形参或局部变量与类的成员变 量同名的情形,使用this.成员名表示当前对象的成员 表示当 ...

  7. 自创ant-design-pro组件

    ant design蚂蚁金服基于react打造的一个服务于企业级产品的UI框架.而ant design pro呢?就是基于Ant Design这个框架搭建的中后台管理控制台的脚手架. 话不多说,今天给 ...

  8. C++set 和 multiset的使用

    最后一个自由支配的暑假,学一些自己感兴趣的部分,也算为大三作准备. C++中set集合的使用 定义一个int类型的集合 set<int> s: set<int>::iterat ...

  9. 小程序webview跳转页面后没有返回按钮完美解决方案

    随着小程序越来越火爆,使一个产品如果只有公众号H5页面和APP显得不怎么完美,总感觉不搭上小程序这趟流量车,就会少了点什么,心里别扭地很.在此驱动下,我所在公司也决定赶紧上车. 但是,如果要按照小程序 ...

  10. Solidity 编程实例--投票

    Voting 投票 思路是为每张选票创建一个合约,每个投票选项提供一个短名称.合约创建者作为会长将会给每个投票参与人各自的地址投票权. 地址后面的人们可以选择自己投票或者委托信任的代表人替他们投票.在 ...