Shiro session和Spring session一样吗?
出自:https://yq.aliyun.com/articles/114167?t=t1
1. 疑问
我们在项目中使用了spring mvc作为MVC框架,shiro作为权限控制框架,在使用过程中慢慢地产生了下面几个疑惑,本篇文章将会带着疑问慢慢地解析shiro源码,从而解开心里面的那点小纠纠。
(1)在spring controller中,request有何不同呢?
于是,在controller中打印了request的类对象,发现request对象是org.apache.shiro.web.servlet.ShiroHttpServletRequest ,很明显,此时的 request 已经被shiro包装过了。
(2)众所周知,spring mvc整合shiro后,可以通过两种方式获取到session:
通过Spring mvc中controller的request获取session
Session session = request.getSession();
通过shiro获取session
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
那么,问题来了,两种方式获取的session是否相同呢?
这里需要看一下项目中的shiro的securityManager配置,因为配置影响了shiro session的来源。这里没有配置session管理器。
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--<property name="sessionManager" ref="sessionManager"/>-->
<property name="realm" ref="shiroRealm"/>
</bean>
在controller中再次打印了session,发现前者的session类型是 org.apache.catalina.session.StandardSessionFacade ,后者的session类型是org.apache.shiro.subject.support.DelegatingSubject$StoppingAwareProxiedSession。
很明显,前者的session是属于httpServletRequest中的HttpSession,那么后者呢?仔细看StoppingAwareProxiedSession,它是属于shiro自定义的session的子类。通过这个代理对象的源码,我们发现所有与session相关的方法都是通过它内部委托类delegate进行的,通过断点,可以看到delegate的类型其实也是 org.apache.catalina.session.StandardSessionFacade ,也就是说,两者在操作session时,都是用同一个类型的session。那么它什么时候包装了httpSession呢?
2. 一起一层一层剥开它的芯
2.1 怎么获取过滤器filter
spring mvc 整合shiro,需要在web.xml中配置该filter
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 设置spring容器filter的bean id,如果不设置则找与filter-name一致的bean-->
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
DelegatingFilterProxy 是一个过滤器,准确来说是目的过滤器的代理,由它在doFilter方法中,获取spring 容器中的过滤器,并调用目标过滤器的doFilter方法,这样的好处是,原来过滤器的配置放在web.xml中,现在可以把filter的配置放在spring中,并由spring管理它的生命周期。另外,DelegatingFilterProxy中的targetBeanName指定需要从spring容器中获取的过滤器的名字,如果没有,它会以filterName过滤器名从spring容器中获取。
2.2 request的来源
前面说 DelegatingFilterProxy 会从spring容器中获取名为 targetBeanName 的过滤器。接下来看下spring配置文件,在这里定义了一个shiro Filter的工厂 org.apache.shiro.spring.web.ShiroFilterFactoryBean。
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login"/>
<property name="successUrl" value="/main"/>
<property name="unauthorizedUrl" value="/unauthorized"/>
<property name="filters">
<map>
<!--表单认证器-->
<entry key="authc" value-ref="formAuthenticationFilter"/>
</map>
</property>
<property name="filterChainDefinitions">
<value>
<!-- 请求 logout地址,shiro去清除session-->
/logout = logout
/static/** = anon
/** = authc
</value>
</property>
</bean>
熟悉spring 的应该知道,bean的工厂是用来生产相关的bean,并把bean注册到spring容器中的。通过查看工厂bean的getObject方法,可知,委托类调用的filter类型是SpringShiroFilter。接下来我们看一下类图,了解一下它们之间的关系。
既然SpringShiroFilter属于过滤器,那么它肯定有一个doFilter方法,doFilter由它的父类 OncePerRequestFilter 实现。OncePerRequestFilter 在doFilter方法中,判断是否在request中有"already filtered"这个属性设置为true,如果有,则交给下一个过滤器,如果没有就执行 doFilterInternal( ) 抽象方法。
doFilterInternal由AbstractShiroFilter类实现,即SpringShiroFilter的直属父类实现。doFilterInternal 一些关键流程如下:
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException { //包装request/response
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain); //创建subject,其实创建的是Subject的代理类DelegatingSubject
final Subject subject = createSubject(request, response); // 继续执行过滤器链,此时的request/response是前面包装好的request/response
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
}
});
}
在doFilterInternal中,可以看到对ServletRequest和ServletReponse进行了包装。除此之外,还把包装后的request/response作为参数,创建Subject,这个subject其实是代理类DelegatingSubject。
那么,这个包装后的request是什么呢?我们继续解析prepareServletRequest。
protected ServletRequest prepareServletRequest(ServletRequest request, ServletResponse response, FilterChain chain) {
ServletRequest toUse = request;
if (request instanceof HttpServletRequest) {
HttpServletRequest http = (HttpServletRequest) request;
toUse = wrapServletRequest(http); //真正去包装request的方法
}
return toUse;
}
继续包装request,看下wrapServletRequest方法。无比兴奋啊,文章前面的ShiroHttpServletRequest终于出来了,我们在controller中获取到的request就是它,是它,它。它是servlet的HttpServletRequestWrapper的子类。
protected ServletRequest wrapServletRequest(HttpServletRequest orig) {
//看看看,ShiroHttpServletRequest
return new ShiroHttpServletRequest(orig, getServletContext(), isHttpSessions());
}
ShiroHttpServletRequest构造方法的第三个参数是个关键参数,我们先不管它怎么来的,进ShiroHttpServletRequest里面看看它有什么用。它主要在两个地方用到,一个是getRequestedSessionId(),这个是获取sessionid的方法;另一个是getSession(),它是获取session会话对象的。
先来看一下getRequestedSessionId()。isHttpSessions决定sessionid是否来自servlet。
public String getRequestedSessionId() {
String requestedSessionId = null;
if (isHttpSessions()) {
requestedSessionId = super.getRequestedSessionId(); //从servlet中获取sessionid
} else {
Object sessionId = getAttribute(REFERENCED_SESSION_ID); //从request中获取REFERENCED_SESSION_ID这个属性
if (sessionId != null) {
requestedSessionId = sessionId.toString();
}
} return requestedSessionId;
}
再看一下getSession()。isHttpSessions决定了session是否来自servlet。
public HttpSession getSession(boolean create) { HttpSession httpSession; if (isHttpSessions()) {
httpSession = super.getSession(false); //从servletRequest获取session
if (httpSession == null && create) {
if (WebUtils._isSessionCreationEnabled(this)) {
httpSession = super.getSession(create); //从servletRequest获取session
} else {
throw newNoSessionCreationException();
}
}
} else {
if (this.session == null) { boolean existing = getSubject().getSession(false) != null; //从subject中获取session Session shiroSession = getSubject().getSession(create); //从subject中获取session
if (shiroSession != null) {
this.session = new ShiroHttpSession(shiroSession, this, this.servletContext);
if (!existing) {
setAttribute(REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
}
}
}
httpSession = this.session;
} return httpSession;
}
既然isHttpSessions()那么重要,我们还是要看一下在什么情况下,它返回true。
protected boolean isHttpSessions() {
return getSecurityManager().isHttpSessionMode();
}
isHttpSessions是否返回true是由使用的shiro安全管理器的 isHttpSessionMode() 决定的。回到前面,我们使用的安全管理器是 DefaultWebSecurityManager ,我们看一下 DefaultWebSecurityManager 的源码,找到 isHttpSessionMode 方法。可以看到,SessionManager 的类型和 isServletContainerSessions() 起到了决定性的作用。
public boolean isHttpSessionMode() {
SessionManager sessionManager = getSessionManager();
return sessionManager instanceof WebSessionManager && ((WebSessionManager)sessionManager).isServletContainerSessions();
}
在配置文件中,我们并没有配置SessionManager ,安全管理器会使用默认的会话管理器 ServletContainerSessionManager,在 ServletContainerSessionManager 中,isServletContainerSessions 返回 true 。
因此,在前面的shiro配置的情况下,request中获取的session将会是servlet context下的session。
2.3 subject的session来源
前面 doFilterInternal 的分析中,还落下了subject的创建过程,接下来我们解析该过程,从而揭开通过subject获取session,这个session是从哪来的。
回忆下,在controller中怎么通过subject获取session。
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession(); // session的类型?
我们看一下shiro定义的session类图,它们具有一些与 HttpSession 相同的方法,例如 setAttribute 和 getAttribute。
还记得在 doFilterInternal 中,shiro把包装后的request/response作为参数,创建subject吗
final Subject subject = createSubject(request, response);
subject的创建时序图
最终,由 DefaultWebSubjectFactory 创建subject,并把 principals, session, request, response, securityManager这些参数封装到subject。由于第一次创建session,此时session没有实例。
那么,当我们调用 subject .getSession() 尝试获取会话session时,发生了什么呢。从前面的代码可以知道,我们获取到的subject是 WebDelegatingSubject 类型的,它的父类 DelegatingSubject 实现了getSession 方法,下面的代码是getSession方法中的关键步骤。
public Session getSession(boolean create) {
if (this.session == null && create) {
// 创建session上下文,上下文里面封装有request/response/host
SessionContext sessionContext = createSessionContext();
// 根据上下文,由securityManager创建session
Session session = this.securityManager.start(sessionContext);
// 包装session
this.session = decorate(session);
}
return this.session;
}
接下来解析一下,安全管理器根据会话上下文创建session这个流程,追踪代码后,可以知道它其实是交由 sessionManager 会话管理器进行会话创建,由前面的代码可以知道,这里的sessionManager 其实是 ServletContainerSessionManager类,找到它的 createSession 方法。
protected Session createSession(SessionContext sessionContext) throws AuthorizationException {
HttpServletRequest request = WebUtils.getHttpRequest(sessionContext); // 从request中获取HttpSession
HttpSession httpSession = request.getSession(); String host = getHost(sessionContext); // 包装成 HttpServletSession
return createSession(httpSession, host);
}
这里就可以知道,其实session是来源于 request 的 HttpSession,也就是说,来源于上一个过滤器中request的HttpSession。HttpSession 以成员变量的形式存在 HttpServletSession 中。回忆前面从安全管理器获取 HttpServletSession 后,还调用 decorate() 装饰该session,装饰后的session类型是 StoppingAwareProxiedSession,HttpServletSession 是它的成员 。
在文章一开始的时候,通过debug就已经知道,当我们通过 subject.getSession() 获取的就是 StoppingAwareProxiedSession,可见,这与前面分析的是一致的 。
那么,当我们通过session.getAttribute和session.addAttribute时,StoppingAwareProxiedSession 做了什么?它是由父类 ProxiedSession 实现 session.getAttribute和session.addAttribute 方法。我们看一下 ProxiedSession 相关源码。
public Object getAttribute(Object key) throws InvalidSessionException {
return delegate.getAttribute(key);
}
public void setAttribute(Object key, Object value) throws InvalidSessionException {
delegate.setAttribute(key, value);
}
可见,getAttribute 和 addAttribute 由委托类delegate完成,这里的delegate就是HttpServletSession 。接下来看 HttpServletSession 的相关方法。
public Object getAttribute(Object key) throws InvalidSessionException {
try {
return httpSession.getAttribute(assertString(key));
} catch (Exception e) {
throw new InvalidSessionException(e);
}
} public void setAttribute(Object key, Object value) throws InvalidSessionException {
try {
httpSession.setAttribute(assertString(key), value);
} catch (Exception e) {
throw new InvalidSessionException(e);
}
}
此处的httpSession就是之前从HttpServletRequest获取的,也就是说,通过request.getSeesion()与subject.getSeesion()获取session后,对session的操作是相同的。
结论
(1)controller中的request,在shiro过滤器中的doFilterInternal方法,将被包装为ShiroHttpServletRequest 。
(2)在controller中,通过 request.getSession(_) 获取会话 session ,该session到底来源servletRequest 还是由shiro管理并管理创建的会话,主要由 安全管理器 SecurityManager 和 SessionManager 会话管理器决定。
(3)不管是通过 request.getSession或者subject.getSession获取到session,操作session,两者都是等价的。在使用默认session管理器的情况下,操作session都是等价于操作HttpSession。
Shiro session和Spring session一样吗?的更多相关文章
- Spring Session实现分布式session的简单示例
前面有用 tomcat-redis-session-manager来实现分布式session管理,但是它有一定的局限性,主要是跟tomcat绑定太紧了,这里改成用Spring Session来管理分布 ...
- Re:从零开始的Spring Session(二)
上一篇文章介绍了一些Session和Cookie的基础知识,这篇文章开始正式介绍Spring Session是如何对传统的Session进行改造的.官网这么介绍Spring Session: Spri ...
- Re:从零开始的Spring Session(一)
Session和Cookie这两个概念,在学习java web开发之初,大多数人就已经接触过了.最近在研究跨域单点登录的实现时,发现对于Session和Cookie的了解,并不是很深入,所以打算写两篇 ...
- 通过Spring Session实现新一代的Session管理
长期以来,session管理就是企业级Java中的一部分,以致于我们潜意识就认为它是已经解决的问题,在最近的记忆中,我们没有看到这个领域有很大的革新. 但是,现代的趋势是微服务以及可水平扩展的原生云应 ...
- 170222、使用Spring Session和Redis解决分布式Session跨域共享问题
使用Spring Session和Redis解决分布式Session跨域共享问题 原创 2017-02-27 徐刘根 Java后端技术 前言 对于分布式使用Nginx+Tomcat实现负载均衡,最常用 ...
- 转:通过Spring Session实现新一代的Session管理
长期以来,session管理就是企业级Java中的一部分,以致于我们潜意识就认为它是已经解决的问题,在最近的记忆中,我们没有看到这个领域有很大的革新. 但是,现代的趋势是微服务以及可水平扩展的原生云应 ...
- Spring Boot与Spring Session集成
1. 参考资料 https://docs.spring.io/spring-session/docs/current/reference/html5/guides/boot-redis.html ht ...
- 别再让你的微服务裸奔了,基于 Spring Session & Spring Security 微服务权限控制
微服务架构 网关:路由用户请求到指定服务,转发前端 Cookie 中包含的 Session 信息: 用户服务:用户登录认证(Authentication),用户授权(Authority),用户管理(R ...
- 【Spring Session】和 Redis 结合实现 Session 共享
[Spring Session]和 Redis 结合实现 Session 共享 参考官方文档 HttpSession with Redis Guide https://docs.spring.io/s ...
随机推荐
- BZOJ4831: [Lydsy1704月赛]序列操作(非常nice的DP& 贪心)
4831: [Lydsy1704月赛]序列操作 Time Limit: 1 Sec Memory Limit: 128 MBSubmit: 250 Solved: 93[Submit][Statu ...
- JSON和JSONP简单总结
jsonp和json的区别,原理,在jquery中的使用 http://www.cnblogs.com/dowinning/archive/2012/04/19/json-jsonp-jquery.h ...
- 在linux安装redis单机和集群后,如何在windows上使用redis客户端或者java代码访问错误的原因很简单,就是没有连接上redis服务,由于redis采用的安全策略,默认会只准许本地访问。需要通过简单配置,完成允许外网访问。
这几天在学习在linux上搭建服务器的工作,可谓历经艰辛.可喜最后收获也不少. 这次是在linux上搭建redis服务器后从windows上缺无法访问,连接不上. 仔细回忆以前搭建nginx和ftp的 ...
- pandas 的Series 里经常会出现DatetimeIndex这个类
DatetimeIndex 的操作还是值得研究一下的. 参考其用法, http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Date ...
- 怎样在windows下和linux下获取文件(如exe文件)的具体信息和属性
版权声明:本文为博主原创文章.未经博主同意不得转载. https://blog.csdn.net/xmt1139057136/article/details/25620685 程序猿都非常懒.你懂的! ...
- Hadoop序列化与Writable接口(一)
Hadoop序列化与Writable接口(一) 序列化 序列化(serialization)是指将结构化的对象转化为字节流,以便在网络上传输或者写入到硬盘进行永久存储:相对的反序列化(deserial ...
- Mac环境下终端(Terminal)用ssh 连接服务器问题 Received disconnect from 120.55.x.x: 2: Too many authentication failures for root
由于这台Mac配置git生成公钥后,ssh连接就出现来这个问题 Received disconnect from 120.55.x.x: 2: Too many authentication fail ...
- foreach(PHP学习)
先来看一个例子: $arr = array(0,1,2,3,4);让数组的每个值都变成原来的两倍,应该怎么来实现? 如果没有学习foreach之前,会想到用for循环 <?php $arr = ...
- JAVASE02-Unit011: TCP通信(小程序)
TCP通信(小程序) server端: package chat; import java.io.BufferedReader; import java.io.IOException; import ...
- NOIP2013 Day1
1.转圈游戏 https://www.luogu.org/problem/show?pid=1965 这道题失误极大,把freopen注释掉了,导致第一题暴0. 注意:在考试时一定要留下最后的时间检查 ...