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存储会话 分支上.

相关推荐
逍遥德8 分钟前
MQTT教程详解-05.SpringBoot集成mqtt client 性能分析
java·spring boot·spring·mt
点燃大海17 分钟前
SpringAI构建智能体
java·spring boot·spring·springai智能体
xier_ran18 分钟前
【infra之路】02_RadixAttention与KV_Cache管理
java·spring boot·spring
Steadfast_GG19 分钟前
Redis中的通用命令
redis·缓存
swipe25 分钟前
做多轮对话 Agent,为什么我建议把短期记忆放到 Redis
后端·面试·llm
小二·26 分钟前
Redis 内存溢出(OOM)排查与恢复实战
数据库·redis·bootstrap
pqk6V6Vep26 分钟前
Redis 分布式锁进阶第一篇讲解
数据库·redis·分布式
码客日记36 分钟前
Spring Boot 配置文件敏感信息加密(Jasypt 企业级完整方案)
java·spring boot·git
giaz14n9X43 分钟前
Redis 分布式锁进阶第六十一篇
数据库·redis·分布式
程序员黑豆1 小时前
AI全栈开发之Java:什么是JDK
前端·后端·ai编程