Shiro学习(四):Shiro对Session的处理和缓存

一、Shiro对Session 的处理

1、Shiro认证成功后是否还依赖Web容器的Session?

在Shiro 与Web(如:SSM、springboot)环境中,Shiro 认证成功后默认使用的是Web容器

的Session,并默认将Session保存到类 HttpServletSession 中的 HttpSession 属性中;

下边以源码的角度看下默认情况下Shiro对Session的处理:

1)打开Springboot默认装载Shiro配置的类 ShiroWebAutoConfiguration

ShiroWebAutoConfiguration位于shiro-spring-boot-web-starter 包下的文件 spring.factores

中;在 ShiroWebAutoConfiguration 中我们找到Session 相关的2个核心配置 SessionDAO

与 SessionManager,如下图所示:

2)点开 sessionManager() 方法

在 sessionManager() 方法中可以看到,首先会先判断当前环境是否配置了

"shiro.userNativeSessionManager" ?

I)配置了该属性:则使用Shiro自身的Session管理器 DefaultWebSessionManager,

来处理Session

II)没有配置该属性:默认情况,则使用Web容器相关的Session管理器

ServletContainerSessionManager

如下图所示:

3)ServletContainerSessionManager

ServletContainerSessionManager 是基于Web容器的Session 管理器,主要用于

获取Http请求中的Session和来源host,然后将Session和Host 封装成HttpServletSession

如下图所示:

4)nativefSessionManager() 方法

该方法主要是创建Shiro 自身的Session管理器 DefaultWebSessionManager,并给

SessionManager 设置 SessionDAO;如下图所示:

5)SessionDAO

Shiro中 SessionDAO 到主要是用来处理Session 存储的问题;

SessionDAO 的默认实现是 MemorySessionDAO,是一个基于内存的存储,即

MemorySessionDAO 将Session 信息存储到一个全局的Map 中,

注意: 默认情况下 MemorySessionDAO 没有被使用,Shiro 将Session 保存到了

Web容器的 HttpSession 中

如下图所示:

二、Shiro对Session 缓存的支持

由上边可以知道,默认情况下Shiro是将Session 保存到了Web 容器的HttpSession 中,但通过

自定义SessionManager 与 SessionDAO 可以将 Session 保存到JVM内存和一些缓存中间件

中,如:将Session 保存到redis 中。

1、将 Session 保存到JVM内存中

由上边知道,Shiro 默认提供了将Session 保存到 JVM内存中的SessionDAO实现类

MemorySessionDAO,但 Shiro 默认情况下并没有使用 MemorySessionDAO;

所以我们只要把 MemorySessionDAO 手动注入到 SessionManager 中就可以将Session

保存到JVM内存中。

实现方式:在Shiro配置类 ShiroConfig 中自定义 SessionManager,来覆盖默认情况下自动

装配的 SessionManager;并手动将 SessionDAO(默认是MemorySessionDAO )

注入到 SessionManager;最后将自定义的 SessionManager注入SecurityManager

示例代码如下:

java 复制代码
/**
     * 使用Shiro 提供的 MemorySessionDAO  将Session 保存到jvm内存中
     * @param sessionDAO
     * @return
     */
    @Bean
    public SessionManager sessionManager(SessionDAO sessionDAO){

        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(sessionDAO);
        return sessionManager;
    }


@Bean
    public DefaultWebSecurityManager securityManager(CustomRealm realm, SessionManager sessionManager){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);

        //将使用MemorySessionDAO的SessionManager注入到SecurityManager
        securityManager.setSessionManager(sessionManager);

        return securityManager;
    }

2、将 Session 保存到Redis 中

将Session保存在jvm内存的情况只适合单机的架构中;在分布式环境中可以将Session单独存

放到大家都可以访问的地方,如Redis,这样就可以解决分布式环境下Session共享的问题。

由上边可以知道Session的存储具体是由SessionDAO完成的,所以我们需要模仿

MemorySessionDAO 来定义一个基于Redis的 SessionDAO 的实现,来完成 Session 存储

到Redis的操作,具体步骤如下:

1)定义基于Redis 的 SessionDAO

自定义 SessionDAO 需要继承抽象类 AbstractSessionDAO,并重写增加、修改、读取、

