背景:

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

shiro 中的 session 特性:

       1、基于 pojo/j2seshiro session 相关的类都是基于接口实现的简单的 java 对象(pojo),兼容所有 java 对象的配置方式,扩展也更方便,完全可以定制自己的会话管理功能 。

       2、简单灵活的会话存储/持久化:因为 shiro 中的 session 对象是基于简单的 java 对象的,所以你可以将 session 存储在任何地方,例如,文件,各种数据库,内存中等。

       3、容器无关的集群功能:shiro 中的 session 可以很容易的集成第三方的缓存产品完成集群的功能。例如:ehcache + terracotta ,coherence,gigaSpaces 等。你可以很容易的实现会话集群而无需关注底层的容器实现。

       4、异构客户端的访问:可以实现 web 中的 session 和非 web 项目中的 session 共享。

       5、会话事件监听:提供对 session 整个生命周期的监听。

       6、保存主机地址:在会话开始 session 会存用户的 ip 地址和主机名,以此可以判断用户的位置。

       7、会话失效/过期的支持:用户长时间处于不活跃状态可以使会话过期,调用 touch() 方法可以主动更新最后访问时间,让会话处于活跃状态。

       8、透明的 web 支持:shiro 全面支持 servlet 2.5 中的 session 规范。这意味着你可以将你现有的 web 程序改为 shiro 会话,而无需修改代码。

       9、单点登录的支持:shiro session 基于普通 java 对象,使得它更容易存储和共享,可以实现跨应用程序共享。可以根据共享的会话,来保证认证状态到另一个程序。从而实现单点登录。

session 相关 api :

       与 web 中的 HttpServletRequest.getSession(boolean create) 类似,Subject.getSession(true) 表示如果当前没有 session 对象就会创建一个;Subject.getSession(false), 表示如果当前没有 session 对象就返回 null 。默认情况下为 true

Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();

      获取到 session 对象后,就可以调用下面的 api 方法:

返回值方法名描述
ObjectgetAttribute(Object key)根据 key 标识返回绑定到 session 的对象
CollectiongetAttributeKeys()获取在 session 中存储的所有的 key
StringgetHost()获取当前主机 ip 地址,如果未知,返回 null
SerializablegetId()获取 session 的唯一 id
DategetLastAccessTime()获取最后的访问时间
DategetStartTimestamp()获取 session 的启动时间
longgetTimeout()获取 session 失效时间,单位毫秒
voidsetTimeout(long maxIdleTimeInMillis)设置 session 的失效时间,单位毫秒
ObjectremoveAttribute(Object key)通过 key 移除 session 中绑定的对象
voidsetAttribute(Object key, Object value)设置 session 会话属性
voidstop()销毁会话
voidtouch()更新会话最后访问时间

会话管理器 SessionManager:

       会话管理器 SessionManager 管理着应用中所有 Subject 会话创建维护删除失效验证等工作。是 Shiro 的核心组件,顶层组件 SecurityManager 直接继承了SessionManager,且提供了 SessionsSecurityManager 实现,直接把会话管理委托给相应的 SessionManager ,如:DefaultSecurityManager 和 DefaultWebSecurityManager 。

       SecurityManager 接口提供了以下的方法:

// 启动会话
Session start(SessionContext context);

// 根据会话 key 获取会话
Session getSession(SessionKey key) throws SessionException;

       另外,用于 web 环境的 WebSessionManager 接口又提供了如下方法:

// 是否使用Servlet容器的会话 
boolean isServletContainerSessions();

       最后,shiro 还提供了 ValidatingSessionManager 接口用于验资并过期会话的方法:

// 验证会话是否过期
void validateSessions();

        SessionManager 所有的关联关系如下所示(如果看不清,右键保存图片就可以看清):

