4_springboot_shiro_jwt_多端认证鉴权_Redis存储会话

news2024/11/18 6:40:54

1. 什么是会话

所谓的会话,就是用户与应用程序在某段时间内的一系列交互,在这段时间内应用能识别当前访问的用户是谁,而且多次交互中可以共享数据。我们把一段时间内的多次交互叫做一次会话。

即用户登录认证后,多次与应用进行交互,直到调用退出登录,这就是一次会话。如果登录后一直没有调用退出登录,那么一段时间后,这个会话会自动结束。

在Shiro中,将会话定义为一个接口:

package org.apache.shiro.session;

import java.io.Serializable;
import java.util.Collection;
import java.util.Date;
public interface Session {
	// 获取sessionID
    Serializable getId();
	// 会话开始的时间
    Date getStartTimestamp();
	// 最后一次访问时间
    Date getLastAccessTime();
	// 获取会话过期时间(毫秒)
    long getTimeout() throws InvalidSessionException;
    // 设置会话过期时间
    void setTimeout(long maxIdleTimeInMillis) throws InvalidSessionException;

    String getHost();
    void touch() throws InvalidSessionException;

	// 可以在一次会话中共享数据,数据以 key-value 的形式保存,这里返回所有的key
    Collection<Object> getAttributeKeys() throws InvalidSessionException;

   //  可以在一次会话中共享数据,通过key 获取相关的数据
    Object getAttribute(Object key) throws InvalidSessionException;

	//  可以在一次会话中共享数据,通过key 保存相关的数据
    void setAttribute(Object key, Object value) throws InvalidSessionException;

    //  可以在一次会话中共享数据,通过key 删除相关数据
    Object removeAttribute(Object key) throws InvalidSessionException;
}

Shiro 会话的概念与 JavaEE Servlet 容器中的会话概念是一致的。但是Shiro会话比 JavaEE 容器会话概念要大一些。因为Shiro的会话管理不依赖于底层容器(如 tomcat web 容器), 不管是 JavaSE 还是 JavaEE环境,它都可以使用。如果使用了Shiro,那么可以直接使用Shiro的会话管理来替代Web容器的会话管理。

2. Shiro会话管理相关接口

