如今的系统多不是孤军奋战,在多结点会话共享管理方面有着各自的解决办法,比如Session粘连,基于Web容器的各种处理等或者类似本文说的完全接管Web容器的Session管理,只是做法不尽相同。

而本文说的是Apache-Shiro+Zookeeper来解决多结点会话管理,Shiro一个优秀的权限框架,有着很好的扩展性,而Zookeeper更是让你激动不已的多功能分布式协调系统,在本例中就用它来做Shiro的会话持久容器!

在有过Shiro和Zookeeper开发后这一切都非常容易理解,实现过程如下:

用到的框架技术:

Spring + Shiro + Zookeeper

第一步:配置WEB.XML

<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

第二步:SHIRO整合SPRING配置

applicationContext-shiro.xml 伪代码:

<!--Session集群配置-->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="3600000"/>
<property name="sessionDAO" ref="zkShiroSessionDAO"/>
<property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
<property name="sessionValidationSchedulerEnabled" value="true"/>
<property name="sessionIdCookie" ref="wapsession"/>
</bean> <!--
指定本系统SESSIONID, 默认为: JSESSIONID
问题: 与SERVLET容器名冲突, 如JETTY, TOMCAT 等默认JSESSIONID,
当跳出SHIRO SERVLET时如ERROR-PAGE容器会为JSESSIONID重新分配值导致登录会话丢失!
-->
<bean id="wapsession" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg name="name" value="WAPSESSIONID"/>
</bean> <!--
定时清理僵尸session,Shiro会启用一个后台守护线程定时执行清理操作
用户直接关闭浏览器造成的孤立会话
-->
<bean id="sessionValidationScheduler"
class="org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler">
<property name="interval" value="3600000"/>
<property name="sessionManager" ref="sessionManager"/>
</bean> <!--由zk做session存储容器-->
<bean id="zkShiroSessionDAO" class="b2gonline.incometaxexamine._systembase.shiro.ZKShiroSessionDAO">
<!--使用内存缓存登录用户信息,一次获取用户登录信息后缓存到内存减少Shiro大量的读取操作,用户退出或超时后自动清除-->
<constructor-arg name="useMemCache" value="true"/>
<property name="zookeeperTemplate" ref="zookeeperTemplate"/>
<property name="shiroSessionZKPath" value="/SHIROSESSIONS"/>
<property name="sessionPrefix" value="session-"/>
</bean> <!-- SHIRO安全接口 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
...
<property name="sessionManager" ref="sessionManager"/>
</bean>

第三步:Zookeeper对Shiro-SessionDao实现类

ZKShiroSessionDAO.JAVA伪代码:

import bgonline.foundation.hadoop.zk.IZookeeperTemplate;
import bgonline.foundation.hadoop.zk.ZNode;
import org.apache.shiro.cache.AbstractCacheManager;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.MapCache;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.SerializationUtils; import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; /**
* ZOOKEEPER实现SHIRO集群SESSION存储
*
* @author aliencode
* @date 13-7-10
*/
public class ZKShiroSessionDAO extends CachingSessionDAO { public ZKShiroSessionDAO() {
} private boolean useMemCache = false; /**
* SESSION ZK DAO 实例
* 如果开户缓存
* 用户登录时自动缓存, 用户登录超时自动删除
* 由于shiro的cacheManager是全局的, 所以这里使用setActiveSessionsCache直接设置Cache来本地缓存, 而不使用全局zk缓存.
* 由于同一用户可能会被路由到不同服务器,所以在doReadSession方法里也做了缓存增加.
*
* @param useMemCache 是否使用内存缓存登录信息
*/
public ZKShiroSessionDAO(boolean useMemCache) {
this.useMemCache = useMemCache;
if (useMemCache) {
setActiveSessionsCache(
new MapCache<>(this.ACTIVE_SESSION_CACHE_NAME, new ConcurrentHashMap<Serializable, Session>())
);
}
} Logger logger = LoggerFactory.getLogger(this.getClass()); /**
* ZK操作类
*/
private IZookeeperTemplate zookeeperTemplate; /**
* 缓存根路径, 结尾不加/
*/
private String shiroSessionZKPath = "/SHIROSESSIONS"; /**
* 缓存项前缀
*/
private String sessionPrefix = "session-"; /**
* 设置Shiro Session 前缀 默认 session-
*
* @param sessionPrefix
*/
public void setSessionPrefix(String sessionPrefix) {
this.sessionPrefix = sessionPrefix;
} public void setZookeeperTemplate(IZookeeperTemplate zookeeperTemplate) {
this.zookeeperTemplate = zookeeperTemplate;
} /**
* 设置Shiro在ZK服务器存放根路径
*
* @param shiroSessionZKPath 默认值:/SHIROSESSIONS/
*/
public void setShiroSessionZKPath(String shiroSessionZKPath) {
this.shiroSessionZKPath = shiroSessionZKPath;
} /**
* session更新
*
* @param session
* @throws UnknownSessionException
*/
@Override
public void update(Session session) throws UnknownSessionException {
if (session == null || session.getId() == null) {
logger.error("session argument cannot be null.");
}
saveSession(session, "update");
} @Override
protected void doUpdate(Session session) {
} /**
* session删除
*
* @param session
*/
@Override
public void delete(Session session) {
if (session == null || session.getId() == null) {
logger.error("session argument cannot be null.");
}
logger.debug("delete session for id: {}", session.getId());
zookeeperTemplate.deleteNode(getPath(session.getId()));
if (useMemCache) {
this.uncache(session);
}
} @Override
protected void doDelete(Session session) {
} /**
* 获取当前活跃的session, 当前在线数量
*
* @return
*/
@Override
public Collection<Session> getActiveSessions() {
ZNode zNode = new ZNode();
zNode.setPath(shiroSessionZKPath);
Set<Session> sessions = new HashSet<Session>();
//读取所有SessionID , 返回形如: session-9e3b5707-fa80-4d32-a6c9-f1c3685263a5
List<String> ss = zookeeperTemplate.getChildren(zNode);
for (String id : ss) {
if (id.startsWith(sessionPrefix)) {
String noPrefixId = id.replace(sessionPrefix, "");
Session session = doReadSession(noPrefixId);
if (session != null) sessions.add(session);
}
}
logger.debug("shiro getActiveSessions. size: {}", sessions.size());
return sessions;
} /**
* 创建session, 用户登录
*
* @param session
* @return
*/
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
saveSession(session, "create");
return sessionId;
} /**
* session读取
*
* @param id
* @return
*/
@Override
protected Session doReadSession(Serializable id) {
if (id == null) {
logger.error("id is null!");
return null;
}
logger.debug("doReadSession for path: {}", getPath(id)); Session session;
byte[] byteData = zookeeperTemplate.getData(getPath(id)).getByteData();
if (byteData != null && byteData.length > 0) {
session = (Session) SerializationUtils.deserialize(byteData);
if (useMemCache) {
this.cache(session, id);
logger.debug("doReadSession for path: {}, add cached !", getPath(id));
}
return session;
} else {
return null;
}
} /**
* 生成全路径
*
* @param sessID
* @return
*/
private String getPath(Serializable sessID) {
return shiroSessionZKPath + '/' + sessionPrefix + sessID.toString();
} /**
* session读取或更新
*
* @param session
* @param act update/save
*/
private void saveSession(Session session, String act) {
Serializable sessionId = session.getId();
ZNode sessionNode = new ZNode();
sessionNode.setByteData(SerializationUtils.serialize(session));
sessionNode.setPath(getPath(sessionId));
logger.debug("save session for id: {}, act: {}", sessionId, act);
if (act == "update")
zookeeperTemplate.setData(sessionNode);
else
zookeeperTemplate.createNode(sessionNode);
} }

小结

本文主要给出会话管理的实现过程和部分核心代码,并且说到并解决了在使用Shiro开发时会遇到的几个关键问题和心得,如

Shiro默认的JSESSIONID和WEB容器同名冲突,这个如果使用默认开发时当访问404等错误页面由WEb容器直接处理并由生成新的JSESSIONID使得Shiro退出;

SESSION会话缓存,这个借鉴EnterpriseCacheSessionDAO,由于Shiro在访问每个链接时都会读取一次Session,所以在用户成功登录后把Session存储并缓存到内存或本地以减少大量读取操作;

孤立会话的清除,当用户直接关闭浏览器会有Session孤立于储存容器中,配置ExecutorServiceSessionValidationScheduler定时清理!

下一篇,有关Shiro的缓存共享管理。

完!

