spring - shiro - session交给redis托管

14 篇文章 0 订阅
1 篇文章 0 订阅

shiro自己也有session,但是我希望将这个session交给redis托管,因为如果系统采用分布式,登录信息只保存在一个节点是肯定不行的。仔细弄了几天,遇到几个坑,查了很多资料,终于走通了。现在做个笔记。有关shiro的认证与校验见https://blog.csdn.net/ws6afa88/article/details/109061610,shiro的session交给redis管理流程主要参考https://www.jianshu.com/p/5aa03c2d118e,但里面的坑很多。

0. ShiroDemo目录结构

在这里插入图片描述

1. 主要依赖

<!--整合shiro-->
 <dependency>
     <groupId>org.apache.shiro</groupId>
     <artifactId>shiro-spring</artifactId>
     <version>1.5.3</version>
 </dependency>
 <!--整合redis-->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>

2. shiro-config

@Configuration
public class ShiroConfig {

    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter() {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(getSecurityManager());

        //设置登录页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        filterChainDefinitionMap.put("/toLogin", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/index", "anon");

        //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截 剩余的都需要认证
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }


    //开启shiro的注解,主要在thymeleaf中使用
    @Bean(name = "shiroDialect")
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }


    @Bean
    public SecurityManager getSecurityManager() {
        DefaultSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
        defaultSecurityManager.setRealm(customRealm());
        //自定义SessionManager
        defaultSecurityManager.setSessionManager(getDefaultWebSessionManager());
        return defaultSecurityManager;
    }

	//相关定义不再赘述,见https://blog.csdn.net/ws6afa88/article/details/109061610
    @Bean  
    public CustomRealm customRealm() {
        CustomRealm customRealm = new CustomRealm();
        customRealm.setCredentialsMatcher(new MyCredentialsMatcher());
        return customRealm;
    }


    /**
     * *
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * *
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {

        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();

        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {

        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(getSecurityManager());
        return authorizationAttributeSourceAdvisor;
    }


    /**
     * Session Manager
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public DefaultWebSessionManager getDefaultWebSessionManager(){
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//        sessionManager.setGlobalSessionTimeout(10);//设置session过期时间 10ms
//        sessionManager.setSessionValidationInterval(1000);//定时查询所有session是否过期的时间
//        Collection<SessionListener> listeners
        ArrayList<SessionListener> list = new ArrayList<SessionListener>();
        list.add(new MySessionListener());
        list.add(new MySessionListener2());
        //定义了自定义了session的增删改查操作
        sessionManager.setSessionDAO(redisSessionDao());
        //自定义了cookie的存管理操作
        sessionManager.setSessionIdCookie(sessionIdCookie());
		//定义了两个session监听器
        sessionManager.setSessionListeners(list);
        return sessionManager;
    }



    /**
     * SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件 MemorySessionDAO 直接在内存中进行会话维护
     * EnterpriseCacheSessionDAO
     * 提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
     *
     * @return
     */
    @Bean
    public RedisSessionDao redisSessionDao() {
        RedisSessionDao redisSessionDao = new RedisSessionDao();

        return redisSessionDao;
    }

    /**
     * 配置保存sessionId的cookie 注意:这里的cookie 不是记住我 cookie 记住我需要一个cookie session管理
     * 也需要自己的cookie 默认为: JSESSIONID 问题: 与SERVLET容器名冲突,重新定义为
     *
     * @return
     */
    @Bean("sessionIdCookie")
    public SimpleCookie sessionIdCookie() {
        // 这个参数是cookie的名称
        SimpleCookie simpleCookie = new SimpleCookie("REDIS-SESSION");
        // setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:
        // setcookie()的第七个参数
        // 设为true后,只能通过http访问,javascript无法访问
        // 防止xss读取cookie
        simpleCookie.setHttpOnly(true);
        simpleCookie.setPath("/");
        // maxAge=-1表示浏览器关闭时失效此Cookie
        simpleCookie.setMaxAge(-1);
        return simpleCookie;
    }

}

3. 自定义RedisSessionDao

public class RedisSessionDao extends AbstractSessionDAO {

    @Autowired
    RedisUtil redisUtil;

    private final String SHIRO_SESSIOM_PREFIX = "shiro-session";

    private String getKey(String key){
        return (SHIRO_SESSIOM_PREFIX+key);
    }

    //创建sessionID
    @Override
    protected Serializable doCreate(Session session) {

        Serializable sessionId = generateSessionId(session);
        assignSessionId(session,sessionId);
        saveSession(session);

        System.out.println("创建了sessionID: "+sessionId);
        return sessionId;
    }

    //将字节ID反序列化为对象
    @Override
    protected Session doReadSession(Serializable sessionId) {
        if (sessionId == null) {
            return null;
        }
        String key = getKey(sessionId.toString());
        String value = (String)redisUtil.get(key);
        System.out.println("反序列化了sessionID: "+key);
        if(value == null){
            return null;
        }

        return (Session) SerializationUtils.deserialize(ByteArrayUtils.toByteArray(value));

    }


    //更新session
    @Override
    public void update(Session session) throws UnknownSessionException {
        System.out.println("更新了sessionID: "+session.getId());
        saveSession(session);

    }

    private void saveSession(Session session){
        if(session !=null&& session.getId()!=null) {
            String key = getKey(session.getId().toString());
            byte[] value = SerializationUtils.serialize(session);

            redisUtil.set(key.toString(), ByteArrayUtils.toHexString(value));
            redisUtil.expire(key.toString(), 600);
        }
    }

