Shiro - 会话管理与SessionDao

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/J080624/article/details/83714339

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

【1】Shiro Session接口与实现类

这里的Session不再是我们通常使用的javax.servlet.http.HttpSession,而是org.apache.shiro.session.Session。

一个Session是与一段时间内与软件系统交互的单个对象(用户、守护进程等)相关的有状态数据上下文。

该Session旨在由业务层管理,并且可以通过其他层访问,而不绑定到任何给定的客户端技术。这是一个很大的好处对Java系统而言,因为到目前为止,唯一可行的会话机制是{javax .servlet .http.httpsession }或有状态会话EJB,这些应用程序多次不必要地将应用程序耦合到Web或EJB技术。

不过在使用上与httpsession 有相似之处,相关API如下:

  • Subject.getSession():即可获取会话;其等价于Subject.getSession(true),即如果当前没有创建Session 对象会创建一个;Subject.getSession(false),如果当前没有创建Session 则返回null
  • session.getId():获取当前会话的唯一标识
  • session.getHost():获取当前Subject的主机地址
  • session.getTimeout() & session.setTimeout(毫秒):获取/设置当前Session的过期时间
  • session.getStartTimestamp() & session.getLastAccessTime():获取会话的启动时间及最后访问时间。
    如果是JavaSE应用需要自己定期调用session.touch() 去更新最后访问时间;如果是Web 应用,每次进入ShiroFilter都会自动调用session.touch() 来更新最后访问时间。
  • session.touch() & session.stop():更新会话最后访问时间及销毁会话。
    当Subject.logout()时会自动调用stop 方法来销毁会话。如果在web中,调用HttpSession. invalidate() 也会自动调用ShiroSession.stop方法进行销毁Shiro的会话
  • session.setAttribute(key, val) & session.getAttribute(key) & session.removeAttribute(key):设置/获取/删除会话属性;在整个会话范围内都可以对这些属性进行操作。

Session实现类如下
在这里插入图片描述


HttpSession实现类如下:

在这里插入图片描述


SessionManager实现类如下:

在这里插入图片描述


【2】会话监听器

会话监听器用于监听会话创建、过期及停止事件。

源码如下:

public interface SessionListener {

    /**
     * Notification callback that occurs when the corresponding Session has started.
     *
     * @param session the session that has started.
     */
    void onStart(Session session);

    /**
     * Notification callback that occurs when the corresponding Session has stopped, either programmatically via
     * {@link Session#stop} or automatically upon a subject logging out.
     *
     * @param session the session that has stopped.
     */
    void onStop(Session session);

    /**
     * Notification callback that occurs when the corresponding Session has expired.
     * <p/>
     * <b>Note</b>: this method is almost never called at the exact instant that the {@code Session} expires.  Almost all
     * session management systems, including Shiro's implementations, lazily validate sessions - either when they
     * are accessed or during a regular validation interval.  It would be too resource intensive to monitor every
     * single session instance to know the exact instant it expires.
     * <p/>
     * If you need to perform time-based logic when a session expires, it is best to write it based on the
     * session's {@link org.apache.shiro.session.Session#getLastAccessTime() lastAccessTime} and <em>not</em> the time
     * when this method is called.
     *
     * @param session the session that has expired.
     */
    void onExpiration(Session session);
}


Shiro Session一个重要应用

在Controller通常会使用HttpSession进行操作,那么在Service层为了降低侵入、解耦,我们就可以使用Shiro Session进行操作。

如在Controller放入Session中一个键值对:

	 @ResponseBody
    @RequestMapping(value="/test",produces="application/json;charset=utf-8")
    public String  test(HttpSession session) {
    	System.out.println("调用方法test");
    	session.setAttribute("key", "123456");
    	return "success";
    }

在Service使用Shiro Session进行获取:

	@Override
	public List<SysRole> getRoleListByUserId(Long id) {
		// TODO Auto-generated method stub
		Session session = SecurityUtils.getSubject().getSession();
		Object attribute = session.getAttribute("key");
		List<SysRole> roleListByUserId = userServiceDao.getRoleListByUserId(id);
		return roleListByUserId;
	}

【3】SessionDao

SessionDao提供了一种方式,使我们能够将session存入数据库(缓存中)中进行CRUD操作。这有什么意义?当只有一台服务器一个项目的时候通常你不必管理Session,Shiro会自行管理Session。

但是如果有多个服务器同时跑一个项目呢?或者单点登录,不同项目在不同服务器,但是需要实现单点登录功能。这是你就需要在服务器之间共享Session!项目中通常我们使用Redis来实现共享Session。

① SessionDao接口继承图如下:

在这里插入图片描述


② 几个实现类

AbstractSessionDAO提供了SessionDAO的基础实现,如生成会话ID等。

