写在前面

1.在上一篇帖子 Shiro一些补充 中提到过Shiro可以使用Shiro自己的Session或者自定义的Session来代替HttpSession

2.Redis/Jedis参考我写的 http://sgq0085.iteye.com/category/317384 一系列内容

一. SessionDao

配置在sessionManager中,可选项,如果不修改默认使用MemorySessionDAO,即在本机内存中操作。

如果想通过Redis管理Session,从这里入手。只需要实现类似DAO接口的CRUD即可。

经过1:最开始通过继承AbstractSessionDAO实现,发现doReadSession方法调用过于频繁,所以改为通过集成CachingSessionDAO来实现。

注意,本地缓存通过EhCache实现,失效时间一定要远小于Redis失效时间,这样本地失效后,会访问Redis读取,并重新设置Redis上会话数据的过期时间。

因为Jedis API KEY和Value相同,同为String或同为byte[]为了方便扩展下面的方法

package com.gqshao.authentication.utils;

import com.google.common.collect.Lists;

import org.apache.commons.lang3.SerializationUtils;

import org.apache.shiro.codec.Base64;

import org.apache.shiro.session.Session;

import java.io.Serializable;

import java.util.Collection;

import java.util.List;

public class SerializeUtils extends SerializationUtils {

public static String serializeToString(Serializable obj) {

try {

byte[] value = serialize(obj);

return Base64.encodeToString(value);

} catch (Exception e) {

throw new RuntimeException("serialize session error", e);

}

}

public static Session deserializeFromString(String base64) {

try {

byte[] objectData = Base64.decode(base64);

return deserialize(objectData);

} catch (Exception e) {

throw new RuntimeException("deserialize session error", e);

}

}

public static Collection deserializeFromStringController(Collection base64s) {

try {

List list = Lists.newLinkedList();

for (String base64 : base64s) {

byte[] objectData = Base64.decode(base64);

T t = deserialize(objectData);

list.add(t);

}

return list;

} catch (Exception e) {

throw new RuntimeException("deserialize session error", e);

}

}

}

我的Dao实现,ShiroSession是我自己实现的,原因在后面说明,默认使用的是SimpleSession

package com.gqshao.authentication.dao;

import com.gqshao.authentication.session.ShiroSession;

import com.gqshao.authentication.utils.SerializeUtils;

import com.gqshao.redis.component.JedisUtils;

import org.apache.commons.lang3.StringUtils;

import org.apache.shiro.cache.Cache;

import org.apache.shiro.session.Session;

import org.apache.shiro.session.UnknownSessionException;

import org.apache.shiro.session.mgt.ValidatingSession;

import org.apache.shiro.session.mgt.eis.CachingSessionDAO;

import org.apache.shiro.subject.support.DefaultSubjectContext;

import org.apache.shiro.util.CollectionUtils;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import redis.clients.jedis.Jedis;

import redis.clients.jedis.Transaction;

import java.io.Serializable;

import java.util.Collection;

import java.util.List;

import java.util.Set;

/**

* 针对自定义的ShiroSession的Redis CRUD操作,通过isChanged标识符,确定是否需要调用Update方法

* 通过配置securityManager在属性cacheManager查找从缓存中查找Session是否存在,如果找不到才调用下面方法

* Shiro内部相应的组件(DefaultSecurityManager)会自动检测相应的对象(如Realm)是否实现了CacheManagerAware并自动注入相应的CacheManager。

*/

