shiro(6)- SessionMananger(操作session)

shiro安全控制目录

Shiro提供了完整的企业级会话管理功能,不依赖底层容器(如web容器的tomcat),不管是JavaSE还是JavaEE环境都可以使用,提供了会话管理,会话监听,会话存储/持久化,容器无关的集群,失效/过期支持。对Web的透明支持,SSO单点登录的支持等特性。即使用Shiro的会话管理可以直接替换如Web容器的会话管理。

序 什么叫做会话?

会话是用户访问应用时保持的连接关系。

因为http协议是无状态的协议,所以,需要借助会话(session)来使得应用在多次交互中能够识别出当前访问的用户是谁。并且可以在多次会话中保存一些数据。

如访问一些网站时登录,网站可以记住用户,且在退出之前都可以识别当前用户是谁。

1. shiro Session简单的API

Shiro Session和HttpSession使用方式很像。当然它们最大的区别在于你可以在任何应用中使用Shiro Session,而不仅仅局限于Web应用。

1. 获取session对象
Shiro的会话支持不仅可以在普通JavaEE应用中使用,也可以在web应用中使用,且获取方式是一致的。

Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
//这个参数用于判定会话不存在时是否创建新会话。
Session session = subject.getSession(boolean create);

可以使用subject.getSession()获取会话,其等价于subject.getSession(true),即如果当前没有创建Session对象,会创建一个。

2. 获取会话的唯一标识

session.getId();

3. 获取主机地址

session.getHost();

获取当前subject的主机地址,该地址是通过HostAuthenticationToken.getHost()提供的。

4. 设置会话超时时间

//获取超时时间
session.getTimeout();
//设置超时时间
session.setTimeout(毫秒);

获取/设置当前Session的过期时间;如果不设置是默认的会话管理器的全局过期时间。

5. 获取启动/访问时间

//获取会话的启动时间
session.getStartTimestamp();
//获取会话的最后访问时间
session.getLastAccessTime();

获取会话的启动时间和最后访问时间;如果是JavaSE应用需要自己定期调用session.touch()去更新最后访问时间;如果是web应用,每次进入ShiroFilter都会自动调用session.touch()来更新最后访问时间。

6. 更新/删除会话

//更新会话最后访问时间
session.touch();
//销毁session会话
session.stop();

更新会话最后访问时间及销毁会话;当Subject.logout()时会自动调用stop方法来销毁会话的。如果在web中,调用javax.servlet.http.HttpSession. invalidate()也会自动调用Shiro Session.stop方法进行销毁Shiro的会话。

7. 操作会话

session.setAttribute("key", "123");  
Assert.assertEquals("123", session.getAttribute("key"));  
session.removeAttribute("key"); 

设置/获取/删除会话属性;在整个会话范围内都可以对这些属性进行操作。


SessionManager负责创建和管理用户Session生命周期,在任何环境下都可以提供用户健壮的session体验。默认情况下,Shiro会使用容器自带的session机制,但若是容器不存在session,那么Shiro会提供内置的企业级session来管理。当然在开发中,也可以使用SessionDAO允许数据源持久化Session。

2. 会话管理器

在安全框架领域,Apache Shiro提供了一些独特的东西,可以在任何应用和架构层一致的使用Session API,即Session不再依赖于Servlet或EJB容器。

Shiro会话最重要的一个好处便是它们独立于容器。通过Shiro会话,可以获取一个容器无关的集群解决方案。Shiro架构允许可插拔的会话数据存储,如企业缓存,关系型数据库,noSQL系统等。并且Shiro会话可以跨客户端技术进行共享。

值得一提的是Shiro在Web环境中对会话的支持。

缺省 Http 会话

对于Web应用,Shiro缺省将我们习以为常的Servlet容器会话作为其会话的基础设施。即调用subject.getSession()subject.getSession(boolean)方法时,Shiro会返回Servlet容器的HttpSession实例支持的Session实例。这种方式巧妙之处在于调用subject.getSession()的业务层代码会跟一个Shiro Session实例交互,并且实际上它也会跟基于Web的HttpSession打交道。可以维护架构层之间的清晰隔离。

