美文网首页
springboot使用shiro框架基于redis实现sess

springboot使用shiro框架基于redis实现sess

作者: mrfly520 | 来源:发表于2019-07-25 17:19 被阅读0次

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

    相关文章

      网友评论

          本文标题:springboot使用shiro框架基于redis实现sess

          本文链接:https://www.haomeiwen.com/subject/euixrctx.html