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

1. 什么是会话

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

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

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

java 复制代码
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 会话怎么创建,如何保存,如何更新。即会话的 增,删,改,查

    java 复制代码
    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对象

    java 复制代码
    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

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

    java 复制代码
    public interface SessionListener {
        void onStart(Session session);
        void onStop(Session session);
        void onExpiration(Session session);
    }

3. 应用中如何使用session

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

java 复制代码
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:

java 复制代码
@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对象。

java 复制代码
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

java 复制代码
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

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

java 复制代码
@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 的代码到配置文件中:

java 复制代码
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存储会话 分支上.

相关推荐
郑祎亦16 分钟前
Spring Boot 项目 myblog 整理
spring boot·后端·java-ee·maven·mybatis
本当迷ya29 分钟前
💖2025年不会Stream流被同事排挤了┭┮﹏┭┮(强烈建议实操)
后端·程序员
计算机毕设指导61 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
paopaokaka_luck2 小时前
[371]基于springboot的高校实习管理系统
java·spring boot·后端
煎饼小狗2 小时前
Redis五大基本类型——Zset有序集合命令详解(命令用法详解+思维导图详解)
数据库·redis·缓存
捂月3 小时前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
瓜牛_gn3 小时前
依赖注入注解
java·后端·spring
Estar.Lee4 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
喜欢猪猪4 小时前
Django:从入门到精通
后端·python·django
一个小坑货4 小时前
Cargo Rust 的包管理器
开发语言·后端·rust