springboot + shiro 配置session管理
提供了完整的企业级会话管理功能,不用依赖于底层容器(如等),不管是还是环境都可以使用,还提供了。即直接使用的会话管理可以直接替换如容器的会话管理。
背景:
shiro 提供了完整的企业级会话管理功能,不用依赖于底层容器(如 tomcat、weblogic 等),不管是 j2se 还是 j2ee 环境都可以使用,还提供了会话管理、会话事件监听、会话存储/持久化、容器无关的集群支持、失效/过期支持、对 web 的透明支持、sso 单点登录的支持等特性。即直接使用 shiro 的会话管理可以直接替换如 web 容器的会话管理。
shiro 中的 session 特性:
1、基于 pojo/j2se:shiro 中 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 方法:
返回值 | 方法名 | 描述 |
---|---|---|
Object | getAttribute(Object key) | 根据 key 标识返回绑定到 session 的对象 |
Collection | getAttributeKeys() | 获取在 session 中存储的所有的 key |
String | getHost() | 获取当前主机 ip 地址,如果未知,返回 null |
Serializable | getId() | 获取 session 的唯一 id |
Date | getLastAccessTime() | 获取最后的访问时间 |
Date | getStartTimestamp() | 获取 session 的启动时间 |
long | getTimeout() | 获取 session 失效时间,单位毫秒 |
void | setTimeout(long maxIdleTimeInMillis) | 设置 session 的失效时间,单位毫秒 |
Object | removeAttribute(Object key) | 通过 key 移除 session 中绑定的对象 |
void | setAttribute(Object key, Object value) | 设置 session 会话属性 |
void | stop() | 销毁会话 |
void | touch() | 更新会话最后访问时间 |
会话管理器 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 提供了三个默认的实现类。
DefaultSessionManager:DefaultSecurityManager 使用的默认实现,用于 javase 环境。
DefaultWebSessionManager:用于 web 环境的实现,可以替代 ServletContainerSessionManager,自己维护会话,直接废弃了 Servlet 容器的会话管理。
ServletContainerSessionManager:DefaultWebSecurityManager 使用的默认实现,用于 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 中清除,到时候就会报错。
更多推荐
所有评论(0)