删除Session 的方法,具体代码如下:

java 复制代码
/****************************************************
 * 自定义 SessionDAO,用于将Session 存储到Redis
 * 让 SessionDAO 与 Redis 交互
 *
 * 将 自定义的 SessionDAO 交由SessionManager 管理
 *
 * @author lbf
 * @date 
 ****************************************************/
@Component
public class RedisSessionDAO extends AbstractSessionDAO {

    // 存储到Redis时,sessionId作为key,Session作为Value
    // sessionId就是一个字符串
    // Session可以和sessionId绑定到一起,绑定之后,可以基于Session拿到sessionId
    // 需要给Key设置一个统一的前缀,这样才可以方便通过keys命令查看到所有关联的信息

    @Resource
    private RedisTemplate redisTemplate;

    private final String SHIOR_SESSION = "session:";


    @Override
    protected Serializable doCreate(Session session) {
        //1、生成SessionId,唯一标识
        Serializable sessionId = this.generateSessionId(session);
        //2、将SessionId 与 session 绑定一起,基于 sessionId可以拿到 session
        this.assignSessionId(session,sessionId);

        //3. 将 前缀:sessionId 作为key,session作为value存储,并设置过期时间为30分钟
        redisTemplate.opsForValue().set(SHIOR_SESSION+sessionId,session,30, TimeUnit.MINUTES);

        //4、返回 sessionId
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        //1. 基于sessionId获取Session (与Redis交互)
        if (sessionId == null) {
            return null;
        }
        Session session = (Session) redisTemplate.opsForValue().get(SHIOR_SESSION + sessionId);
        if (session != null) {
            //更新session超时时间
            redisTemplate.expire(SHIOR_SESSION + sessionId,30,TimeUnit.MINUTES);
        }
        return session;
    }

    @Override
    public void update(Session session) throws UnknownSessionException {

        //1. 修改Redis中session
        if(session == null){
            return ;
        }
        //覆盖操作
        redisTemplate.opsForValue().set(SHIOR_SESSION + session.getId(),session,30, TimeUnit.MINUTES);

    }

    @Override
    public void delete(Session session) {

        // 删除Redis中的Session
        if(session == null){
            return ;
        }
        redisTemplate.delete(SHIOR_SESSION + session.getId());

    }

    /**
     * 获取所有的Session
     * @return
     */
    @Override
    public Collection<Session> getActiveSessions() {

        //获取所有前缀为 session: 的key集合
        Set keys = redisTemplate.keys(SHIOR_SESSION + "*");

        Set<Session> sessionSet = new HashSet<>();
        // 尝试修改为管道操作,pipeline(Redis的知识)
        for (Object key : keys) {
            Session session = (Session) redisTemplate.opsForValue().get(key);
            sessionSet.add(session);
        }

        return sessionSet;
    }
}

2)将自定义 SessionDAO 注入到 SessionManager

java 复制代码
 @Bean
    public SessionManager sessionManager(RedisSessionDAO sessionDAO){

        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(sessionDAO);
        return sessionManager;
    }



 @Bean
    public DefaultWebSecurityManager securityManager(CustomRealm realm, SessionManager sessionManager){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);

        
        securityManager.setSessionManager(sessionManager);

        return securityManager;
    }

3、分析2 中的问题

上边2的实现中有个问题,即:发现每次请求需要访问多次Redis服务,即使是同一个请求,

每次也要从Redis 读取Session,这个访问的频次会出现很长时间的IO等待,对每次请求的

性能减低了,并且对Redis的压力也提高了。

通过debug 发现,系统是在 DefaultWebSessionManager.retrieveSession() 方法中调用

SessionDAO的readSession() 方法去读取 Session 的,如下图所示:

到这里应该有个大致思路了,我们可以自定义一个SessionManager,继承

DefaultWebSessionManager,并重写方法 retrieveSession,来重写读取Session

的逻辑,即:先从HttpRequest 域 中获取Session,若获取不到再从Redis 中查询Session;

示例代码如下:

java 复制代码
/****************************************************
 * 自定义 WebSessionManager
 * 将传统的基于Web容器或者ConcurrentHashMap切换为Redis之后,发现每次请求需要访问多次Redis服务,这个访问的频次会出现很长时间的IO等待,
 * 对每次请求的性能减低了,并且对Redis的压力也提高了。
 *
 * 基于装饰者模式重新声明SessionManager中提供的retrieveSession方法,让每次请求先去request域中查询session信息,request域中没有,再去Redis中查询
 *
 * @author lbf
 * @date 
 ****************************************************/
public class DefaultRedisWebSessionManager  extends DefaultWebSessionManager {

    private static final Logger log = LoggerFactory.getLogger(DefaultRedisWebSessionManager.class);

    @Override
    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
        // 通过sessionKey获取sessionId
        Serializable sessionId = getSessionId(sessionKey);

        // 将sessionKey转为WebSessionKey
        if(sessionKey instanceof WebSessionKey){
            WebSessionKey webSessionKey = (WebSessionKey) sessionKey;
            // 获取到request域
            ServletRequest request = webSessionKey.getServletRequest();
            // 通过request尝试获取session信息
            Session session = (Session) request.getAttribute(sessionId + "");
            if(session != null){
                System.out.println("从request域中获取session信息");
                return session;
            }else{
                session = retrieveSessionFromDataSource(sessionId);
                if (session == null) {
                    //session ID was provided, meaning one is expected to be found, but we couldn't find one:
                    String msg = "Could not find session with ID [" + sessionId + "]";
                    throw new UnknownSessionException(msg);
                }
                System.out.println("Redis---doReadSession");
                request.setAttribute(sessionId + "",session);
                return session;
            }
        }
        return null;
    }
}

2)修改配置文件,使用自定义的 DefaultRedisWebSessionManager ,

java 复制代码
/**
     * 
     * 这里自定义 SessionManager 是为了 不使用基于Web 的Session,因为基于web的HttpSession 在分布式环境中是无法使用的
     *
     * todo 注意:
     *     这里虽然将 SessionManager 注入到spring容器了,但自定义的 SessionManager 并没有生效,需要
     *     把自定义的 SessionManager 交给 SecurityManager 管理后才能生效
     *
     */
    @Bean
    public SessionManager sessionManager(RedisSessionDAO sessionDAO){

        //DefaultSessionManager sessionManager = new DefaultSessionManager();

        //使用自定义的SessionManager
        DefaultRedisWebSessionManager sessionManager = new DefaultRedisWebSessionManager();
        sessionManager.setSessionDAO(sessionDAO);
        return sessionManager;
    }



@Bean
    public DefaultWebSecurityManager securityManager(CustomRealm realm, SessionManager sessionManager){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);

        //将SessionManager注入到SecurityManager
        securityManager.setSessionManager(sessionManager);

        return securityManager;
    }
相关推荐
直视太阳1 小时前
springboot+easyexcel实现下载excels模板下拉选择
java·spring boot·后端
longlong int1 小时前
【每日算法】Day 16-1:跳表(Skip List)——Redis有序集合的核心实现原理(C++手写实现)
数据库·c++·redis·算法·缓存
Code成立1 小时前
《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》第2章 Java内存区域与内存溢出异常
java·jvm·jvm内存模型·jvm内存区域
虾球xz1 小时前
游戏引擎学习第208天
学习·游戏引擎
小军要奋进1 小时前
httpx模块的使用
笔记·爬虫·python·学习·httpx
一 乐1 小时前
实验室预约|实验室预约小程序|基于Java+vue微信小程序的实验室预约管理系统设计与实现(源码+数据库+文档)
java·数据库·微信小程序·小程序·毕业设计·论文·实验室预约小程序
程序媛学姐1 小时前
SpringRabbitMQ消息模型:交换机类型与绑定关系
java·开发语言·spring
努力努力再努力wz1 小时前
【c++深入系列】:类与对象详解(中)
java·c语言·开发语言·c++·redis
兰亭序咖啡1 小时前
学透Spring Boot — 009. Spring Boot的四种 Http 客户端
java·spring boot·后端
独行soc2 小时前
2025年渗透测试面试题总结- 某四字大厂面试复盘扩展 一面(题目+回答)
java·数据库·python·安全·面试·职场和发展·汽车