解决Springboot整合Shiro自定义SessionDAO+Redis管理会话,登录后不跳转首页

解决Springboot整合Shiro自定义SessionDAO+Redis管理会话,登录后不跳转首页

  • 问题发现
  • 问题解决

问题发现

Shiro框架中,SessionDAO的默认实现是MemorySessionDAO。它内部维护了一个ConcurrentMap来保存session数据,即将session数据缓存在内存中。

再使用Redis作为Session存储解决分布式系统中的Session共享问题。

依赖文件如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.7.18</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.13.0</version>
</dependency>

示例代码如下:

@Controller
@RequestMapping(value = "/user")
public class UserController {

    @GetMapping("/index")
    public ModelAndView index() {
        Subject subject = SecurityUtils.getSubject();
        System.out.println("===============index==========");
        System.out.println(subject.getSession().getId());
        System.out.println(subject.isAuthenticated());
        if (subject.isAuthenticated() || subject.isRemembered()) {
            return new ModelAndView("redirect:main");
        }
        return new ModelAndView("login.html");
    }
    @PostMapping("/login")
    public ModelAndView login(HttpServletRequest request, @RequestParam("username") String username
            , @RequestParam("password") String password) {
        // 提前加密,解决自定义缓存匹配时错误
        UsernamePasswordToken token = new UsernamePasswordToken(
                username,//身份信息
                password);//凭证信息
        ModelAndView modelAndView = new ModelAndView();
        // 对用户信息进行身份认证
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated() && subject.isRemembered()) {
            modelAndView.setViewName("redirect:main");
            return modelAndView;
        }
        try {
            subject.login(token);
            // 判断savedRequest不为空时,获取上一次停留页面,进行跳转
            SavedRequest savedRequest = WebUtils.getSavedRequest(request);
            if (savedRequest != null) {
                String requestUrl = savedRequest.getRequestUrl();
                modelAndView.setViewName("redirect:"+ requestUrl);
                return modelAndView;
            }
        } catch (AuthenticationException e) {
            e.printStackTrace();
            modelAndView.addObject("responseMessage", "用户名或者密码错误");
            modelAndView.setViewName("redirect:index");
            return modelAndView;
        }
        System.out.println(subject.getSession().getId());
        System.out.println(subject.isAuthenticated());
        modelAndView.setViewName("redirect:main");
        return modelAndView;
    }

    @GetMapping("/main")
    public String main() {
        Subject subject = SecurityUtils.getSubject();
        System.out.println("===============main==========");
        System.out.println(subject.getSession().getId());
        System.out.println(subject.isAuthenticated());
        return "main.html";
    }
}

自定义SessionDAO,示例代码如下:

public class RedisSessionDao extends AbstractSessionDAO {
    private HashOperations<String, Object, Session> hashOperations;

    private static final String key = "shiro:";

    public RedisSessionDao(RedisTemplate<String, Object> redisTemplate) {
        hashOperations = redisTemplate.opsForHash();
    }

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = super.generateSessionId(session);
        this.assignSessionId(session, sessionId);
        this.storeSession(sessionId, session);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable serializable) {
        return (Session) hashOperations.get(key, serializable.toString());
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        this.storeSession(session.getId(), session);
    }

    @Override
    public void delete(Session session) {
        if (session == null) {
            throw new NullPointerException("session argument cannot be null.");
        } else {
            Serializable id = session.getId();
            if (id != null) {
                hashOperations.delete(key, id.toString());
            }

        }
    }

    @Override
    public Collection<Session> getActiveSessions() {
        return hashOperations.values(key);
    }

    protected void storeSession(Serializable id, Session session) {
        if (id == null) {
            throw new NullPointerException("id argument cannot be null.");
        } else {
            this.hashOperations.putIfAbsent(key, id.toString(), session);
        }
    }
}

Config配置文件示例代码如下:

@Configuration
public class ShiroConfig {
    /**
     * 核心安全过滤器对进入应用的请求进行拦截和过滤,从而实现认证、授权、会话管理等安全功能。
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 当未登录的用户尝试访问受保护的资源时,重定向到这个指定的登录页面。
        shiroFilterFactoryBean.setLoginUrl("/user/index");
        // 成功后跳转地址,但是测试时未生效
        shiroFilterFactoryBean.setSuccessUrl("/user/main");
        // 当用户访问没有权限的资源时,系统重定向到指定的URL地址。
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/user/login", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 创建Shiro Web应用的整体安全管理
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(realm());
        defaultWebSecurityManager.setSessionManager(defaultWebSessionManager()); // 注册会话管理
        // 可以添加其他配置,如缓存管理器、会话管理器等
        return defaultWebSecurityManager;
    }

    /**
     * 创建会话管理
     */
    @Bean
    public DefaultWebSessionManager defaultWebSessionManager() {
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        defaultWebSessionManager.setGlobalSessionTimeout(10000);
        defaultWebSessionManager.setSessionDAO(sessionDAO());
        defaultWebSessionManager.setCacheManager(cacheManager());
        return defaultWebSessionManager;
    }

