博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Shiro通过Redis管理会话实现集群
阅读量:4047 次
发布时间:2019-05-25

本文共 20293 字,大约阅读时间需要 67 分钟。

流程概要说明

1.Servlet容器在用户浏览器首次访问后会产生Session,并将Session的ID保存到Cookie中(浏览器不同key不一定相同),同时Shiro会将该Session缓存到Redis中; 

 

2.用户登录认证成功后Shiro会修改Session属性,添加用户认证成功标识,并同步修改Redis中Session;

 

3.用户发起请求后,Shiro会先判断本地EhCache缓存中是否存在该Session,如果有,直接从本地EhCache缓存中读取,如果没有再从Redis中读取Session,并在此时判断Session是否认证通过,如果认证通过将该Session缓存到本地EhCache中; 

 

4.如果Session发生改变,或被删除(用户退出登录),先对Redis中Session做相应修改(修改或删除);再通过Redis消息通道发布缓存失效消息,通知其它节点EhCache失效。

1.S

写在前面

1.在上一篇帖子  中提到过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[]为了方便扩展下面的方法

 

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 
T 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

import com.genertech.adp.web.common.utils.SerializeUtils;import com.genertech.adp.web.sys.authentication.component.ShiroSession;import com.genertech.adp.web.sys.redis.component.JedisUtils;import org.apache.commons.lang3.StringUtils;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.joda.time.DateTime;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 ShiroSessionDao extends CachingSessionDAO {    private static final Logger logger = LoggerFactory.getLogger(ShiroSessionDao.class);    // 保存到Redis中key的前缀 prefix+sessionId    private String prefix = "";    // 设置会话的过期时间    private int seconds = 0;    // 特殊配置 只用于没有Redis时 将Session放到EhCache中    private Boolean onlyEhCache;    @Autowired    private JedisUtils jedisUtils;    /**     * 重写CachingSessionDAO中readSession方法,如果Session中没有登陆信息就调用doReadSession方法从Redis中重读     *///    @Override    public Session readSession(Serializable sessionId) throws UnknownSessionException {        Session cached = null;        try {            cached = super.getCachedSession(sessionId);        } catch (Exception e) {            e.printStackTrace();        }        if (onlyEhCache) {            return cached;        }        // 如果缓存不存在或者缓存中没有登陆认证后记录的信息就重新从Redis中读取        if (cached == null || cached.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {            try {                cached = this.doReadSession(sessionId);                if (cached == null) {                    throw new UnknownSessionException();                } else {                    // 重置Redis中缓存过期时间并缓存起来 只有设置change才能更改最后一次访问时间                    ((ShiroSession) cached).setChanged(true);                    super.update(cached);                }            } catch (Exception e) {                logger.warn("There is no session with id [" + sessionId + "]");            }        }        return cached;    }    /**     * 从Redis中读取Session,并重置过期时间     *     * @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("shiro session id {} 被读取", sessionId);            }        } catch (Exception e) {            logger.warn("读取Session失败", e);        } finally {            jedisUtils.returnResource(jedis);        }        return session;    }    /**     * 从Redis中读取,但不重置Redis中缓存过期时间     */    public Session doReadSessionWithoutExpire(Serializable sessionId) {        if (onlyEhCache) {            return readSession(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);        if (onlyEhCache) {            return sessionId;        }        Jedis jedis = null;        try {            jedis = jedisUtils.getResource();            // session由Redis缓存失效决定,这里只是简单标识            session.setTimeout(seconds);            jedis.setex(prefix + sessionId, seconds, SerializeUtils.serializeToString((ShiroSession) session));            logger.info("shiro session id {} 被创建", sessionId);        } 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");        }        if (onlyEhCache) {            return;        }        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);                    ss.setLastAccessTime(DateTime.now().toDate());                    tx.setex(prefix + session.getId(), seconds, SerializeUtils.serializeToString(ss));                    logger.info("shiro session id {} 被更新", 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("ID {} classname {} 作为非ShiroSession对象被更新, ", session.getId(), session.getClass().getName());            } else {                logger.info("ID {} classname {} 不能被序列化 更新失败", session.getId(), session.getClass().getName());            }        } catch (Exception e) {            logger.warn("更新Session失败", e);        } finally {            jedisUtils.returnResource(jedis);        }    }    /**     * 删除会话;当会话过期/会话停止(如用户退出时)会调用     */    @Override    public void doDelete(Session session) {        Jedis jedis = null;        try {            jedis = jedisUtils.getResource();            jedis.del(prefix + session.getId());            logger.info("shiro session id {} 被删除", session.getId());        } catch (Exception e) {            logger.warn("删除Session失败", e);        } finally {            jedisUtils.returnResource(jedis);        }    }    /**     * 删除cache中缓存的Session     */    public void uncache(Serializable sessionId) {        try {            Session session = super.getCachedSession(sessionId);            super.uncache(session);            logger.info("shiro session id {} 的缓存失效", sessionId);        } catch (Exception e) {            e.printStackTrace();        }    }    /**     * 获取当前所有活跃用户,如果用户量多此方法影响性能     */    @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; } /** * 返回本机Ehcache中Session */ public Collection
getEhCacheActiveSessions() { return super.getActiveSessions(); } public void setPrefix(String prefix) { this.prefix = prefix; } public void setSeconds(int seconds) { this.seconds = seconds; } public void setOnlyEhCache(Boolean onlyEhCache) { this.onlyEhCache = onlyEhCache; }}

 

 

 

 

