会话
- 即用户访问应用时保持的连接关系,在多次交互中应用能识别出当前访问的用户是谁,且可以在多次交互中保存一些数据。如访问一些网站时登录成功后可以记住用户,且在退出之前都可以识别当前用户是谁。
- 获取会话:登陆成功后即可获取会话。
login("classpath:shiro.ini", "zhang", "123"); //登录成功相当于创建了会话
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
//等价于
subject.getSession(true) //没有Session对象会创建一个,若传入false则无Session将返回null
若启用会话存储功能则在创建Subject时会主动创建一个Session。
获取属性:
//获取当前会话的唯一标识
session.getId();
//获取主机地址
session.getHost();
//获取、设置当前Session的过期时间,默认是会话管理器的全局过期时间
session.getTimeout();
session.setTimeout();
//获取会话的启动时间及最后访问时间;javaSE应用需要自己定期调用session.touch()更新最后访问时间
//web应用每次进入ShiroFilter都会自动调用它来更新。
session.getStartTimestamp();
session.getLastAccessTime();
//更新会话最后访问时间及销毁会话;Subject.logout()时会自动调用stop()。
//在web中调用javax.servlet.http.HttpSession. invalidate()也会自动调用它来销毁Session会话。
session.touch();
session.stop();
//操作会话属性
session.setAttribute("key","123");
session.getattribute("key");
session.removeAttribute("key");
会话管理器
- 管理着应用中所有Subject的会话的创建、维护、删除、失效和验证等工作,是Shiro的核心组件。
- SecurityManager都继承了会话管理器SessionSecurityManager。
- SecurityManager:提供了如下接口:
Session start(SessionContext context); //启动会话
Session getSession(SessionKey key) throws SessionException; //根据会话Key获取会话
- WebSessionManager:专用于web应用,又提供了如下接口:
boolean isServletContainerSessions();//是否使用Servlet容器的会话
- ValidatingSessionManager:用于验证会话是否过期:
void validateSessions(); //所有会话是否过期
- 三个内置实现类
- DefaultSessionManager:DefaultSecurityManager使用,用于javaSE环境。
- ServletContainerSessionManager:DefaultWebSecurityManager使用,用于web环境,直接使用Servlet容器的会话。
- DefaultWebSessionManager:同上,不过是由自己维护着会话,不用Servlet容器的会话管理。
- 替换SecurityManager默认的SessionManager可以在ini中配置:
[main]
sessionManager=org.apache.shiro.session.mgt.DefaultSessionManager
;web环境下应换为
sessionManager=org.apache.shiro.web.session.mgt.ServletContainerSessionManager
securityManager.sessionManager=$sessionManager
- 设置会话的全局过期时间(单位:ms),默认30分钟。将应用给所有Session,不过可以单独设置每个session的timeout属性。
sessionManager. globalSessionTimeout=1800000
如果使用ServletContainerSessionManager进行会话管理Session的超时依赖于底层Servlet容器的超时时间,可以在web.xml中设置(单位:min):
<session-config> <session-timeout>30</session-timeout> </session-config>
使用DefaultWebSessionManager维护会话:
//sessionIdCookie:创建会话cookie的模板
sessionIdCookie=org.apache.shiro.web.servlet.SimpleCookie
sessionManager=org.apache.shiro.web.session.mgt.DefaultWebSessionManager
//设置cookie名,默认为JSESSIONID
sessionIdCookie.name=sid
//设置cookie域名,默认空,即当前访问的域名
#sessionIdCookie.domain=sishuok.com
//设置cookie路径,默认空,即存储在域名根下
#sessionIdCookie.path=
//设置cookie过期时间,单位秒,默认-1,即关闭浏览器时过期
sessionIdCookie.maxAge=1800
//true表示客户端不会暴露脚本代码,有助于减少某些类型的跨站点脚本攻击,Servlet2.5及以上才支持
sessionIdCookie.httpOnly=true
sessionManager.sessionIdCookie=$sessionIdCookie
是否启用Session Id Cookie,默认启用;禁用将不会设置Session Id Cookie,即默认使用JSESSIONID
sessionManager.sessionIdCookieEnabled=true
securityManager.sessionManager=$sessionManager
- 配置了会话之后,第一次访问项目(任何页面)报错:There is no session with id […],没事,这是shiro自己的一个bug,无伤大雅。
会话监听器
- 用于监听会话创建、过期及停止事件。
public class MySessionListener1 implements SessionListener {
@Override
public void onStart(Session session) {//会话创建时触发
System.out.println("会话创建:" + session.getId());
}
@Override
public void onExpiration(Session session) {//会话过期时触发
System.out.println("会话过期:" + session.getId());
}
@Override
public void onStop(Session session) {//退出/会话过期时触发
System.out.println("会话停止:" + session.getId());
}
}
- 如果只想监听某一事件,可以继承SessionListenerAdapter实现类。
public class MySessionListener2 extends SessionListenerAdapter {
@Override
public void onStart(Session session) {
System.out.println("会话创建:" + session.getId());
}
}
- ini配置监听器
sessionListener1=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener1
sessionListener2=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener2
sessionManager.sessionListeners=$sessionListener1,$sessionListener2
会话持久化
- SessionDAO接口:用于会话的CRUD,可以把会话保存到数据库。提供如下基本接口:
//如DefaultSessionManager在创建完session后会调用该方法;如保存到关系数据库/文件系统/
//NoSQL数据库;即可以实现会话的持久化;返回会话ID,返回的ID.equals(session.getId());
Serializable create(Session session);
//根据会话ID获取会话
Session readSession(Serializable sessionId) throws UnknownSessionException;
//更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
void update(Session session) throws UnknownSessionException;
//删除会话;当会话过期/会话停止(如用户退出时)会调用
void delete(Session session);
//获取当前所有活跃用户,如果用户量多此方法影响性能
Collection<Session> getActiveSessions();
- AbstractSessionDAO: 提供了SessionDAO的基础实现,如生成会话id等。
- CacheSessionDAO:提供了对开发者透明的会话缓存的功能,如查询会话时先到缓存看有没有,没有再查数据库。只需设置相应的CacheManager即可。自定义SessionDAO继承这个即可。
- MemorySessionDAO:直接在内存中进行会话维护。
EnterpriseCacheSessionDAO:Enterprise,事业;提供了缓存功能的会话维护,默认使用MapCache,内部使用ConcurrentHashMap保存缓存的会话,ConCurrent,同时发生的,同时存在的。
ini配置SessionDAO
sessionDAO=org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
sessionManager.sessionDAO=$sessionDAO
Shiro使用Ehcache存储会话,可以配合TerraCotta实现容器无关的分布式集群,依赖如下:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.2</version> </dependency>
shiro-web.ini
sessionDAO=org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
//CachingSessionDAO设置Session缓存名字,默认为此
sessionDAO.activeSessionsCacheName=shiro-activeSessionCache
sessionManager.sessionDAO=$sessionDAO
//cacheManager:缓存管理器,次数使用Ehcache实现
cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager
//指定Ehcache缓存的配置文件
cacheManager.cacheManagerConfigFile=classpath:ehcache.xml
securityManager.cacheManager = $cacheManager
ehcache.xml
<cache name="shiro-activeSessionCache" //名字与sessionDAO的activeSessionsCacheName属性一致 maxEntriesLocalHeap="10000" overflowToDisk="false" eternal="false" diskPersistent="false" timeToLiveSeconds="0" timeToIdleSeconds="0" statistics="true"/>
另外可以在ini中配置会话id生成器
//默认为此,底层使用UUID生成。
sessionIdGenerator=org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator
sessionDAO.sessionIdGenerator=$sessionIdGenerator
自定义SessionDAO
public class MySessionDAO extends CachingSessionDAO { //带缓存的SessionDAO,会先从缓存找
private JdbcTemplate jdbcTemplate = JdbcTemplateUtils.jdbcTemplate();
//保存会话
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();
}
//更新会话
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());
}
//删除会话
protected void doDelete(Session session) {
String sql = "delete from sessions where id=?";
jdbcTemplate.update(sql, session.getId());
}
//查询会话
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));
}
}
- 接着在shiro-web.ini中配置这个自定义的会话持久化
sessionDAO=com.github.zhangkaitao.shiro.chapter10.session.dao.MySessionDAO
会话验证
- SessionValidationScheduler:会话验证调度器,用于定期地验证会话是否已过期,是将停止会话。出于性能考虑,一般都是获取会话时来验证会话是否过期并停止会话的,但是在web环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期检测。
//默认
sessionValidationScheduler=org.apache.shiro.session.mgt
.ExecutorServiceSessionValidationScheduler
//调度时间间隔,单位ms,默认1小时
sessionValidationScheduler.interval = 3600000
//设置进行会话验证时的会话管理器
sessionValidationScheduler.sessionManager=$sessionManager
//设置全局会话超时时间,默认30min,30min内没有访问会话将过期
sessionManager.globalSessionTimeout=1800000
//是否开启会话验证器,默认开启
sessionManager.sessionValidationSchedulerEnabled=true
sessionManager.sessionValidationScheduler=$sessionValidationScheduler
Quartz会话验证调度器
依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-quartz</artifactId> <version>1.2.2</version> </dependency> <dependency> <!--使用quartz要用到的--> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
ini配置
sessionValidationScheduler=org.apache.shiro.session.mgt.quartz
.QuartzSessionValidationScheduler
sessionValidationScheduler.sessionValidationInterval = 3600000
sessionValidationScheduler.sessionManager=$sessionManager
分页获取会话并验证
- 会话验证调度器实际都是调用AbstractValidatingSessionManager的validateSessions()进行验证,该方法调用SessionDAO的getActiveSessions()获取all会话进行验证,会话较多时会影响性能,可以考虑分页获取会话并进行验证。
- 自定义分页获取会话并验证的会话验证调度器,核心代码:
//分页获取会话
String sql = "select session from sessions limit ?,?";
int start = 0; //起始记录
int size = 20; //每页大小
List<String> sessionList = jdbcTemplate.queryForList(sql, String.class, start, size);
while(sessionList.size() > 0) {
for(String sessionStr : sessionList) {
try {
Session session = SerializableUtils.deserialize(sessionStr); //反序列化回session对象
//获取验证方法
Method validateMethod =
ReflectionUtils.findMethod(AbstractValidatingSessionManager.class,
"validate", Session.class, SessionKey.class);
validateMethod.setAccessible(true);
//调用验证方法
ReflectionUtils.invokeMethod(validateMethod,
sessionManager, session, new DefaultSessionKey(session.getId()));
} catch (Exception e) {
//ignore
}
}
start = start + size;
sessionList = jdbcTemplate.queryForList(sql, String.class, start, size);
}
ini配置和之前类似。
如果在会话过期时不想删除,可以通过如下配置:
//默认开启,在会话过期后会调用SessionDAO的delete()删除会话
sessionManager.deleteInvalidSessions=false
若在获取会话时验证了会话已过期,将抛出InvalidSessionException,需要捕获该异常并跳转相应页面提示会话已过期,让用户重新登录。可以在web.xml配置错误页面
<error-page> <exception-type>org.apache.shiro.session.InvalidSessionException</exception-type> <location>/invalidSession.jsp</location> </error-page>
SessionFactory
- 创建会话的工厂。默认用SimpleSessionFactory来创建SimpleSession会话。
- 首先自定义一个Session:保存用户的状态。
public class OnlineSession extends SimpleSession {
public static enum OnlineStatus {
//创建实例,调用构造器(要求传入字符串,则不能不传)
on_line("在线"), hidden("隐身"), force_logout("强制退出");
private final String info;
private OnlineStatus(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
}
private String userAgent; //用户浏览器类型
private OnlineStatus status = OnlineStatus.on_line; //在线状态
private String systemHost; //用户登录时系统IP
//省略其他
}
- 接着自定义SessionFactory
public class OnlineSessionFactory implements SessionFactory {
/**
* 根据会话上下文创建OnlineSession,保存用户信息进去
*/
@Override
public Session createSession(SessionContext initData) {
OnlineSession session = new OnlineSession();
if (initData != null && initData instanceof WebSessionContext) {
WebSessionContext sessionContext = (WebSessionContext) initData;
HttpServletRequest request = (HttpServletRequest) sessionContext.getServletRequest();
if (request != null) {
session.setHost(IpUtils.getIpAddr(request));
session.setUserAgent(request.getHeader("User-Agent"));
session.setSystemHost(request.getLocalAddr() + ":" + request.getLocalPort());
}
}
return session;
}
}
- 最后在shiro-web.ini配置自定义的SessionFactory
sessionFactory=org.apache.shiro.session.mgt.OnlineSessionFactory
sessionManager.sessionFactory=$sessionFactory