Web 层中 Shiro 的原生会话

如果需要Shiro的企业级会话特性(如与容器无关的集群)而打开了Shiro的原生会话管理。而实际上我们也希望HttpServletRequest.getSession()HttpSession API能和Shiro原生的会话协作。而Shiro完整实现了Servlet规范中Session部分以及在Web应用中支持原生会话。这意味着,不管何时你使用相应的HttpServletRequest或HttpSession调用,Shiro都会将这些调用委托给内部的原生会话API。即无需修改Web代码,即使正在使用Shiro内置的Session机制,获取到的Servlet Session和Shiro Session依旧保持一致。

详见——让 Apache Shiro 保护你的应用

会话管理器

虚线:实现的接口;
实线:继承的父类;

Shiro提供了三个默认实现:

  • DefaultSessionManager:用于JavaSE环境;
  • ServletContainerSessionManager:用于Web环境,直接使用Servlet容器会话;
  • DefaultWebSessionManager:用于Web环境,自己维护会话,不会使用Servlet容器的会话管理。

3. subject和request获取Session的区别

3.1. 两者方式获取的session是否相同

1. 在Spring mvc中获取session有两种方法:

  1. 使用request对象获取session
Session session = request.getSession();
  1. 通过shiro获取session
 Subject currentUser = SecurityUtils.getSubject();
 Session session = currentUser.getSession();

一般在web中,有两种会话管理器

  • DefaultWebSessionManager (自己维护会话)
  • ServletContainerSessionManager(默认,直接使用servlet的会话)

而在项目中需要配置shiro的securityManager,因为配置影响了shiro session的来源。

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
      <property name="realm" ref="shiroRealm"/>
</bean>

2. 两种会话操纵的session是否相同?(注:以Servlet会话进行分析)

在controller中打印session,发现request获取的会话类型是:org.apache.catalina.session.StandardSessionFacade,而subject的session类型是org.apache.shiro.subject.support.DelegatingSubject$StoppingAwareProxiedSession

session的类型

在上图中,我们可以知道request获取的session明显是httpSession,而subject获取的session类型,本质上也是httpSession即两者在操作session时,都是操作的同一类型的session对象。

3.2 request对象中session的来源

  1. 如何获取过滤器filter

SpringMVC整合shiro,需要在web.xml中配置filter

 <filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>
            org.springframework.web.filter.DelegatingFilterProxy
        </filter-class>
    <init-param>
      <param-name>targetFilterLifecycle</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

DelegateFilterProxy是一个过滤器,准确来说是目的过滤器的代理,由它在doFilter方法中,获取spring容器中的过滤器,并调用目标过滤器的doFilter方法。这样做的好处是:原来的过滤器配置放在web.xml中,现在可以把filter的配置放在spring中,并由spring管理它的生命周期。

DelegatingFilterProxy——将Filter交由Spring管理

