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实现类如下
org.apache.shiro.web.session.HttpServletSession由标准servlet容器javax.servlet.http.HttpSession支持。它不与Shiro的会话相关组件SessionManager、SecurityManager等交互,而是通过与提供的servlet容器 httpsession实例交互来满足所有方法实现。其属性和方法如下:
javax.servlet.http.HttpSession实现类如下:
其中ShiroHttpSession是一个包装类,在底层使用一个Shiro Session替代标准servlet容器javax.servlet.http.HttpSession。这在异类客户机环境中是必需的,在异类客户机环境中,会话既用于业务层,也用于多种客户机技术(Web、Swing、Flash等),因为单独的servlet容器会话不支持此功能。
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