public class CachingShiroSessionDao extends CachingSessionDAO {

private static final Logger logger = LoggerFactory.getLogger(CachingShiroSessionDao.class);

// 保存到Redis中key的前缀 prefix+sessionId

private String prefix = "";

// 设置会话的过期时间

private int seconds = 0;

@Autowired

private JedisUtils jedisUtils;

/**

* 重写CachingSessionDAO中readSession方法,如果Session中没有登陆信息就调用doReadSession方法从Redis中重读

*/

@Override

public Session readSession(Serializable sessionId) throws UnknownSessionException {

Session session = getCachedSession(sessionId);

if (session == null

|| session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {

session = this.doReadSession(sessionId);

if (session == null) {

throw new UnknownSessionException("There is no session with id [" + sessionId + "]");

} else {

// 缓存

cache(session, session.getId());

}

}

return session;

}

/**

* 根据会话ID获取会话

*

* @param sessionId 会话ID

* @return ShiroSession

*/

@Override

protected Session doReadSession(Serializable sessionId) {

Session session = null;

Jedis jedis = null;

try {

jedis = jedisUtils.getResource();

String key = prefix + sessionId;

String value = jedis.get(key);

if (StringUtils.isNotBlank(value)) {

session = SerializeUtils.deserializeFromString(value);

logger.info("sessionId {} ttl {}: ", sessionId, jedis.ttl(key));

// 重置Redis中缓存过期时间

jedis.expire(key, seconds);

logger.info("sessionId {} name {} 被读取", sessionId, session.getClass().getName());

}

} catch (Exception e) {

logger.warn("读取Session失败", e);

} finally {

jedisUtils.returnResource(jedis);

}

return session;

}

public Session doReadSessionWithoutExpire(Serializable sessionId) {

Session session = null;

Jedis jedis = null;

try {

jedis = jedisUtils.getResource();

String key = prefix + sessionId;

String value = jedis.get(key);

if (StringUtils.isNotBlank(value)) {

session = SerializeUtils.deserializeFromString(value);

}

} catch (Exception e) {

logger.warn("读取Session失败", e);

} finally {

jedisUtils.returnResource(jedis);

}

return session;

}

/**

* 如DefaultSessionManager在创建完session后会调用该方法;

* 如保存到关系数据库/文件系统/NoSQL数据库;即可以实现会话的持久化;

* 返回会话ID;主要此处返回的ID.equals(session.getId());

*/

@Override

protected Serializable doCreate(Session session) {

// 创建一个Id并设置给Session

Serializable sessionId = this.generateSessionId(session);

assignSessionId(session, sessionId);

Jedis jedis = null;

try {

jedis = jedisUtils.getResource();

// session由Redis缓存失效决定,这里只是简单标识

session.setTimeout(seconds);

jedis.setex(prefix + sessionId, seconds, SerializeUtils.serializeToString((ShiroSession) session));

logger.info("sessionId {} name {} 被创建", sessionId, session.getClass().getName());

} catch (Exception e) {

logger.warn("创建Session失败", e);

} finally {

jedisUtils.returnResource(jedis);

}

return sessionId;

}

/**

* 更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用

*/

@Override

protected void doUpdate(Session session) {

//如果会话过期/停止 没必要再更新了

try {

if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {

return;

}

} catch (Exception e) {

logger.error("ValidatingSession error");

}

Jedis jedis = null;

try {

if (session instanceof ShiroSession) {

// 如果没有主要字段(除lastAccessTime以外其他字段)发生改变

ShiroSession ss = (ShiroSession) session;

if (!ss.isChanged()) {

return;

}

Transaction tx = null;

try {

jedis = jedisUtils.getResource();

// 开启事务

tx = jedis.multi();

ss.setChanged(false);

tx.setex(prefix + session.getId(), seconds, SerializeUtils.serializeToString(ss));

logger.info("sessionId {} name {} 被更新", session.getId(), session.getClass().getName());

// 执行事务

tx.exec();

} catch (Exception e) {

if (tx != null) {

// 取消执行事务

tx.discard();

}

throw e;

}

} else if (session instanceof Serializable) {

jedis = jedisUtils.getResource();

jedis.setex(prefix + session.getId(), seconds, SerializeUtils.serializeToString((Serializable) session));

logger.info("sessionId {} name {} 作为非ShiroSession对象被更新, ", session.getId(), session.getClass().getName());

} else {

logger.warn("sessionId {} name {} 不能被序列化 更新失败", session.getId(), session.getClass().getName());

}

} catch (Exception e) {

logger.warn("更新Session失败", e);

} finally {

jedisUtils.returnResource(jedis);

}

}

/**

* 删除会话;当会话过期/会话停止(如用户退出时)会调用

*/

@Override

protected void doDelete(Session session) {

Jedis jedis = null;

try {

jedis = jedisUtils.getResource();

jedis.del(prefix + session.getId());

logger.debug("Session {} 被删除", session.getId());

} catch (Exception e) {

logger.warn("修改Session失败", e);

} finally {

jedisUtils.returnResource(jedis);

}

}

/**

* 删除cache中缓存的Session

*/

public void uncache(Serializable sessionId) {

Session session = this.readSession(sessionId);

super.uncache(session);

logger.info("取消session {} 的缓存", sessionId);

}

/**

* 获取当前所有活跃用户,如果用户量多此方法影响性能

*/

@Override

public Collection getActiveSessions() {

Jedis jedis = null;

try {

jedis = jedisUtils.getResource();

Set keys = jedis.keys(prefix + "*");

if (CollectionUtils.isEmpty(keys)) {

return null;

}

List valueList = jedis.mget(keys.toArray(new String[0]));

return SerializeUtils.deserializeFromStringController(valueList);

} catch (Exception e) {

logger.warn("统计Session信息失败", e);

} finally {

jedisUtils.returnResource(jedis);

}

return null;

}

public void setPrefix(String prefix) {

this.prefix = prefix;

}

public void setSeconds(int seconds) {

this.seconds = seconds;

}

}

二.Session和SessionFactory

步骤2:经过上面的开发已经可以使用的,但发现每次访问都会多次调用SessionDAO的doUpdate方法,来更新Redis上数据,过来发现更新的字段只有LastAccessTime(最后一次访问时间),由于会话失效是由Redis数据过期实现的,这个字段意义不大,为了减少对Redis的访问,降低网络压力,实现自己的Session,在SimpleSession上套一层,增加一个标识位,如果Session除lastAccessTime意外其它字段修改,就标识一下,只有标识为修改的才可以通过doUpdate访问Redis,否则直接返回。这也是上面SessionDao中doUpdate中逻辑判断的意义

package com.gqshao.authentication.session;

import org.apache.shiro.session.mgt.SimpleSession;

import java.io.Serializable;

import java.util.Date;

import java.util.Map;

/**

* 由于SimpleSession lastAccessTime更改后也会调用SessionDao update方法,

* 增加标识位,如果只是更新lastAccessTime SessionDao update方法直接返回

*/

public class ShiroSession extends SimpleSession implements Serializable {

// 除lastAccessTime以外其他字段发生改变时为true

private boolean isChanged;

public ShiroSession() {

super();

this.setChanged(true);

}

public ShiroSession(String host) {

super(host);

this.setChanged(true);

}

@Override

public void setId(Serializable id) {

super.setId(id);

this.setChanged(true);

}

@Override

public void setStopTimestamp(Date stopTimestamp) {

super.setStopTimestamp(stopTimestamp);

this.setChanged(true);

}

@Override

public void setExpired(boolean expired) {

super.setExpired(expired);

this.setChanged(true);

}

@Override

public void setTimeout(long timeout) {

super.setTimeout(timeout);

this.setChanged(true);

}

@Override

public void setHost(String host) {

super.setHost(host);

this.setChanged(true);

}

@Override

public void setAttributes(Map attributes) {

super.setAttributes(attributes);

this.setChanged(true);

}

@Override

public void setAttribute(Object key, Object value) {

super.setAttribute(key, value);

this.setChanged(true);

}

@Override

public Object removeAttribute(Object key) {

this.setChanged(true);

return super.removeAttribute(key);

}

/**

* 停止

*/

@Override

public void stop() {

super.stop();

this.setChanged(true);

}

/**

* 设置过期

*/

@Override

protected void expire() {

this.stop();

this.setExpired(true);

}

public boolean isChanged() {

return isChanged;

}

public void setChanged(boolean isChanged) {

this.isChanged = isChanged;

}

@Override

public boolean equals(Object obj) {

return super.equals(obj);

}

@Override

protected boolean onEquals(SimpleSession ss) {

return super.onEquals(ss);

}

@Override

public int hashCode() {

return super.hashCode();

}

@Override

public String toString() {

return super.toString();

}

}

package com.gqshao.authentication.session;

import org.apache.shiro.session.Session;

import org.apache.shiro.session.mgt.SessionContext;

import org.apache.shiro.session.mgt.SessionFactory;

public class ShiroSessionFactory implements SessionFactory {

@Override

public Session createSession(SessionContext initData) {

ShiroSession session = new ShiroSession();

return session;

}

}

三.SessionListener

步骤3:发现用户推出后,Session没有从Redis中销毁,虽然当前重新new了一个,但会对统计带来干扰,通过SessionListener解决这个问题

package com.gqshao.authentication.listener;

import com.gqshao.authentication.dao.CachingShiroSessionDao;

import org.apache.shiro.session.Session;

import org.apache.shiro.session.SessionListener;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

public class ShiroSessionListener implements SessionListener {

private static final Logger logger = LoggerFactory.getLogger(ShiroSessionListener.class);

@Autowired

private CachingShiroSessionDao sessionDao;

@Override

public void onStart(Session session) {

// 会话创建时触发

logger.info("ShiroSessionListener session {} 被创建", session.getId());

}

@Override

public void onStop(Session session) {

sessionDao.delete(session);

// 会话被停止时触发

logger.info("ShiroSessionListener session {} 被销毁", session.getId());

}

@Override

public void onExpiration(Session session) {

sessionDao.delete(session);

//会话过期时触发

logger.info("ShiroSessionListener session {} 过期", session.getId());

}

}

四.将账号信息放到Session中

修改realm中AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)方法,在返回AuthenticationInfo之前添加下面的代码,把用户信息放到Session中