    @Bean
    public SessionDAO sessionDAO() {
        RedisSessionDao redisSessionDao = new RedisSessionDao(redisTemplate());
        return redisSessionDao;
    }

    /**
     * 指定密码加密算法类型
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("SHA-256"); // 设置哈希算法
        return hashedCredentialsMatcher;
    }

    /**
     * 注册Realm的对象,用于执行安全相关的操作,如用户认证、权限查询
     */
    @Bean
    public Realm realm() {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher()); // 为realm设置指定算法
        userRealm.setCachingEnabled(true); // 启动全局缓存
        userRealm.setAuthorizationCachingEnabled(true); // 启动授权缓存
        userRealm.setAuthenticationCachingEnabled(true); // 启动验证缓存
        userRealm.setCacheManager(cacheManager());
        return userRealm;
    }

    @Bean
    public CacheManager cacheManager() {
        RedisCacheManage redisCacheManage = new RedisCacheManage(redisTemplate());
        return redisCacheManage;
    }

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
	// redis序列化配置
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        //设置了 ObjectMapper 的可见性规则。通过该设置,所有字段(包括 private、protected 和 package-visible 等)都将被序列化和反序列化,无论它们的可见性如何。
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //启用了默认的类型信息 NON_FINAL 参数表示只有非 final 类型的对象才包含类型信息,这可以帮助在反序列化时正确地将 JSON 字符串转换回对象。
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
        return redisTemplate;
    }
}

进入浏览器登陆成功后跳转首页,跳转过程中302,返回登录页面,如图所示:
在这里插入图片描述

问题解决

根据代码日志,可知道,跳转到其他页面时Session没有共享,如图所示:

在这里插入图片描述
最开始以为Redis中没有保存记录,其实已经保存了,如图所示:

在这里插入图片描述
参考网上诸多案例,似乎没什么区别,也不知道他们测过没有。

然后再Debug的时候,发现了另外一个类EnterpriseCacheSessionDAO,于是参考该类,我就把对应代码继承CachingSessionDAO,示例代码如下:

public class RedisSessionDao extends CachingSessionDAO {
    private HashOperations<String, Object, Session> hashOperations;

    protected Serializable doCreate(Session session) {
        Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, sessionId);
        return sessionId;
    }

    protected Session doReadSession(Serializable sessionId) {
        return null;
    }

    protected void doUpdate(Session session) {
    }

    protected void doDelete(Session session) {
    }
}

Config配置文件,示例代码如下:

    @Bean
    public DefaultWebSessionManager defaultWebSessionManager() {
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        defaultWebSessionManager.setGlobalSessionTimeout(10000);
        defaultWebSessionManager.setSessionDAO(sessionDAO());
        defaultWebSessionManager.setCacheManager(cacheManager());
        return defaultWebSessionManager;
    }
    @Bean
    public SessionDAO sessionDAO() {
        RedisSessionDao redisSessionDao = new RedisSessionDao();
        redisSessionDao.setCacheManager(cacheManager()); // 设置缓存管理器
        redisSessionDao.setActiveSessionsCacheName("shiro:session"); // 自定义redis存放的key名称
        return redisSessionDao;
    }

重启项目后运行,成功跳转,如图所示:

在这里插入图片描述
Redis中也有记录,如图所示:
在这里插入图片描述
至于继承AbstractSessionDAO为什么没有共享Session,大概率的原因是Redis没有被Shiro给管理导致的。

示例代码如下:

public class RedisSessionDao extends AbstractSessionDAO {
    private CacheManager cacheManager;
    private Cache<Serializable, Session> activeSessions;
    private static final String key = "shiro:";

    public RedisSessionDao() {
    }

