会话

  • 即用户访问应用时保持的连接关系,在多次交互中应用能识别出当前访问的用户是谁,且可以在多次交互中保存一些数据。如访问一些网站时登录成功后可以记住用户,且在退出之前都可以识别当前用户是谁。
  • 获取会话:登陆成功后即可获取会话。
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