从上图可以看出,Shiro 提供了三个默认的实现类。

       DefaultSessionManagerDefaultSecurityManager 使用的默认实现,用于 javase 环境。

       DefaultWebSessionManager:用于 web 环境的实现,可以替代 ServletContainerSessionManager,自己维护会话,直接废弃了 Servlet 容器的会话管理。

       ServletContainerSessionManagerDefaultWebSecurityManager 使用的默认实现,用于 web 环境,其直接使用 Servlet 容器的会话。

shiro 配置会话管理:

创建 session 监听类:

       我们首先创建 session 的监听类 ShiroSessionListener ,需要实现 SessionListener 接口,代码如下所示:

public class ShiroSessionListener implements SessionListener{

	/**
     * 统计在线人数
     * juc包下线程安全自增
     */
	private final AtomicInteger sessionCount = new AtomicInteger(0);
	
	@Override
	public void onStart(Session session) {
		// 会话创建,在线人数加一
        sessionCount.incrementAndGet();
		
	}
	/**
     * 退出会话时触发
     * @param session
     */
	@Override
	public void onStop(Session session) {
		 // 会话退出,在线人数减一
        sessionCount.decrementAndGet();
	}
	/**
     * 会话过期时触发
     * @param session
     */
	@Override
	public void onExpiration(Session session) {
		// 会话过期,在线人数减一
        sessionCount.decrementAndGet();
	}
	/**
     * 获取在线人数使用
     * @return
     */
    public AtomicInteger getSessionCount() {
        return sessionCount;
    }
}

配置ShiroConfig:

        需要在 ShiroConfig 中配置 session 监听 ShiroSessionListener ,然后配置会话 ID 生成器 SessionIdGenerator 用于生成会话 ID ,配置 sessionDAO,配置 sessionId cookie ,配置 session 管理器 SessionManager ,如下所示:

    /**
	 * 配置session监听
	 * @return
	 */
	@Bean("sessionListener")
	public ShiroSessionListener sessionListener(){
	    ShiroSessionListener sessionListener = new ShiroSessionListener();
	    return sessionListener;
	}
	/**
	 * 配置会话ID生成器
	 * @return
	 */
	@Bean
	public SessionIdGenerator sessionIdGenerator() {
	    return new JavaUuidSessionIdGenerator();
	}
	/**
	 * SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件
	 * MemorySessionDAO 直接在内存中进行会话维护
	 * EnterpriseCacheSessionDAO  提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
	 * @return
	 */
	@Bean
	public SessionDAO sessionDAO() {
	    EnterpriseCacheSessionDAO enterpriseCacheSessionDAO = new EnterpriseCacheSessionDAO();
	    // 使用ehCacheManager
	    enterpriseCacheSessionDAO.setCacheManager(ehCacheManager());
	    // 设置session缓存的名字 默认为 shiro-activeSessionCache
	    enterpriseCacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
	    //sessionId生成器
	    enterpriseCacheSessionDAO.setSessionIdGenerator(sessionIdGenerator());
	    return enterpriseCacheSessionDAO;
	}
	/**
	 * 配置保存sessionId的cookie 
	 * 注意:这里的cookie 不是上面的记住我 cookie 记住我需要一个cookie session管理 也需要自己的cookie
	 * @return
	 */
	@Bean("sessionIdCookie")
	public SimpleCookie sessionIdCookie(){
	    // 这个参数是cookie的名称
	    SimpleCookie simpleCookie = new SimpleCookie("sid");
	    // setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:
	    // 设为true后,只能通过http访问,javascript无法访问, 防止xss读取cookie
	    simpleCookie.setHttpOnly(true);
	    simpleCookie.setPath("/");
	    // maxAge=-1表示浏览器关闭时失效此Cookie
	    simpleCookie.setMaxAge(-1);
	    return simpleCookie;
	}
	/**
	 * 配置会话管理器,设定会话超时及保存
	 * @return
	 */
	@Bean("sessionManager")
	public SessionManager sessionManager() {

	    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
		// 为了解决输入网址地址栏出现 jsessionid 的问题
		sessionManager.setSessionIdUrlRewritingEnabled(false);
	    Collection<SessionListener> listeners = new ArrayList<SessionListener>();
	    // 配置监听
	    listeners.add(sessionListener());
	    sessionManager.setSessionListeners(listeners);
	    sessionManager.setSessionIdCookie(sessionIdCookie());
	    sessionManager.setSessionDAO(sessionDAO());
	    sessionManager.setCacheManager(ehCacheManager());
	    return sessionManager;

	}