    public void setCacheManager(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
        this.activeSessions = cacheManager.getCache(key);
    }

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = super.generateSessionId(session);
        this.assignSessionId(session, sessionId);
        this.storeSession(sessionId, session);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable serializable) {
        return (Session) activeSessions.get(serializable);
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        this.storeSession(session.getId(), session);
    }

    @Override
    public void delete(Session session) {
        if (session == null) {
            throw new NullPointerException("session argument cannot be null.");
        } else {
            Serializable id = session.getId();
            if (id != null) {
                activeSessions.remove(id);
            }

        }
    }

    @Override
    public Collection<Session> getActiveSessions() {
        return activeSessions.values();
    }

    protected void storeSession(Serializable id, Session session) {
        if (id == null) {
            throw new NullPointerException("id argument cannot be null.");
        } else {
            activeSessions.put(id, session);
        }
    }
}

配置文件,示例代码如下:

    /**
     * 创建会话管理
     */
    @Bean
    public DefaultWebSessionManager defaultWebSessionManager() {
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        defaultWebSessionManager.setGlobalSessionTimeout(10000);
        defaultWebSessionManager.setSessionDAO(sessionDAO());
        defaultWebSessionManager.setCacheManager(cacheManager());
        return defaultWebSessionManager;
    }

    @Bean
    public SessionDAO sessionDAO() {
        RedisSessionDao redisSessionDao = new RedisSessionDao();
        redisSessionDao.setCacheManager(cacheManager()); // 设置缓存管理器
        return redisSessionDao;
    }

经过测试也是可以成功跳转,会话共享。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/944039.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【蓝桥杯——物联网设计与开发】基础模块8 - RTC

目录 一、RTC &#xff08;1&#xff09;资源介绍 &#x1f505;简介 &#x1f505;时钟与分频&#xff08;十分重要‼️&#xff09; &#xff08;2&#xff09;STM32CubeMX 软件配置 &#xff08;3&#xff09;代码编写 &#xff08;4&#xff09;实验现象 二、RTC接口…

API安全学习笔记

必要性 前后端分离已经成为web的一大趋势&#xff0c;通过TomcatNgnix(也可以中间有个Node.js)&#xff0c;有效地进行解耦。并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务&#xff08;多种客户端&#xff0c;例如&#xff1a;浏览器&#x…

2011-2020年各省城镇职工基本医疗保险年末参保人数数据

2011-2020年各省城镇职工基本医疗保险年末参保人数数据 1、时间&#xff1a;2011-2020年 2、来源&#xff1a;国家统计局 3、指标&#xff1a;省份、时间、城镇职工基本医疗保险年末参保人数 4、范围&#xff1a;31省 5、指标解释&#xff1a;参保人数指报告期末按国家有关…

【蓝桥杯——物联网设计与开发】拓展模块4 - 脉冲模块

目录 一、脉冲模块 &#xff08;1&#xff09;资源介绍 &#x1f505;原理图 &#x1f505;采集原理 &#xff08;2&#xff09;STM32CubeMX 软件配置 &#xff08;3&#xff09;代码编写 &#xff08;4&#xff09;实验现象 二、脉冲模块接口函数封装 三、踩坑日记 &a…

Kubernetes Gateway API-2-跨命名空间路由

1 跨命名空间路由 Gateway API 具有跨命名空间路由的核心支持。当多个用户或团队共享底层网络基础设施时,这很有用,但必须对控制和配置进行分段,以尽量减少访问和容错域。 Gateway 和 Route(HTTPRoute,TCPRoute,GRPCRoute) 可以部署到不同的命名空间中,路由可以跨命名空间…

Windows Powershell实战指南(未完成)

目前只作简单了解&#xff0c;开始吧。 一、初识Powershell 目标 初步认识 Powershell和其集成环境 Ise&#xff0c;学会基本设置 实验 我们从简单的例子开始&#xff1a;希望你能从控制台和ISE的配置中实现相同的结果。然后按照下面五步进行。 &#xff08;1&#xff09;选…

Android着色器SweepGradient渐变圆环,Kotlin

Android着色器SweepGradient渐变圆环&#xff0c;Kotlin import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Path import android.graphics.SweepGradient import android…

项目上传到gitcode

首先需要在个人设置里面找到令牌 记住自己的账号和访问令牌&#xff08;一长串&#xff09;&#xff0c;后面git要输入这个&#xff0c; 账号是下面这个 来到自己的仓库 #查看远程仓库&#xff0c;是不是自己的云仓库 git remote -v # 创建新分支 git checkout -b llf # 三步…

SAQ问卷的定义,SAQ问卷是什么?