    //删除了session
    @Override
    public void delete(Session session) {
        if(session == null || session.getId() ==null){
            return;
        }
        String key = getKey(session.getId().toString());
        redisUtil.del(key.toString());
        System.out.println("删除了sessionID: "+session.getId());
    }

    @Override
    public Collection<Session> getActiveSessions() {

        System.out.println("getActiveSessions()=====");
        Set<String> keys = redisUtil.getKeys(SHIRO_SESSIOM_PREFIX);
        Set<Session> sessions = new HashSet<>();
        if(CollectionUtils.isEmpty(keys)){
            return sessions;
        }
        for(String key:keys){
            Session session = (Session)SerializationUtils.deserialize(((String)redisUtil.get(key)).getBytes());
            sessions.add(session);
        }
        return sessions;
    }


}

这里 SerializationUtils.deserialize(@Nullable byte[] bytes)进行反序列时,我遇到了一个大坑。由于 byte[]类型value被redis保存时,自动保存为Object,实际上还是保存为String,这时再取出value,将value转换成byte[]传给SerializationUtils.deserialize(@Nullable byte[] bytes),就会报反序列化错误:java.io.StreamCorruptedException: invalid stream header: EFBFBDEF,真是太坑爹了。后来发现是 byte[]类型value强转为Object,取出再转换回byte[]类型时会有问题。于是网上找到了这样的方法,先将byte[]类型value转换为16进制表示格式的字符串保存,取出时再将其转换回byte[]类型即可,这里主要参考https://blog.csdn.net/qq_34446485/article/details/81542691,用到的ByteArrayUtils定义如下:

import org.springframework.util.StringUtils;

public class ByteArrayUtils {
    /**
     * 字节数组转成16进制表示格式的字符串
     *
     * @param byteArray 需要转换的字节数组
     * @return 16进制表示格式的字符串
     **/
    public static String toHexString(byte[] byteArray) {
        if (byteArray == null || byteArray.length < 1)
            throw new IllegalArgumentException("this byteArray must not be null or empty");

        final StringBuilder hexString = new StringBuilder();
        for (int i = 0; i < byteArray.length; i++) {
            if ((byteArray[i] & 0xff) < 0x10)//0~F前面不零
                hexString.append("0");
            hexString.append(Integer.toHexString(0xFF & byteArray[i]));
        }
        return hexString.toString().toLowerCase();
    }
    public static byte[] toByteArray(String hexString) {
        if (StringUtils.isEmpty(hexString))
            throw new IllegalArgumentException("this hexString must not be empty");

        hexString = hexString.toLowerCase();
        final byte[] byteArray = new byte[hexString.length() / 2];
        int k = 0;
        for (int i = 0; i < byteArray.length; i++) {//因为是16进制,最多只会占用4位,转换成字节需要两个16进制的字符,高位在先
            byte high = (byte) (Character.digit(hexString.charAt(k), 16) & 0xff);
            byte low = (byte) (Character.digit(hexString.charAt(k + 1), 16) & 0xff);
            byteArray[i] = (byte) (high << 4 | low);
            k += 2;
        }
        return byteArray;
    }

}

我的RedisConfig定义如下:

@Configuration
public class RedisConfig {

    //编写我们自己的template
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {

        //我们为了自己开发方便,一般使用<String,Object>
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        //Json序列化配置
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om=new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        //String的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        //key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value的序列化采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //hash的value也采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

}

redisUtil中主要方法定义如下:

    public Object get(String key) {

        return key == null ? null : redisTemplate.opsForValue().get(key);

    }

    public boolean set(String key, Object value) {

        try {

            redisTemplate.opsForValue().set(key, value);

            return true;

        } catch (Exception e) {

            e.printStackTrace();

            return false;

        }

    }
    
    public boolean expire(String key, long time) {

        try {

            if (time > 0) {

                redisTemplate.expire(key, time, TimeUnit.SECONDS);

            }

            return true;

        } catch (Exception e) {

            e.printStackTrace();

            return false;
        }

    }
    //通过前缀模糊查询keys
    public Set<String> getKeys(String shiro_sessiom_prefix) {
        StringRedisTemplate sr=new StringRedisTemplate();
        Set<String> keys = sr.keys(shiro_sessiom_prefix + "*");
        return keys;

    }

4. 两个监听类

//监听类MySessionListener
public class MySessionListener implements SessionListener {
    private final AtomicInteger sessionCount = new AtomicInteger(0);

    MySessionListener(){
        System.out.println("MySessionListener注册进来了============");
    }

    @Override
    public void onStart(Session session) {
        sessionCount.incrementAndGet();
        System.out.println("登陆+1=="+sessionCount.get());


    }

    @Override
    public void onStop(Session session) {
        sessionCount.decrementAndGet();
        System.out.println("登陆-1=="+sessionCount.get());

    }

    @Override
    public void onExpiration(Session session) {
        sessionCount.decrementAndGet();
        System.out.println("登陆过期-1=="+sessionCount.get());

    }
}


//监听类MySessionListener2
public class MySessionListener2 extends SessionListenerAdapter {
    @Override
    public void onStart(Session session) {

        System.out.println("会话创建:" + session.getId());

    }
}

5.测试

登录后,后台显示:
在这里插入图片描述
再看redis中:
在这里插入图片描述
再看cookie:
在这里插入图片描述

6. 总结

搞了两天,反序列化过程因为一直报“java.io.StreamCorruptedException: invalid stream header: EFBFBDEF”,挺烦,还好最后解决了问题,也发现对序列化理解不够,这个坑得找个时间好好填一下。同时,我还发现登录后经常有两个sessionID,不是特别清楚,是不是因为页面跳转?还需要自己下去仔细研究。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值