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

2019-07-25  本文已影响0人  mrfly520

什么是Apache Shiro?

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

Shiro的架构

shiro架构图

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

自定义类的意义

sessionManager和sessionDao

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

cacheManager和cahe

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

结合配置分析

话不多说上配置文件

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

往大的将融合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中

    @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;
    }
    //配置基于redis的SessionDao
    @Bean("redisSessionDao")
    public MyRedisSessionDao getRedisSessionDao() {
        MyRedisSessionDao sessionDAO = new MyRedisSessionDao();
        return sessionDAO;
    }

    //解决nginx分发时出现There is no session with id ....的session不一致,找不到的问题
    @Bean("simpleCookie")
    public SimpleCookie getSimpleCookie (){
        SimpleCookie simpleCookie = new SimpleCookie(SHIRO_SESSION);
        simpleCookie.setPath("/");
        return simpleCookie;
    }
    //配置缓存
    @Bean("redisCacheManager")
    public MyRedisCacheManager getRedisCacheManager() {
        MyRedisCacheManager redisCacheManager = new MyRedisCacheManager();
        return redisCacheManager;
    }

各类实现代码

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



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;
    }
}
说明
    /**
     序列化对象
     @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

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

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

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;
    }
}

顾名思义,不做赘述;

其他

总结

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

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

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

上一篇下一篇

猜你喜欢

热点阅读