Shiro中,实现了 org.apache.shiro.session.Session接口的都是会话对象, 在这个对象的管理上也有一套接口来支撑.

  • org.apache.shiro.session.mgt.eis.SessionDAO 会话怎么创建,如何保存,如何更新。即会话的 增,删,改,查

    public interface SessionDAO {
        // 保存session对象到 数据库,或者持久化的缓存,或者文件系统中。这依赖于具体的实现。保存完毕之后返回的是 ID,即sessionId
        Serializable create(Session session);
        // 根据sessionId 获取整个Session对象
        Session readSession(Serializable sessionId) throws UnknownSessionException;
    	// 更新Session
        void update(Session session) throws UnknownSessionException;
        // 删除Session
        void delete(Session session);
    	// 获取所有活动的session
        Collection<Session> getActiveSessions();
    
  • org.apache.shiro.session.mgt.eis.SessionManager 如何开始一个会话,根据key获取session对象

    public interface SessionManager {
        //此方法主要用于框架开发,因为实现通常会将参数传递给底层SessionFactory,后者可以使用上下文以特定方式构造内部会话实例。只需将SessionFactory注入到SessionManager实例中,就可以实现可插入的会话创建逻辑。
        Session start(SessionContext context);
    
        // 使用key 检索session
        Session getSession(SessionKey key) throws SessionException;
    }
    
  • org.apache.shiro.session.mgt.SessionKey session的key标识,不一定是个字符串,实现了Serializable都可以作为session的key

    public interface SessionKey {
        Serializable getSessionId();
    }
    
  • org.apache.shiro.session.SessionListener 监听器,通过它可以监听到session什么时候开始,什么时候借宿,什么时候过期

    public interface SessionListener {
        void onStart(Session session);
        void onStop(Session session);
        void onExpiration(Session session);
    }
    

3. 应用中如何使用session

开发的时候,我们往往需要在一次会话的多次交互中共享数据通常是这样来实现的:

Subject curSubject = SecurityUtils.getSubject();
// 取得当前session
Session session = currentUser.getSession();
// 存放共享数据
session.setAttribute("someKey", someValue);
// 获取共享数据
Object someValue= session.getAttribute("someKey")
// 删除数据
session.removeAttribute("someKey")

4. SessionDAO

Session中的数据是保存在服务端的,至于到底保存到哪里(数据库中\内存中\磁盘文件中) 由具体的 SessionDAO来实现。本小节实现将Session中的数据保存到Redis中。

4.1 默认SessionDAO

还是先看看框架默认使用的是哪个具体的SessionDAO, 查看自动配置类ShiroWebAutoConfiguration:

@Configuration
@AutoConfigureBefore(ShiroAutoConfiguration.class)
@AutoConfigureAfter(ShiroWebMvcAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
public class ShiroWebAutoConfiguration extends AbstractShiroWebConfiguration {
	...
     // 创建SessionDAO Bean
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected SessionDAO sessionDAO() {
        return super.sessionDAO();
    }
    ...
 
}

public class AbstractShiroConfiguration {
    ...
    protected SessionDAO sessionDAO() {
        // 默认使用的是 MemorySessionDAO
        return new MemorySessionDAO();
    }
    ...
}

默认的SessionDAO是 MemorySessionDAO,这在分布式集群环境下会造成数据的不一致,所以下面自己来扩展,将Session数据保存到Redis中。
在这里插入图片描述

AbstractSessionDAO还有个子类叫做 CachingSessionDAO,所以只需要我们自己定义一个类,去继承 CachingSessionDAO 即可。

4.2 自定义ShiroRedisSessionDAO

这个ShiroRedisSessionDAO 将所有的session数据都存储到Redis 的Hash结构中,Hash结构的Redis Key是固定的。 Hash结构的key是sessionID,value是整个Session对象。

package com.qinyeit.shirojwt.demos.shiro.cache;
...
//  将Session数据保存到Redis中, Redis中存储的数据是Hash结构
@Slf4j
public class ShiroRedisSessionDAO extends EnterpriseCacheSessionDAO {
    //redis中session名称前缀
    private String redisKey = "shiro:session";

    private RedisTemplate<Object, Object> redisTemplate;

    public ShiroRedisSessionDAO(RedisTemplate<Object, Object> redisTemplate, String redisKey) {
        this.redisTemplate = redisTemplate;
        this.redisKey = redisKey;
    }

    // 创建session,保存到数据库
    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = super.doCreate(session);
        log.debug("doCreate:" + session.getId());
        redisTemplate.opsForHash().put(redisKey, session.getId(), session);
        return sessionId;
    }

    // 获取session
    @Override
    protected Session doReadSession(Serializable sessionId) {
        log.debug("doReadSession:" + sessionId);
        // 先从缓存中获取session,如果没有再去数据库中获取
        Session session = super.doReadSession(sessionId);
        if (session == null) {
            session = (Session) redisTemplate.opsForHash().get(redisKey, sessionId);
        }
        return session;
    }

    // 更新session
    @Override
    protected void doUpdate(Session session) {
        super.doUpdate(session);
        log.debug("doUpdate:" + session.getId());
        HashOperations<Object, Object, Object> hashOp = redisTemplate.opsForHash();
        if (!hashOp.hasKey(redisKey, session.getId())) {
            hashOp.put(redisKey, session.getId(), session);
        }
    }

    //删除session
    @Override
    protected void doDelete(Session session) {
        log.debug("doDelete:" + session.getId());
        super.doDelete(session);
        HashOperations<Object, Object, Object> hashOp = redisTemplate.opsForHash();
        hashOp.delete(redisKey, session.getId());
    }

    //获取当前活动的session
    @Override
    public Collection<Session> getActiveSessions() {
        return super.getActiveSessions();
    }
}

4.3 配置ShiroRedisSessionDAO

package com.qinyeit.shirojwt.demos.configuration;
...
@Configuration
@Slf4j
public class ShiroConfiguration {
    ...
    // 配置SessionDAO
    @Bean
    public SessionDAO shiroRedisSessionDAO(RedisTemplate redisTemplate) {
        ShiroRedisSessionDAO sessionDAO = new ShiroRedisSessionDAO(redisTemplate, "shiro:session");
        // 活跃session缓存的名字
        sessionDAO.setActiveSessionsCacheName("shiro:active:session");
        sessionDAO.setCacheManager(cacheManager);
        return sessionDAO;
    }
    ...
}   

5. 执行登录测试

执行正确的登录,看看redis中都保存了些什么?
在这里插入图片描述
很奇怪,明明SessionDAO,却并没有将会话保存到Redis中。

6. 没有调用SessionDAO的原因

在IDEA中,打开SessionDAO 这个接口,选中其中的 create方法,然后 ctrl+alt+F7 (windows环境) ,或者按住 ctrl+鼠标点击方法,可以看到这个方法都在哪些地方调用
在这里插入图片描述

我们发现这个方法是在DefaultSessionManager中调用的。找到这个DefaultSessionManager,发现它还有个子类DefaultWebSessionManager ,没有调用dao,有没有可能是 默认的SessionManager既不是 DefaultSessionManager 也不是 DefaultWebSessionManager

6.1 默认SessionManager

打开自动配置类,发现了如下代码:

@Configuration
@AutoConfigureBefore(ShiroAutoConfiguration.class)
@AutoConfigureAfter(ShiroWebMvcAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
public class ShiroWebAutoConfiguration extends AbstractShiroWebConfiguration {
    ...
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected SessionManager sessionManager() {
        // 调用了父类 AbstractShiroWebConfiguration 中的 sessionManager()方法
        return super.sessionManager();
    }
    ...
}

// 父类 AbstractShiroWebConfiguration
public class AbstractShiroWebConfiguration extends AbstractShiroConfiguration {
    ...
    // 我们没有在外部做任何配置,所以它是false
    @Value("#{ @environment['shiro.userNativeSessionManager'] ?: false }")
    protected boolean useNativeSessionManager;
    ...
    @Override
    protected SessionManager sessionManager() {
        // 默认情况下useNativeSessionManager 为false
        if (useNativeSessionManager) {
            return nativeSessionManager();
        }
        // 这就是默认使用的 SessionManager
        return new ServletContainerSessionManager();
    }
    ...
    protected SessionManager nativeSessionManager() {
        DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
        webSessionManager.setSessionIdCookieEnabled(sessionIdCookieEnabled);
        webSessionManager.setSessionIdUrlRewritingEnabled(sessionIdUrlRewritingEnabled);
        webSessionManager.setSessionIdCookie(sessionCookieTemplate());

        webSessionManager.setSessionFactory(sessionFactory());
        webSessionManager.setSessionDAO(sessionDAO());
        webSessionManager.setDeleteInvalidSessions(sessionManagerDeleteInvalidSessions);

        return webSessionManager;
    }
}

可以看到,这个默认的SessionManager的实现类为:org.apache.shiro.web.session.mgt.ServletContainerSessionManager ,查看这个类,我们发现它根本就没有使用 SessionDAO。

默认启动的SessionManger其实是 ServletContainerSessionManager, 这个类的文档注释上写得很清楚,它不管理会话,因为Servlet容器提供了实际的管理支持。

所以配置的SessionDAO没有被调用是因为默认启动的是 ServletContainerSessionManager , Session的管理实际上由Servlet容器来完成,自然就不会调用SessionDAO来完成Session对象的增,删,改

6.2 替换默认SessionManager

看上面的第 22行和 28 行代码,只需要将shiro.userNativeSessionManager 配置为 true ,就可以启用。 当然也可以在配置文件中直接配置。这里就直接拷贝nativeSessionManager 的代码到配置文件中:

package com.qinyeit.shirojwt.demos.configuration;
...
@Configuration
@Slf4j
public class ShiroConfiguration {
	...
    // 配置SessionDAO
    @Bean
    public SessionDAO shiroRedisSessionDAO(RedisTemplate redisTemplate) {
        return new ShiroRedisSessionDAO(redisTemplate, "shiro:session");
    }
    // sessionManager配置
    @Bean
    public SessionManager sessionManager(
            SessionFactory sessionFactory,
            @Qualifier("sessionCookieTemplate") Cookie cookieTemplate,
            SessionDAO sessionDAO) {
        DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
        // 开启Cookie,即由Cookie来传递 sessionID保持会话
        webSessionManager.setSessionIdCookieEnabled(true);
        // 开启URL重写,即可以从URL中获取sessionID来保持会话
        webSessionManager.setSessionIdUrlRewritingEnabled(true);
        // 自动配置中已经配置了cookieTemplate 直接注入进来,具体看 ShiroWebAutoConfiguration 类中bean的定义
        webSessionManager.setSessionIdCookie(cookieTemplate);
        // 自动配置中已经配置了sessionFactory 直接注入进来
        webSessionManager.setSessionFactory(sessionFactory);
        // 使用自定义的ShiroRedisSessionDAO
        webSessionManager.setSessionDAO(sessionDAO);
        // 清理无效的session
        webSessionManager.setDeleteInvalidSessions(true);
        // 开启session定时检查
        webSessionManager.setSessionValidationSchedulerEnabled(true);
        webSessionManager.setSessionValidationScheduler(new ExecutorServiceSessionValidationScheduler());
        return webSessionManager;
    }
...
}

现在清空redis后,再执行登录操作,然后查看redis中的key:
在这里插入图片描述

发现有三个缓存,这三个缓存都是 Redis Hash 数据类型

  • session缓存
  • 活动session缓存
  • 认证信息缓存

7. Session失效

在IDEA 中,打开SessionDAO源码,选中delete方法后,跟踪这个方法是如何被调用的。最终会跟踪到 subject.logout() 方法 所以退出登录的时候会到dao中将session删除掉.

sessionManager打开了session定时检查,会定时检查失效的session,然后进行清除

8. 总结

  1. 没有做任何配置的情况下,使用的是 ServletContainerSessionManager ,本质是由Servlet容器来管理会话
  2. 分布式集群环境下,使用 Redis来保存会话信息,则必须替换掉默认的ServletContainerSessionManager, 使用 DefaultWebSessionManager ,可以自己写配置Bean,也可以在 application.properties 中配置选项:shiro.userNativeSessionManager =true
  3. 用Redis来保存会话,需要自己实现 SessionDAO 接口,此时可以直接继承Shiro已经实现的子类 EnterpriseCacheSessionDAO

代码仓库 https://github.com/kaiwill/shiro-jwt , 本节代码在 4_springboot_shiro_jwt_多端认证鉴权_Redis存储会话 分支上.

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WPF —— Calendar日历控件详解

1&#xff1a; Calendar的简介 日历控件用于创建可视日历&#xff0c;让用户选择日期并在选择日期时触发事件。 DisplayMode 用来调整日历显示模式&#xff0c;分为Month、Year 和Decade 三种。如下是None 2&#xff1a;Calendar控件常用的属性 SelectionMode 选中日历的类…

原生php单元测试示例

下载phpunit.phar https://phpunit.de/getting-started/phpunit-9.html 官网 然后win点击这里下载 新建目录 这里目录可以作为参考&#xff0c;然后放在根目录下 新建一个示例类 <?phpdeclare(strict_types1);namespace Hjj\DesignPatterns\Creational\Hello;class He…

Python实战:爬取小红书

有读者在公众号后台询问爬取小红书&#xff0c;今天他来了。 本文可以根据关键词&#xff0c;在小红书搜索相关笔记&#xff0c;并保存为excel表格。 爬取的字段包括笔记标题、作者、笔记链接、作者主页地址、作者头像、点赞量。 一、先看效果 1、爬取搜索页 2、爬取结果保存到…

在IDE中配置tomcat服务器

目录 一、新建一个java项目二、添加web框架三、配置tomcat服务器四、运行访问发布的项目 前言&#xff1a;在 IntelliJ IDEA 中配置 Tomcat 服务器是 Java Web 开发的基础步骤&#xff0c;以下是如何在 IDEA 中设置 Tomcat 并部署 Web 项目的简要指南。 一、新建一个java项目 新…

DVWA-master 存储型xss

什么是存储型xss 存储型xss意味着可以与数据库产生交互的&#xff0c;可以直接存在数据库中 先将DVWA安全等级改为低 先随便写点东西上传 我们发现上传的内容会被显示&#xff0c;怎么显示的呢&#xff1f; 它先是上传到数据库中&#xff0c;然后通过数据库查询语句将内容回显 …

TSINGSEE青犀AI烟火识别等算法打造电瓶车消防安全解决方案

一、背景分析 根据国家消防救援局的统计&#xff0c;2023年全国共接报电动自行车火灾2.1万起&#xff0c;相比2022年上升17.4%&#xff0c;电动自行车火灾安全隐患问题不容忽视。 电瓶车火情主要问题和原因&#xff1a; 电瓶车/电池质量良莠不齐用户安全意识薄弱&#xff0c…

Shell编程入门

Shell编程入门 一、Shell概述1.1 Shell的作用1.2 Linux提供的Shell解释器1.3 Centos默认的解析器是bash 二、Shell脚本入门案例三、变量3.1 系统变量3.2 自定义变量3.3 特殊变量 四、运算符五、条件判断5.1 基本语法5.2 常用判断条件5.3 多条件判断 六、流程控制6.1 if语句6.2 …

山景BP1048 烧录器烧写

1.首先确保硬件连接没问题&#xff0c;烧写器不能亮红灯&#xff0c;亮红灯说明硬件没正确连接。硬件连接如下&#xff1a; 2.点击Flash Burner 3.编程目标闪存选择SDK包自带的烧写驱动器&#xff0c;闪存映像档选择编译好的bin文件。 4.点击刻录 5.看见有进度条在跑&#x…

一文看懂 关系模型-完整性约束

关系模型中有三类完整性约束&#xff1a;实体完整性、参照完整性和用户自定义的完整性。其中实体完整性和参照完整性是关系模型必须满足的完整性约束&#xff0c;被称为关系的两个不变性&#xff0c;由关系系统自动支持。 实体完整性详解&#xff1a; 若属性A是基本关系R的主属…

IPFoxy的正确打开方式

IPFoxy是一个全球动静态代理IP服务器软件&#xff0c;为全球用户提供优质的大数据代理服务&#xff0c;促进网络业务高校进行。目前拥有千万真实纯净IP资源&#xff0c;覆盖超过220个国家和地区&#xff0c;汇聚成优质海外代理池&#xff0c;支持http、https、socks5多种协议类…

什么是字节码?采用字节码的好处是什么?

在 Java 中&#xff0c;JVM 可以理解的代码就叫做字节码&#xff08;即扩展名为 .class 的文件&#xff09;&#xff0c;字节码是一种中间代码&#xff0c;它是由源代码经过编译生成的一种二进制表示形式。字节码通常不针对特定的硬件平台&#xff0c;而是针对虚拟机设计的&…

IEEE802.11v协议介绍

IEEE802.11v协议简介 协议全称:无线网络管理(Wireless Network Management) 批准日期:2011年2月 协议状态:并入802.11-2012 协议别名:BSS过渡管理 主要功能 支持AP和STA间交换:关于RF环境和拓扑状态的信息,以协助STA进行漫游决策支持STA之间交换:关于RF环境状态的信…

从政府工作报告探计算机行业发展(医疗健康领域)

从政府工作报告探计算机行业发展 政府工作报告作为政府工作的全面总结和未来规划&#xff0c;不仅反映了国家整体的发展态势&#xff0c;也为各行各业提供了发展的指引和参考。随着信息技术的快速发展&#xff0c;计算机行业已经成为推动经济社会发展的重要引擎之一。因此&…

2024.3.14jsp(2)

一、实验目的 掌握eclipse开发工具的使用&#xff1b;jsp标记、如指令标记&#xff0c;动作标记&#xff1b;变量和方法的声明&#xff1b;Java程序片&#xff1b; 实验&#xff1a;看电影 源代码watchMovie.jsp <% page language"java" contentType"text…

阿里云数据湖存储加速套件JindoData

计算存储分离已经成为云计算的一种发展趋势。在计算存储分离之前&#xff0c;普遍采用的是传统的计算存储相互融合的架构&#xff0c;但是这种架构存在一定的问题&#xff0c;比如在集群扩容的时候会面临计算能力和存储能力相互不匹配的问题。用户在某些情况下只需要扩容计算能…

Java虚拟机 - JVM

JVM的内存区域划分 JVM它其实也是一个进程,进程运行的过程中,会从操作系统中申请一些资源.内存就是其中的一种.这些内存就支撑了java程序的运行.JVM从系统中申请的一大块内存,会根据实际情况和使用用途来划分出不同的空间,这个就是区域划分.它一般分为 堆区, 栈区, 程序计数器…

LeetCode周赛——388

1.重新分装苹果&#xff08;贪心&#xff09; 思路 箱子大小降序排序&#xff0c;按顺序装 class Solution { public:int minimumBoxes(vector<int>& apple, vector<int>& capacity) {int n apple.size(), m capacity.size();int sum 0;for(int i 0;…

python_Anaconda虚拟环境导出以及重现

文章目录 1. 场景2. 解决方案2.1 方案一&#xff1a;直接将打包&#xff0c;然后将包传输到另外一台服务器2.2 方案二&#xff1a;导出环境所有的包名及版本&#xff0c;然后重新安装 1. 场景 我们有时候需要把一个虚拟环境迁移到别的服务器上面去&#xff0c;这时候&#xff…

多项式回归算法模拟

python3.6 环境 import numpy as np import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegression from sklearn.preprocessing import PolynomialFeatures# 生成随机数作为x变量&#xff0c;范围在-5到5之间&#xff0c;共100个样本 x np.random.un…

基于boost库的搜索引擎项目

文章目录 一、项目背景二、什么样的搜索引擎三、搜索引擎的宏观图原理四、Parse模块4.1下载boost库源代码4.2提取boost库中以.html为结尾的文件4.2.1 boost库的简单使用 4.3数据清洗(去标签化)4.3.1数据清洗的具体实现 4.4将清洗后的数据写入到raw.txt文件中 五、正排索引 vs 倒…