// 把账号信息放到Session中,并更新缓存,用于会话管理

Subject subject = SecurityUtils.getSubject();

Serializable sessionId = subject.getSession().getId();

ShiroSession session = (ShiroSession) sessionDao.doReadSessionWithoutExpire(sessionId);

session.setAttribute("userId", su.getId());

session.setAttribute("loginName", su.getLoginName());

sessionDao.update(session);

五. 配置文件

xmlns:util="http://www.springframework.org/schema/util"

xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd

http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd">

Shiro安全配置

/login = authc

/logout = logout

/static/** = anon

/** = user

depends-on="lifecycleBeanPostProcessor">

maxElementsInMemory="10000"

eternal="false"

timeToLiveSeconds="60"

overflowToDisk="false"

diskPersistent="false"

diskExpiryThreadIntervalSeconds="10"

/>

六.测试会话管理

package com.gqshao.authentication.controller;

import com.gqshao.authentication.dao.CachingShiroSessionDao;

import org.apache.shiro.session.Session;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import java.io.Serializable;

import java.util.Collection;

@Controller

@RequestMapping("/session")

public class SessionController {

@Autowired

private CachingShiroSessionDao sessionDao;

@RequestMapping("/active")

@ResponseBody

public Collection getActiveSessions() {

return sessionDao.getActiveSessions();

}

@RequestMapping("/read")

@ResponseBody

public Session readSession(Serializable sessionId) {

return sessionDao.doReadSessionWithoutExpire(sessionId);

}

}

七.集群情况下的改造

1.问题上面启用了Redis中央缓存、EhCache本地JVM缓存,AuthorizingRealm的doGetAuthenticationInfo登陆认证方法返回的AuthenticationInfo,默认情况下会被保存到Session的Attribute下面两个字段中

org.apache.shiro.subject.support.DefaultSubjectContext.PRINCIPALS_SESSION_KEY 保存 principal

org.apache.shiro.subject.support.DefaultSubjectContext.AUTHENTICATED_SESSION_KEY 保存 boolean是否登陆

然后在每次请求过程中,在ShiroFilter中组装Subject时,读取Session中这两个字段

现在的问题是Session被缓存到本地JVM堆中,也就是说服务器A登陆,无法修改服务器B的EhCache中Session属性,导致服务器B没有登陆。

处理方法有很多思路,比如重写CachingSessionDAO,readSession如果没有这两个属性就不缓存(没登陆就不缓存),或者cache的session没有这两个属性就调用自己实现的doReadSession方法从Redis中重读一下。

/**

* 重写CachingSessionDAO中readSession方法,如果Session中没有登陆信息就调用doReadSession方法从Redis中重读

*/