SAQ问卷&#xff0c;全称为可持续发展评估问卷&#xff08;Sustainability Assessment Questionnaire&#xff09;&#xff0c;是一种在线自评工具&#xff0c;其深远意义与广泛应用在当今商业环境中愈发凸显。它不仅是一种衡量企业在环境、社会和治理&#xff08;ESG&#xff…

SpringBoot获取bean的几种方式

目录 一、BeanFactory与ApplicationContext的区别 二、通过BeanFactory获取 三、通过BeanFactoryAware获取 四、启动获取ApplicationContext 五、通过继承ApplicationObjectSupport 六、通过继承WebApplicationObjectSupport 七、通过WebApplicationContextUtils 八、通…

web3基于zkEVM的L2扩容方案-Scroll

项目简介 Scroll 是2021年由华人创始团队推出的 基于zkEVM 的 以太坊ZKR扩容方案&#xff0c;不同于zkSync的语言级别兼容&#xff0c;Scroll实现了完全EVM等效&#xff0c;即字节码层级兼容&#xff0c;除了数据结构和状态树等部分&#xff0c;zkEVM看起来与以太坊完全一样&a…

深入浅出 Linux 操作系统

深入浅出 Linux 操作系统 引言 在当今数字化的时代&#xff0c;Linux 操作系统无处不在。从支撑互联网巨头庞大的数据中心&#xff0c;到嵌入智能家居设备的微型芯片&#xff0c;Linux 都发挥着关键作用。然而&#xff0c;对于许多人来说&#xff0c;Linux 仍笼罩着一层神秘的…

Python毕业设计选题:基于python的白酒数据推荐系统_django+hive

开发语言&#xff1a;Python框架&#xff1a;djangoPython版本&#xff1a;python3.7.7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 管理员登录 管理员功能界面 用户管理 白酒管理 系统管理 看板展示 系统首页 白酒详情…

【赵渝强老师】MongoDB逻辑存储结构

MongoDB的逻辑存储结构是一种层次结构&#xff0c;主要包括了三个部分&#xff0c;即&#xff1a;数据库&#xff08;Database&#xff09;、集合&#xff08;Collection&#xff0c;也可以叫做表&#xff09;和文档&#xff08;Document&#xff0c;也可以叫做记录&#xff09…

Python数据可视化小项目

英雄联盟S14世界赛选手数据可视化 由于本学期有一门数据可视化课程&#xff0c;课程结课作业要求完成一个数据可视化的小Demo&#xff0c;于是便有了这个小项目&#xff0c;课程老师要求比较简单&#xff0c;只要求熟练运用可视化工具展示数据&#xff0c;并不要求数据来源&am…

继承超详细介绍

一 、继承 1 继承的概念 继承是面向对象程序设计使得代码可以复用的最重要手段&#xff0c;它使得我们可以在原有类的特性的基础上进行扩展&#xff0c;增加方法和属性&#xff08;成员函数与成员变量&#xff09;&#xff0c;这样产生新的类&#xff0c;叫作派生类。继承呈现了…

Numpy指南:解锁Python多维数组与矩阵运算(上)

文章一览 前言一、nmupy 简介和功能二、numpy 安装三、numpy基本使用3.1、ndarray 对象3.2、基础数据结构 ndarray 数组3.3、ndarray 数组定义3.4、ndarray 数组属性计算3.5、ndarray 数组创建3.5.1 通过 array 方式创建 ndarray 数组3.5.2 通过 arange 创建数组3.5.3 通过 lin…

C++:单例模式

创建自己的对象&#xff0c;同时确保对象的唯一性。 单例类只能有一个实例☞静态成员static☞静态成员 必须类外初始化 单例类必须自己创建自己的唯一实例 单例类必须给所有其他对象提供这一实例 静态成员类内部可以访问 构造函数私有化☞构造函数私有外部不能创建&#x…

【火猫DOTA2】VP一号位透露队伍不会保留原阵容

1、最近VP战队的一号位选手Kiritych在直播中透露,VP战队的阵容将会有新的变动,原有的阵容将不再保留。 【目前VP战队阵容名单如下】 一号位:Kiritych 二号位:squad1x 三号位:Noticed 四号位:Antares 五号位:待定 2、Spirit的战队经理Korb3n在直播时谈到了越来越多的职业选…

FOC软件 STM32CubeMX 使用

1、安装-及相关软件版本 展示版本注意事项:keil MDK和STM32CubeMX版本至少要大于等于图中版本。 2、 Motor Profiler 5.2.0使用方法