CachingSessionDAO提供了对开发者透明的会话缓存的功能,需要设置相应的CacheManager。

MemorySessionDAO直接在内存中进行会话维护。

EnterpriseCacheSessionDAO提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。


③ xml配置与自定义MySessionDao

pom文件中关于Shiro依赖如下:

 <!-- shiro 版本为1.4.0 -->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>${shiro.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-ehcache</artifactId>
      <version>${shiro.version}</version>
    </dependency>
   
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-web</artifactId>
      <version>${shiro.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring</artifactId>
      <version>${shiro.version}</version>
    </dependency>
    <dependency> 
	    <groupId>org.apache.shiro</groupId>
	    <artifactId>shiro-quartz</artifactId> 
	    <version>${shiro.version}</version>
	</dependency>

自定义MySessionDao:

public class MySessionDao extends EnterpriseCacheSessionDAO {

	//这里注入Spring提供的JdbcTemplate
	@Autowired
	private JdbcTemplate jdbcTemplate = null;

	@Override
	protected Serializable doCreate(Session session) {
		Serializable sessionId = generateSessionId(session);
		assignSessionId(session, sessionId);
		String sql = "insert into sessions(id, session) values(?,?)";
		jdbcTemplate.update(sql, sessionId,
				SerializableUtils.serialize(session));
		return session.getId();
	}

	@Override
	protected Session doReadSession(Serializable sessionId) {
		String sql = "select session from sessions where id=?";
		List<String> sessionStrList = jdbcTemplate.queryForList(sql,
				String.class, sessionId);
		if (sessionStrList.size() == 0)
			return null;
		return SerializableUtils.deserialize(sessionStrList.get(0));
	}
	
	@Override
	protected void doUpdate(Session session) {
		if (session instanceof ValidatingSession
				&& !((ValidatingSession) session).isValid()) {
			return; 
		}
		String sql = "update sessions set session=? where id=?";
		jdbcTemplate.update(sql, SerializableUtils.serialize(session),
				session.getId());
	}

	@Override
	protected void doDelete(Session session) {
		String sql = "delete from sessions where id=?";
		jdbcTemplate.update(sql, session.getId());
	}
}

Shiro XML配置如下:

 <!-- 配置需要向Cookie中保存数据的配置模版 --> 
	<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> 
	    <!-- 在Tomcat运行下默认使用的Cookie的名字为JSESSIONID --> 
	    <constructor-arg value="shiro-session-id"/> 
	    <!-- 保证该系统不会受到跨域的脚本操作供给 --> 
	    <property name="httpOnly" value="true"/> 
	    <!-- 定义Cookie的过期时间,单位为秒,如果设置为-1表示浏览器关闭,则Cookie消失 --> 
	    <property name="maxAge" value="-1"/> 
	</bean>

    <!-- Session ID 生成器-->
	<bean id="sessionIdGenerator"
		class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>
	
	<!-- Session DAO. 继承 EnterpriseCacheSessionDAO -->
	<bean id="sessionDAO"
		class="com.web.maven.shiro.MySessionDao">
		<property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
		<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
	</bean>
	
	<!-- 会话管理器-->
	<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
	 <!-- 定义的是全局的session会话超时时间,此操作会覆盖web.xml文件中的超时时间配置 -->
		<property name="globalSessionTimeout" value="1800000"/>
		<!-- 删除所有无效的Session对象,此时的session被保存在了内存里面 -->
		<property name="deleteInvalidSessions" value="true"/>
		 <!-- 定义要使用的无效的Session定时调度器 -->
        <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
         <!-- 需要让此session可以使用该定时调度器进行检测 -->
        <property name="sessionValidationSchedulerEnabled" value="true"/>
        <!-- 定义Session可以进行操作的DAO -->
		<property name="sessionDAO" ref="sessionDAO"/>
		<!-- 所有的session一定要将id设置到Cookie之中,需要提供有Cookie的操作模版 -->
        <property name="sessionIdCookie" ref="sessionIdCookie"/>
         <!-- 定义sessionIdCookie模版可以进行操作的启用 -->
        <property name="sessionIdCookieEnabled" value="true"/>
        <!-- url sessionId  重写 -->
        <property name="sessionIdUrlRewritingEnabled" value="true"/>
	</bean>
	
	<!-- 配置session的定时验证检测程序类,以让无效的session释放 -->
    <bean id="sessionValidationScheduler"
        class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
        <!-- 设置session的失效扫描间隔,单位为毫秒 -->
        <property name="sessionValidationInterval" value="100000"/>
        <property name="sessionManager" ref="sessionManager" />
    </bean> 

	
    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!-- 注入自定义Realm -->
<!--         <property name="realm" ref="customRealm"/> -->
        <!-- 注入缓存管理器 -->
        <property name="cacheManager" ref="cacheManager"/>
        <property name="authenticator" ref="authenticator" />
        <property name="realms">
        	<list>
    			<ref bean="customRealm"/>
<!--     			<ref bean="customRealm2"/> -->
    		</list>
        </property>
        <property name="sessionManager" ref="sessionManager" />
    </bean>
    <!-- 认证器 -->
    <bean id="authenticator" 
    	class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
    	<property name="authenticationStrategy">
    		<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
    	</property>
    </bean>

    <!-- 缓存管理器 -->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
    </bean>
    
      <!-- 自定义Realm -->
    <bean id="customRealm" class="com.web.maven.shiro.CustomRealm">
        <!-- 将凭证匹配器设置到realm中,realm按照凭证匹配器的要求进行散列 -->
        <property name="credentialsMatcher">
           <bean  class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
              <property name="hashAlgorithmName" value="MD5"/>
              <property name="hashIterations" value="1"/>
            </bean>
        </property>
    </bean>
      <!-- 自定义SecondRealm -->
    <bean id="customRealm2" class="com.web.maven.shiro.CustomRealm2">
        <!-- 将凭证匹配器设置到realm中,realm按照凭证匹配器的要求进行散列 -->
        <property name="credentialsMatcher">
           <bean  class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
              <property name="hashAlgorithmName" value="SHA1"/>
              <property name="hashIterations" value="1"/>
            </bean>
        </property>
    </bean>
    
    <!-- 配置lifecycleBeanPostProcessor,可以自动的调用配置在spring IOC 容器中shiro bean的生命周期方法。 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"></bean>

	 <!-- 开启Shiro的注解,实现对Controller的方法级权限检查(如@RequiresRoles,@RequiresPermissions),
	 		需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证  -->   
    <!-- Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor has run -->  
	<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor" /> 
	
	<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
		<property name="securityManager" ref="securityManager" />
	</bean>
	
    <!-- Shiro过滤器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!-- Shiro的核心安全接口,这个属性是必须的 -->
        <property name="securityManager" ref="securityManager"/>
        <!-- loginUrl认证提交地址,如果没有认证将会请求此地址进行认证,请求此地址将由formAuthenticationFilter进行表单认证 -->
        <property name="loginUrl" value="/login"/>
        <!-- if is Authenticated,then ,rediret to the url  -->
        <property name="successUrl" value="/index"/>
        <!-- has no permission and then redirect to the url  -->
        <property name="unauthorizedUrl" value="/refuse"></property>
       <!--<property name="filters">
            <map>
               		重写 退出过滤器
                <entry key="logout" value-ref="systemLogoutFilter" />
            </map>
        </property>-->
        <!-- Shiro连接约束配置,即过滤链的定义 -->
        <property name="filterChainDefinitions">
            <value>
                <!-- /** = anon所有url都可以匿名访问 -->
                <!-- 对静态资源设置匿名访问 -->
                /test=anon
                /favicon.ico = anon
                /images/** = anon
                /js/** = anon
                /styles/** = anon
                /css/** = anon
                /*.jar = anon
                <!-- 验证码,可匿名访问 -->
                /validateCode = anon  
                /login = anon
                /doLogin = anon
                <!--请求logout,shrio擦除sssion-->
                /logout=logout
                <!-- /** = authc 所有url都必须认证通过才可以访问 -->
                /**=authc
            </value>
        </property>
<!--         <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap" /> -->
    </bean>
     <!-- 配置一个 bean, 该 bean 实际上是一个 Map. 通过实例工厂方法的方式 -->
<!--     <bean id="filterChainDefinitionMap"  -->
<!--     	factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"></bean> -->
    
<!--     <bean id="filterChainDefinitionMapBuilder" -->
<!--     	class="com.web.maven.factory.FilterChainDefinitionMapBuilder"></bean> -->

shiro-ehcache.xml中配置缓存如下:

<cache name="shiro-activeSessionCache"
      eternal="false"
      timeToIdleSeconds="3600"
      timeToLiveSeconds="0"
      overflowToDisk="false"
      statistics="true">
</cache>

数据表sessions创建语句如下:

create table sessions (
	id varchar(200),
	session varchar(2000),
	constraint pk_sessions primary key(id)
) charset=utf8 ENGINE=InnoDB;

【4】会话验证

Shiro提供了会话验证调度器,用于定期的验证会话是否已过期,如果过期将停止会话。

出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的。但是如在web 环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期。

Shiro提供了会话验证调度器SessionValidationScheduler,也提供了使用Quartz会话验证调度器–QuartzSessionValidationScheduler

具体配置参考【3】。

猜你喜欢

转载自blog.csdn.net/J080624/article/details/83714339