二.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.commons.lang3.StringUtils;import org.apache.shiro.session.Session;import org.apache.shiro.session.mgt.SessionContext;import org.apache.shiro.session.mgt.SessionFactory;import org.apache.shiro.web.session.mgt.DefaultWebSessionContext;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import javax.servlet.http.HttpServletRequest;public class ShiroSessionFactory implements SessionFactory {    private static final Logger logger = LoggerFactory.getLogger(ShiroSessionFactory.class);    @Override    public Session createSession(SessionContext initData) {        ShiroSession session = new ShiroSession();        HttpServletRequest request = (HttpServletRequest)initData.get(DefaultWebSessionContext.class.getName() + ".SERVLET_REQUEST");        session.setHost(getIpAddress(request));        return session;    }    public static String getIpAddress(HttpServletRequest request) {        String localIP = "127.0.0.1";        String ip = request.getHeader("x-forwarded-for");        if (StringUtils.isBlank(ip) || (ip.equalsIgnoreCase(localIP)) || "unknown".equalsIgnoreCase(ip)) {            ip = request.getHeader("Proxy-Client-IP");        }        if (StringUtils.isBlank(ip) || (ip.equalsIgnoreCase(localIP)) || "unknown".equalsIgnoreCase(ip)) {            ip = request.getHeader("WL-Proxy-Client-IP");        }        if (StringUtils.isBlank(ip) || (ip.equalsIgnoreCase(localIP)) || "unknown".equalsIgnoreCase(ip)) {            ip = request.getRemoteAddr();        }        return ip;    }}

 

 

三.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);

 

  

五. 配置文件

 

Shiro安全配置
/login = authc /logout = logout /static/** = anon /** = user

 

 

 

六.测试会话管理

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 保存 principalorg.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中重读一下。

 

2.readSession中每次调用doReadSession方法的时候,都代表第一次读取,或本地EhCache失效,我们可以在这个时候调用一下updateSession方法,重新设置一下最后一次访问时间,当然要把isChange设置为true才会保存到Redis中。

 

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

 

 

 

 

转载地址:http://uqyci.baihongyu.com/

你可能感兴趣的文章
GNU hello代码分析
查看>>
Qt继电器控制板代码
查看>>
wpa_supplicant控制脚本
查看>>
gstreamer相关工具集合
查看>>
RS232 四入四出模块控制代码
查看>>
linux 驱动开发 头文件
查看>>
container_of()传入结构体中的成员,返回该结构体的首地址
查看>>
linux sfdisk partition
查看>>
ipconfig,ifconfig,iwconfig
查看>>
opensuse12.2 PL2303 minicom
查看>>
网络视频服务器移植
查看>>
Encoding Schemes
查看>>
移植QT
查看>>
如此调用
查看>>
计算机的发展史
查看>>
带WiringPi库的交叉编译如何处理一
查看>>
带WiringPi库的交叉笔译如何处理二之软链接概念
查看>>
Spring事务的七种传播行为
查看>>
ES写入找不到主节点问题排查
查看>>
Java8 HashMap集合解析
查看>>