测试:

       配置完成之后启动测试,登陆的时候点击 “记住我” 并查看 cookie ,可以看到一个 sessionId  和一个记住我 cookie ,如下所示:

清除 session 

       以上整合会话管理时还需要考虑一个问题,如果用户不点击注销,而是直接关闭浏览器,这样就无法进行 session清理工作,所以为了防止这样的问题出现,还需要增加一个会话的验证调度。

       修改 ShiroConfig 类中的 sessionManager() 方法设定会话超时时间,代码如下所示:

    /**
	 * 配置会话管理器,设定会话超时及保存
	 * @return
	 */
	@Bean("sessionManager")
	public SessionManager sessionManager() {

	    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
		// 为了解决输入网址地址栏出现 jsessionid 的问题
		sessionManager.setSessionIdUrlRewritingEnabled(false);
	    Collection<SessionListener> listeners = new ArrayList<SessionListener>();
	    //配置监听
	    listeners.add(sessionListener());
	    sessionManager.setSessionListeners(listeners);
	    sessionManager.setSessionIdCookie(sessionIdCookie());
	    sessionManager.setSessionDAO(sessionDAO());
	    sessionManager.setCacheManager(ehCacheManager());

	    // 全局会话超时时间(单位毫秒),默认30分钟  暂时设置为10秒钟 用来测试
	    // sessionManager.setGlobalSessionTimeout(10000);
	    sessionManager.setGlobalSessionTimeout(1800000);
	    // 是否开启删除无效的session对象  默认为true
	    sessionManager.setDeleteInvalidSessions(true);
	    // 是否开启定时调度器进行检测过期session 默认为true
	    sessionManager.setSessionValidationSchedulerEnabled(true);
	    // 设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
	    // 设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
	    // 暂时设置为 5秒 用来测试
	    sessionManager.setSessionValidationInterval(3600000);
	    // sessionManager.setSessionValidationInterval(5000);
	
	    return sessionManager;
	}

注意:

       到此为止设置完成了 session 的管理,但是,当用户在前端调用 ajax 的请求时,无法将此时 session 过期的状态返回给界面,所以下面还需要加监听。

 编写拦截器:

       需要编写一个拦截器 ClearSessionCacheFilter 来拦截用户的 ajax 请求,若当前的 session 处于超时状态,则给他设置 session-status 为 timeout ,然后在 js 文件里面做一个校验即可。

public class ClearSessionCacheFilter implements Filter{
 
	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		
	}
 
	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String basePath = request.getContextPath();
        request.setAttribute("basePath", basePath);
        // 判断 session 里是否有用户信息
        if (!SecurityUtils.getSubject().isAuthenticated()) {
        	 // 如果是ajax请求响应头会有,x-requested-with
            if (request.getHeader("x-requested-with") != null
                    && request.getHeader("x-requested-with").equalsIgnoreCase("XMLHttpRequest")) {
        		// 在响应头设置session状态
                response.setHeader("session-status", "timeout");
                return;
            }
        }
        filterChain.doFilter(request, servletResponse);
	}
 
	@Override
	public void destroy() {
		
	}
}

