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
具体配置参考【3】。