springboot使用shiro框架基于redis实现session共享

什么是Apache Shiro?

Apache Shiro是一个功能强大且易于使用的Java安全框架,为开发人员提供了一个直观而全面的解决方案,用于身份验证、授权、加密和会话管理。
由于本篇主要解决shiro管理下session共享问题,shiro基本功能和用法就不赘述了.

Shiro的架构

shiro架构图

session共享主要自定义 session Manager ,session DAO , cacheManager 和 cache来实现

  • sessionManager : shiro 会话的管理器,网络会话使用WebSessionManager
  • sessionDao: 存取会话的DAO层实现
  • cacheManager: 缓存管理器
  • cache: 负责缓存存取的DAO层实现

自定义类的意义

sessionManager和sessionDao

由于sessionManager负责管理sessionDao ,想要自定义 sessionDao 必须先自定义 sessionManager
sessionDao是实现session共享的核心,通过自定义该类,来实现用redis管理session

cacheManager和cahe

cacheManager和cahe和上面两者的关系基本相同,前者是管理器,后者是dao层实现;
shiro框架对身份认证和权限认证有缓存功能,在配置realm时可以设置是否开启此功能(realm主要就是负责身份认证和权限认证的工作),由于realm的功能原因,基本都要去数据库或网络IO,所有使用缓存是十分有用的,在这里我们也用redis进行缓存的管理;

结合配置分析

话不多说上配置文件

我们使用spirng注解方式来配置shiro

  • SecurityManager

往大的将融合redis就是改造SecurityManager,通过setCacheManager()和setSessionManager()方法分别设置缓存管理器和会话管理器,配置如下:

    @Bean("securityManager")
    public DefaultWebSecurityManager getManager() {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(getRealm());
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);
        //以上部分未做改动,重点在下面两方法

        //redis缓存和session共享
        if("y".equals(redis)){
            // 设置缓存,该缓存使用不使用,由realm的配置决定
            System.out.println("使用redis进行session共享");
            manager.setCacheManager(getRedisCacheManager());
            manager.setSessionManager(getDefaultWebSessionManager());
        }
        return manager;
    }

将该类设置进ShiroFilterFactoryBean中

  • 会话管理器

  • SessionManager:
    @Bean(name = "sessionManager")
    public DefaultWebSessionManager getDefaultWebSessionManager() {
        //自定义sessionManager为了解决浏览器访问时网址后面添加;SESSIONID=xxxx的问题
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(){
            //参考https://blog.csdn.net/yyf314922957/article/details/51038322
            @Override
            protected void onStart(Session session, SessionContext context) {
                super.onStart(session, context);
                ServletRequest request = WebUtils.getRequest(context);
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
            }
        };
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionDAO(getRedisSessionDao());
        sessionManager.setSessionIdCookie(getSimpleCookie());
        //相隔多久检查一次session的有效性
        sessionManager.setSessionValidationInterval(validate);
        //session 有效期
        sessionManager.setGlobalSessionTimeout(timeout);
        return sessionManager;
    }
  • SessionDao
    //配置基于redis的SessionDao
    @Bean("redisSessionDao")
    public MyRedisSessionDao getRedisSessionDao() {
        MyRedisSessionDao sessionDAO = new MyRedisSessionDao();
        return sessionDAO;
    }

  • SimpleCookie
    可以注意到SessionManager通过setSessionIdCookie()方法设置了个SimpleCookie,该类解决nginx分发访问时不同tomcat的sessionID不一致导致获取不到session的问题
    //解决nginx分发时出现There is no session with id ....的session不一致,找不到的问题
    @Bean("simpleCookie")
    public SimpleCookie getSimpleCookie (){
        SimpleCookie simpleCookie = new SimpleCookie(SHIRO_SESSION);
        simpleCookie.setPath("/");
        return simpleCookie;
    }
  • 缓存管理器

  • CacheManager
    而cache多例不需要交由spring管理,只需配置CacheManager
    //配置缓存
    @Bean("redisCacheManager")
    public MyRedisCacheManager getRedisCacheManager() {
        MyRedisCacheManager redisCacheManager = new MyRedisCacheManager();
        return redisCacheManager;
    }

