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值得注意;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。