我们可以知道,使用DelegatingFilterProxy那么过滤器的生命周期由Spring来管理。若是没有指定targetBeanName,那么使用<filter-name>

  • spring.xml配置
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> 
  <!-- Shiro的核心安全接口,这个属性是必须的 -->  
  <property name="securityManager" ref="securityManager"/>  
  <!-- 要求登录时的链接,非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->  
  <property name="loginUrl" value="/login/init"/>  
  <!-- 用户访问未对其授权的资源时,所显示的连接 -->  
  <property name="unauthorizedUrl" value="/pages/error/403.jsp"/>  
  <property name="filterChainDefinitions"> 
    <value> 
      <!-- Shiro 过滤链的定义 --> 
      /login/init/** = anon 
      <!-- 对于登录相关不进行鉴权 --> 
      /login/getVerifyCode/** = anon
      <!-- 对于注册相关不进行鉴权 -->
      /register/** = anon
      <!-- 静态资源不进行鉴权 -->
      /static/** = anon
    </value> 
  </property>  
  <property name="filters"> 
    <map> 
      <entry key="user" value-ref="userFilter"/> 
    </map> 
  </property> 
</bean>

熟悉spring的应该知道,bean的工厂是用来生产相关的bean,并将bean注册到spring容器中。通过查看工厂bean的getObject方法,可见,委托类调用的filter类型是SpringShiroFilter。

SpringShiroFilter类图

既然SpringShiroFilter属于过滤器,那么肯定有一个doFilter方法,doFilter由它的父类OncePerRequestFilter实现。

OncePerRequestFilter在doFilter方法中,判断是否在request中有"already filtered"这个属性设置为true,如果有,则交给下一个过滤器,如果没有,就执行doFilterInternal()抽象方法。

doFilterInternal由AbstractShiroFilter类实现,即SpringShiroFilter的直属父类实现。

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和ServletResponse进行了包装,除此之外,还把包装后的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;
    }
 protected ServletRequest wrapServletRequest(HttpServletRequest orig) {
        //看看看,ShiroHttpServletRequest
        return new ShiroHttpServletRequest(orig, getServletContext(), isHttpSessions());  
    }

由此我们可以看到controller获取到的ShiroHttpServletRequest对象。

ShiroHttpServletRequest构造方法的第三个参数是关键参数。进入ShiroHttpServletRequest里面看看它有什么用?

  • 在getRequestedSessionId()方法用到,获取sessionId。
  • 在getSession()用到,获取session会话对象。

(1)先看下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;
    }

(2)再看下getSession。isHttpSession决定了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()方法。

public boolean isHttpSessionMode() {
        SessionManager sessionManager = getSessionManager();
        return sessionManager instanceof WebSessionManager && ((WebSessionManager)sessionManager).isServletContainerSessions();
    }

需要注意的是:在配置文件中,我们并没有配置SessionManager,安全管理器会使用会话管理器ServletContainerSessionManager,在ServletContainerSessionManager中,isServletContainerSessions返回true。

因此,在前面的配置中,request中获取的session将是servlet context下的session。

3.3. subject的session来源

前面的doFilterInternal的分析中,还提到了subject创建的过程。接着我们继续分析该过程,判断subject中的session的来源。

在controller中subject获取session

 Subject currentUser = SecurityUtils.getSubject();
 Session session = currentUser.getSession();

我们看下shiro定义的session类图,具有一些与HttpSession相同的方法,例如setAttribute和getAttribute。

session类图

在doFilterInternal中,shiro把包装后的request/response作为参数,创建subject

final Subject subject = createSubject(request, response);

最终,由DefaultWebSubjectFactory创建subject,并把principals [资本 普瑞色跑死], session, request, response, securityManager参数封装到subject。由于第一次创建session,此时session没有实例。

那么,当我们第一次调用subject.getSession()尝试获取session时,发生了什么?从前面的代码我们知道,我们获取到的subject是WebDelegatingSubject类型,它的父类DelegatingSubject实现了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;
    }

接下来解析一下,securityManager根据sessionContext 创建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就是它的成员。

session的getAttribute和addAttribute方法,StoppingAwareProxiedSession做了些什么?

它是由父类ProxiedSession实现session.getAttribute和session.addAttribute方法。

 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。

 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);
        }
    }

最后总结一下,通过request.getSeesion()与subject.getSeesion()获取session后,对session的操作是相同的。而session的来源是servletRequest还是shiro。主要是由安全管理器SecurityManager和SessionManager会话管理器决定。

参考文档:
springmvc集成shiro后,session、request姓汪还是姓蒋?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • 这部分主要是与Java Web和Web Service相关的面试题。 96、阐述Servlet和CGI的区别? 答...
    杂货铺老板阅读 1,343评论 0 10
  • 问题描述 之前在公司搭项目平台的时候权限框架采用的是shiro,由于系统主要面向的是APP端的用户,PC端仅仅是公...
    Briseis阅读 33,721评论 4 28
  • 1课程回顾 Servlet编程 1)Servlet生命周期(重点) 构造方法:创建servlet对象。默认情况下,...
    守亭翁阅读 196评论 0 0
  • 一. Java基础部分.................................................
    wy_sure阅读 3,729评论 0 11
  • 我大学 学的「计算机科学与技术专业」,是在大一的时候,想听听其他老师上的专业课(C language),然后就去旁...
    ColdRomantic阅读 324评论 0 0