配置 ShiroConfig :

       需要将上面新增的 ClearSessionCacheFilter 配置到 ShiroConfig 中,代码如下所示:

    /**
	 * 校验当前缓存是否失效的拦截器
	 * 
	 * */
	@Bean
	public ClearSessionCacheFilter clearSessionCacheFilter() {
		ClearSessionCacheFilter clearSessionCacheFilter = new ClearSessionCacheFilter();
		return clearSessionCacheFilter;
	}
 
    // Filter工厂,设置对应的过滤条件和跳转条件
	@Bean
	public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
		ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
		// Shiro的核心安全接口,这个属性是必须的
		shiroFilter.setSecurityManager(securityManager);	
	
		//不输入地址的话会自动寻找项目web项目的根目录下的/page/login.jsp页面。
		shiroFilter.setLoginUrl("/login");
		//登录成功默认跳转页面,不配置则跳转至”/”。如果登陆前点击的一个需要登录的页面,则在登录自动跳转到那个需要登录的页面。不跳转到此。
		//shiroFilter.setSuccessUrl("/");
		
		// 自定义拦截器处理session过期的操作
	    LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();	  
	    filtersMap.put("clearSession", SystemFilter());
	    shiroFilter.setFilters(filtersMap);
	    
		//没有权限默认跳转的页面
		//shiroFilter.setUnauthorizedUrl("");
		
		//filterChainDefinitions的配置顺序为自上而下,以最上面的为准
		//shiroFilter.setFilterChainDefinitions("");
        // Shiro验证URL时,URL匹配成功便不再继续匹配查找(所以要注意配置文件中的URL顺序,尤其在使用通配符时),配置不会被拦截的链接 顺序判断
		Map<String, String> map = new LinkedHashMap<>();
		
		// 不能对login方法进行拦截,若进行拦截的话,这辈子都登录不上去了,这个login是LoginController里面登录校验的方法
		map.put("/login", "anon"); 
		map.put("/static/**", "anon");
		//map.put("/", "anon");
		//对所有用户认证
		map.put("/**", "clearSession,authc");
		
		shiroFilter.setFilterChainDefinitionMap(map);
		return shiroFilter;
	}

 前端代码:

       在我们平常的项目中,一般都会引入一些公共的 js 文件,在里面添加一些常用的工具方法或者常量,这时我们需要在 js 文件中添加一个 ajax 的父级方法 $.ajaxSetup ,用于拦截ajax 返回后的数据,用于校验当前的 session 是否处于过期状态,若处于过期状态则跳转到首页,代码如下所示:

function addRoleIds(){
 
	$.ajax({
		url :"addRoleIds",
		data : {"userName" : "lisi"} ,
		async:false,
		type : "get",
		success : function(data) {
			alert(data);
		}
	})
}
 
function delRoleIds(){
 
	$.ajax({
		url :"delRoleIds",
		data : {"userName" : "zhangsan"} ,
		async:false,
		type : "get",
		success : function(data) {
			alert(data);
		},
 
	})
}
$.ajaxSetup({
	complete:function(XMLHttpRequest,textStatus){
		var sessionstatus=XMLHttpRequest.getResponseHeader("session-status");
		if(sessionstatus == "timeout"){
			alert("会话超时,请重新登录!")
			//如果超时就处理 ,指定要跳转的页面
			window.location.href= "/login";
		}		
	}
});

测试:

       设置 session 的超时时间为 10000 毫秒,设置 session 的失效扫描时间为 5000 毫秒,重启项目,用张三的账号登录成功后,日志每 5 秒打印一次,打印结果如下:

       可以发现我们的这次 session 被停止掉了, 即 session 过期了,再点击连接跳转到首页,后台报错 提示找不到 session 。

添加 session 缓存:

       在 ehcache-shiro.xml 文件中添加 session 的缓存属性,代码如下所示:

<!-- session缓存 -->
<cache name="shiro-activeSessionCache"
       maxEntriesLocalHeap="2000"
       eternal="false"
       timeToIdleSeconds="0"
       timeToLiveSeconds="0"
       overflowToDisk="false"
       statistics="true">
</cache>

       这里一定要注意设置缓存的过期时间,还有 setGlobalSessionTimeout 的值,如果任意一个时间设置的比较短的话,session 就会从 ehcache 中清除,到时候就会报错。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