各类实现代码

由以上配置可以看出需要实现的类其实只有三个,分别为SessionDao,CacheManager,Cache.代码如下:

  • SessionDao



import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.Serializable;
import java.util.*;

public class MyRedisSessionDao extends AbstractSessionDAO {
    private Object lock = new Object();
    public static final String SESSION_KEY = "session_key";

    //基于RedisTemplate集成工具类
    @Autowired
    private RedisUtil redisUtil;
    //删除session,退出时会调用到该方法
    @Override
    public void delete(Session session) {
        if(session == null || session.getId() == null){
            System.out.println("Session is null");
            return;
        }
        redisUtil.hdel(SESSION_KEY,session.getId().toString());
    }
    //获得sessin集合
    @Override
    public Collection<Session> getActiveSessions() {

        List<Session> list = new ArrayList<>();
        List<Object> objects = redisUtil.hgetList(SESSION_KEY);
        for (Object object : objects) {
            Session session = null;
            Base64 base64 = new Base64();
            try {
                byte[] decode = base64.decode((String) object);
                session = (Session) JSONUtil.unSerialize(decode);
            } catch (Exception e) {
                e.printStackTrace();
            }
            list.add(session);
        }
        return list;
    }
    //更新session
    @Override
    public void update(Session session) throws UnknownSessionException {
        this.saveSession(session);
    }
    private void saveSession(Session session){
        if(session == null || session.getId() == null){
            System.out.println("Session is null");
            return;
        }
        //添加进redis
        Base64 base64 = new Base64();
        String sessionStr = base64.encodeToString(JSONUtil.serialize(session));
        redisUtil.hset(SESSION_KEY,session.getId().toString(),sessionStr);
    }
     //shiro框架创建session的方法,shiro通过doReadSession得到空的session得知当前会话还没有session就会去创建session
    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = generateSessionId(session);
        assignSessionId(session, sessionId);
        //添加进redis
        this.saveSession(session);
        return sessionId;
    }
    //获取shiro框架session的方法
    @Override
    protected Session doReadSession(Serializable sessionId) {
        Session session = null;
        String sessionStr = (String) redisUtil.hget(SESSION_KEY, sessionId.toString());
        if(StringUtils.isNotEmpty(sessionStr)){
            Base64 base64 = new Base64();
                try {
                    synchronized (lock){
                        byte[] decode = base64.decode(sessionStr);
                        session = (Session) JSONUtil.unSerialize(decode);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
        }
        System.out.println("get sessionId:  "+sessionId + "session:  "+ session);
        return session;
    }
}
说明
  • RedisUtil
    这个类基于RedisTemplate集成的工具类,对RedisTemplate的简单封装,主要功能就是基于redis对数据进行增删改查,由于本篇关注点不在此,就不展开了.如何配置RedisTemplate请自行百度.
  • Base64
    为什么要使用Base64转码呢?说来话长,首先是这个Session实现类的问题,shiro对session的默认实现类是org.apache.shiro.session.mgt.SimpleSession这个类.我使用RedisTemplate<String,Object>来进行存取时(内部使用jackson来实现object与string相互转转换,redis只能存储字符串),会报错如下图:


    jackson报错

    究其原因,可能是SimpleSession中有个isValid()的方法,却没有valid字段,导致jackson创建类时报错.
    解决这个问题的方法有很多种,我决定绕过jackson,自己实现session与string互相转换,于是使用java的对象序列化,转换成字节数组,再用base64转换成字符串,如上面代码的saveSession()方法,其中JSONUtil的相关代码如下:

    /**
     序列化对象
     @param object
     @return
     */
    public static byte[] serialize(Object object) {
        ObjectOutputStream oos = null;
        ByteArrayOutputStream baos = null;
        try {
            if (object != null){
                baos = new ByteArrayOutputStream();
                oos = new ObjectOutputStream(baos);
                oos.writeObject(object);
                return baos.toByteArray();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    /**

     反序列化对象
     @param bytes
     @return
     */
    public static  Object unSerialize(byte[] bytes) throws Exception{
        ByteArrayInputStream bais = null;

            if (bytes != null && bytes.length > 0){
                bais = new ByteArrayInputStream(bytes);
                ObjectInputStream ois = new ObjectInputStream(bais);
                return ois.readObject();
            }
        return null;
    }

本以为万事大吉却还是报错,错误的大体问题是在doReadSession()方法中unSerialize反序列化时出现错误,内部原因暂时不知.但经过排查,主要是由于doReadSession存在着并发调用,在并发情况下java对象流处理时会出错!?本人并没有做更多的测试,同时百度无果.于是就加了个锁解决并发报错的问题,希望有大佬能解答这个疑惑!

ps

为什么不选则其他JSON工具类而使用java序列化呢?
答:实在是束手无策,诸如fastjson,gson都会报错,SimpleSession这个类真滴有毒,恨不得自定义,但是根据时下流行奥卡姆剃刀原则,还是惰性一点好,搞不好又是一堆bug.
而这个类的一些属性被transient关键字所修饰,如下图:


SimpleSession

最大的问题就是这个类,剩下的两个类就简单了;

  • CacheManager

import com.zoan.jdcbj.utils.RedisUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class MyRedisCacheManager implements CacheManager {
    @Autowired
    private RedisUtil redisUtil;
    private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<>();
    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        Cache cache = caches.get(name);
        if (cache == null) {
            cache = new RedisCache(redisUtil);
            caches.put(name, cache);
        }
        return cache;
    }
}

内部存在一个map来存储Cache

  • Cache

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.*;

public class RedisCache<K, V> implements Cache<K, V> {
    private RedisUtil redisUtil;
    private RedisTemplate<String, Object> redisTemplate;

    public RedisCache(RedisUtil redisUtil) {
        this.redisUtil = redisUtil;
    }

    private static final String CACHE_KEY = "cache_key";

    @Override
    public V get(K key) throws CacheException {
        return (V) redisUtil.hget(CACHE_KEY,key.toString());
    }

    @Override
    public V put(K key, V value) throws CacheException {
        redisUtil.hset(CACHE_KEY,key.toString(),value);
        return value;
    }

    @Override
    public V remove(K key) throws CacheException {
        Object value = redisUtil.hget(CACHE_KEY, key.toString());
        redisUtil.hdel(CACHE_KEY,key.toString());
        return (V) value;
    }

    @Override
    public void clear() throws CacheException {
        Set<K> keys = keys();
        for (K key : keys) {
            redisUtil.hdel(CACHE_KEY,key.toString());
        }
    }
    @Override
    public int size() {
        return keys().size();
    }
    @Override
    public Set<K> keys() {
        Set<Object> objects = redisUtil.hgetKeys(CACHE_KEY);
        Set<K> keys  = new HashSet<>();
        for (Object object : objects) {
            keys.add((K) object);
        }
        return keys;
    }
    @Override
    public Collection<V> values() {
        List<Object> objects = redisUtil.hgetList(CACHE_KEY);
        List<V> list = new ArrayList<>();
        for (Object object : objects) {
           list.add((V)object);
        }
        return list;
    }
}

顾名思义,不做赘述;

其他

  • nginx的配置没什么特别之处,就不展示了;

总结

其实整合的过程并不难,需要理解的概念也很简单,但是问题在于坑实在有点多,主要体现在:

  1. simpleSession使用redis存取时对象和字符串转换问题
  2. nginx分发时session不一致问题

这两个问题结合shiro框架会引发许多bug值得注意;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343