Apache-Shiro+Zookeeper系统集群安全解决方案之会话管理的更多相关文章

  1. Apache-Shiro+Zookeeper系统集群安全解决方案之缓存管理

    上篇[Apache-Shiro+Zookeeper系统集群安全解决方案之会话管理],解决了Shiro在系统集群开发时安全的会话共享问题,系统在使用过程中会有大量的权限检查和用户身份检验动作,为了不频繁 ...

  2. 一键运行CIS安全扫描,集群安全无忧!

    CIS安全扫描是Rancher 2.4推出的其中一个重磅功能,旨在帮助用户快速.有效地加强集群的安全性.本文将详细介绍CIS安全扫描这一功能,包含详细的操作demo. 本文来自Rancher Labs ...

  3. shiro注解,初始化资源和权限,会话管理

     有具体问题的可以参考之前的关于shiro的博文,关于shiro的博文均是一次工程的内容  注解: 新建一个类: 此时需要有admin的权限才可以执行下面的代码 public class ShiroS ...

  4. mongodb副本集加分片集群安全认证使用账号密码登录

    mongodb副本集加分片集群搭建网上资料有很多.粘贴一个写的比较好的.副本集加分片搭建 对于搭建好的mongodb副本集加分片集群,为了安全,启动安全认证,使用账号密码登录. 默认的mongodb是 ...

  5. kafka集群安全化之启用kerberos与acl

    一.背景 在我们部署完kafka之后,虽然我们已经可以“肆意”的用kafka了,但是在一个大公司的实际生产环境中,kafka集群往往十分庞大,每个使用者都应该只关心自己所负责的Topic,并且对其他人 ...

  6. kubernetes实战(八):k8s集群安全机制RBAC

    1.基本概念 RBAC(Role-Based Access Control,基于角色的访问控制)在k8s v1.5中引入,在v1.6版本时升级为Beta版本,并成为kubeadm安装方式下的默认选项, ...

  7. Kubernetes集群安全概述

    API的访问安全性 API Server的端口和地址 在默认情况下,API Server通过本地端口和安全端口两个不同的HTTP端口,对外提供API服务,其中本地端口是基于HTTP协议的,用于在本机( ...

  8. kubernetes(k8s)集群安全机制RBAC

    1.基本概念 RBAC(Role-Based Access Control,基于角色的访问控制)在k8s v1.5中引入,在v1.6版本时升级为Beta版本,并成为kubeadm安装方式下的默认选项, ...

  9. elasticsearch集群安全重启节点

    es2.x 关闭集群的动态分片:(动态分片开启状态如果节点宕机了,会导致集群重新分配数据进行数据转移,会导致节点直接大量传输数据)curl -XPUT 'http://192.168.248.193: ...

随机推荐

  1. Delphi内嵌汇编语言BASM精要(转帖)

    1 BASM概念简要  汇编语句由指令和零至三个表达式构成.表达式由常数(立即数).寄存器和标识符构成.例如: movsb        // 单指令语句 jmp @Here    // 一个表达式: ...

  2. 10 个迅速提升你 Git 水平的提示【转】

    转自:https://www.oschina.net/translate/10-tips-git-next-level 最近我们推出了两个教程:熟悉Git的基本功能和让你在开发团队中熟练的使用Git  ...

  3. NLog

    C# 使用NLog记录日志 C#第三方日志库Nlog NLog类库使用探索——详解配置 使用Nlog记录日志到数据库 NLog文章系列——系列文章目录以及简要介绍 NLog日志框架简写用法(write ...

  4. php number_format()保留小数点后几位

    [PHP_保留两位小数的相关函数] php保留两位小数并且四舍五入 Php代码   1     $num = 123213.666666;  2     echo sprintf("%.2f ...

  5. 对js原型的理解

    1.值类型和引用类型,引用类型都是对象,通过typeof()测试类型,instanceof测试是否是对象.对象是属性的集合. 2.对象都是由函数创建的,函数又是一个对象. 3.函数有一个默认的属性,叫 ...

  6. 跟我一起Django - 01 安装和初始化

    跟我一起Django - 01 安装和初始化 python环境 C:\Python27\Lib\site-packages>python --versionPython 2.7.5 安装setu ...

  7. C/C++ 调用qsort/sort 对字符数组排序的cmp函数写法

    这个问题屡次碰到解决之后再次遇到又忘记怎么弄了,这次打算彻底搞清楚. ·C 首先对所谓字符数组的排序应该是对(char)*a[]数组而非(char)a[][]进行的排序,后者是无法直接调用qsort实 ...

  8. awk用法小结(作者总结)

    http://www.chinaunix.net/old_jh/24/691456.html http://wenku.baidu.com/view/ebac4fc658f5f61fb736664d. ...

  9. 项目文件中含有两个config文件,app.config与app1.config,如何获取app1.config中的配置

    想要通过配置文件配置C#前台画面,好奇做了以下测试:在项目中新建了app.config与app1.config两个配置文件,请教一下各位高手如果想从app1.config中读取配置信息应该如何读取?采 ...

  10. flask安装

    Flask简介 Flask算是小型框架,自开发伊始就被设计为可扩展的框架,它具有一个包含基本服务的强健核心.Flask有两个依赖:路由.调试.和web服务器网关接口(Web Server Gatewa ...