@Override

public Session readSession(Serializable sessionId) throws UnknownSessionException {

Session session = getCachedSession(sessionId);

if (session == null

|| session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {

session = this.doReadSession(sessionId);

if (session == null) {

throw new UnknownSessionException("There is no session with id [" + sessionId + "]");

} else {

// 缓存

cache(session, session.getId());

}

}

return session;

}

2.如果需要保持各个服务器Session是完全同步的,可以通过Redis消息订阅/发布功能,再调用SessionDao中实现了删除Session本地缓存的方法

redis插件连接集群 shiro_Shiro经过Redis管理会话实现集群(转载)相关推荐

  1. 使用IDEA的Redis插件连接Redis服务器

    IDEA中的Redis插件作为Redis的可视化工具,可以通过该插件连接Redis服务器,并并进行增删改查操作. 一.IDEA安装Redis插件 1.点击 File->Setting 2.点击P ...

  2. redis远程连接不上

    解决redis远程连接不上的问题 redis现在的版本开启redis-server后,redis-cli只能访问到127.0.0.1,因为在配置文件中固定了ip,因此需要修改redis.conf(有的 ...

  3. 阿里云redis公网连接

    阿里云redis公网连接 前提条件 拥有一个Redis实例和ECS实例,并且ECS与Redis实例同属于一个vpc 部署步骤 在Redis实例的白名单中加入ECS服务器私网地址 在ECS服务器的安全组 ...

  4. Redis实战:第五章-使用Redis构建支持程序

    本章主要讲解redis的使用案例,相对于以往的技术,redis在这些领域将大大简化或者提高程序的便利和稳定.比如日志记录,相对于以往的文件记录方式将更加灵活,便于数据操作, 日志记录 以往的日志记录采 ...

  5. Redis高可用解决方案:sentinel(哨兵模式)和集群

    一. redis高可用方案–sentinel(哨兵模式) 当我们搭建好redis主从复制方案后会发现一个问题,那就是当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力, ...

  6. 常用服务的集群搭建(redis、MQ、es、zookerper)

    常用服务的集群搭建(redis.MQ.es.zookerper) 1.集群概述 1.1什么是集群 1.1.1集群概念 集群是一种计算机系统, 它通过一组松散集成的计算机软件和/或硬件连接起来高度紧密地 ...

  7. Redis数据库搭建集群(集群概念、redis集群、搭建集群(配置机器1、2、创建集群、数据操作验证)、Python与redis集群交互)

    1. 集群的概念 集群是一组相互独立的.通过高速网络互联的计算机,它们构成了一个组,并以单一系统的模式加以管理.一个客户与集群相互作用时,集群像是一个独立的服务器.集群配置是用于提高可用性和可缩放性. ...

  8. redis指定配置文件启动_深入学习 Redis 集群搭建方案及实现原理

    " 在前面的文章中已经介绍了 本文将详细介绍集群,主要内容包括: 集群的作用 集群的搭建方法及设计方案 集群的基本原理 客户端访问集群的方法 实践须知(集群伸缩.故障转移.参数优化等) 集群 ...

  9. Redis进阶-5.x 单节点 及Redis Cluster 3主3从集群部署

    文章目录 Redis 下载地址 Redis 5.x 单节点 编译安装 Redis 启停 Redis Cluster 4.x VS Redis Cluster 5.x 演进之路 ( Master/Sla ...

最新文章

  1. C#进阶系列——动态Lamada
  2. boost::mp11::mp_is_map相关用法的测试程序
  3. keras用cpu加速_在训练某些网络时,Keras(Tensorflow后端)在GPU上比在CPU上慢CPU
  4. mysql修改忘记了root密码忘记了,mysql忘记root密码后,重新设置、修改root密码
  5. scrollView的几个属性contentSize contentOffset contentInset
  6. fragment photoshop_史上最接地气的Photoshop?谈PS 2021的黑科技 - Adobe
  7. 荷兰铁路在采纳敏捷和精益中的做法
  8. google chrome 历史版本下载
  9. 思科ASDM导入与实战配置经验总结-操作系统版本V842
  10. 方舟单机/管理员生物指令代码大全
  11. pycharm中TODO注释
  12. php漂浮广告代码,JS随机漂浮广告代码具体实例
  13. CentOS 8.5下安装R语言经验总结
  14. ajax中怎样获取下拉列表,如何从Ajax/Jquery的下拉列表中获取复选框中的数据
  15. 数据分析需要掌握的知识(2)
  16. 浅谈SEO的优化问题
  17. 半波对称振子方向图_对称振子阵列天线.ppt
  18. 05-SA8155 QNX Hypervisor 之 Pass-through直通模式
  19. 迪酷CS1.6 3248 版本选择角色或进游戏闪退的解决办法!
  20. 软件开发可行性分析九个流程的理解

热门文章

  1. Oracle存储过程中跳出循环的写法
  2. 把16进制值转换成颜色颜色16进制值表 .
  3. android camera分辨率设置,请问如何使用camera2设置全屏preview,要求适配所有屏幕尺寸?...
  4. RabbitMq 本地连接报错 org.springframework.amqp.AmqpIOException: java.io.IOException
  5. java io流(字符流) 文件打开、读取文件、关闭文件
  6. MYSQL(04)-间隙锁详解
  7. mysql分布式数据库中间件对比
  8. 为PHP安装Memcached扩展连接Memcached
  9. Go的RESTful
  10. 基于h5的跳一跳游戏的开发与实现_「南宁小程序开发」企业开发小程序有哪些好处?...