spring-redis-session 自定义 key 和过期时间
对于分布式应用来说,最开始遇到的问题就是session的存储了,解决方案大致有如下几种
- 使用spring-session它可以把session存储到你想存储的位置,如redis,mysql等
- 使用JWTs,它使用算法来验证token的合法性,是否过期,并且token无法被伪造,信息也是无法被篡改的
本文内容主要说spring-session使用redis来存储session,实现原理,修改过期时间,自定义key等
spring-session对于内部系统来说还是可以的,使用方便,但如果用户量上来了的话,会使redis有很大的session存储开销,不太划算。
使用
使用起来比较简单,简单说一下,引包,配置,加注解。如下面三步,就配置好了使用redis-session
org.springframework.boot spring-boot-starter-data-redis org.springframework.session spring-session-data-redis
spring.redis.host=localhost #其它超时,端口,库,连接池,集群,就自己去找了
@EnableRedisHttpSession(maxInactiveIntervalInSeconds=1800)
测试:因为是在getSession的时候才会创建Session,所以我们必须在接口中调用一次才能看到效果
@GetMapping("/sessionId") publicStringsessionId(){ HttpServletRequestrequest=((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest(); HttpSessionsession=request.getSession(); session.setAttribute("user","sanri"); returnsession.getId(); }
它的存储结果如下
hashspring:session:sessions:e3d4d84f-cc9f-44d5-9199-463cd9de8272
stringspring:session:sessions:expires:e3d4d84f-cc9f-44d5-9199-463cd9de8272
setspring:session:expirations:1577615340000
第一个hash结构存储了session的一些基本信息和用户设置的一些属性信息
creationTime创建时间
lastAccessedTime最后访问时间
maxInactiveInterval过期时长,默认是30分钟,这里保存的秒值
sessionAttr:user这是我通过session.setAttribute设置进去的属性
第二个string结构,它没有值,只有一个ttl信息,标识这组key还能活多久,可以用ttl查看
第三个set结构,保存了所以需要过期的key
实现原理
说明:这个实现没多少难度,我就照着源码念一遍了,就是一个过滤器的应用而已。
首先从网上了解到,它是使用过滤器来实现把session存储到redis的,然后每次请求都是从redis拿到session的,所以目标就是看它的过滤器是哪个,是怎么存储的,又是怎么获取的。
我们可以从它唯一的入口@EnableRedisHttpSession进入查看,它引入了一个RedisHttpSessionConfiguration开启了一个定时器,继承自SpringHttpSessionConfiguration,可以留意到RedisHttpSessionConfiguration创建一个BeanRedisOperationsSessionRepositoryrepository是仓库的意思,所以它就是核心类了,用于存储session;那过滤器在哪呢,查看SpringHttpSessionConfiguration它属于spring-session-core包,这是一个spring用来管理session的包,是一个抽象的概念,具体的实现由spring-session-data-redis来完成,那过滤器肯定在这里创建的,果然可以看到它创建一个SessionRepositoryFilter的过滤器,下面分别看过滤器和存储。
SessionRepositoryFilter
过滤器一定是有doFilter方法,查看doFilter方法,spring使用OncePerRequestFilter把doFilter包装了一层,最终是调用doFilterInternal来实现的,查看doFilterInternal方法
实现方式为使用了包装者设计把request和response响应进行了包装,我们一般拿session一般是从request.getSession(),所以包装的request肯定要重写getSession,所以可以看getSession方法来看是如何从redis获取session;
前面都是已经存在session的判断相关,关键信息在这里
Ssession=SessionRepositoryFilter.this.sessionRepository.createSession();
这里的sessionRepository就是我们用来存取session的RedisOperationsSessionRepository查看createSession方法
RedisOperationsSessionRepository
//这里保存了在redis中hash结构能看到的数据 RedisSessionredisSession=newRedisSession(); this(newMapSession()); this.delta.put(CREATION_TIME_ATTR,getCreationTime().toEpochMilli()); this.delta.put(MAX_INACTIVE_ATTR,(int)getMaxInactiveInterval().getSeconds()); this.delta.put(LAST_ACCESSED_ATTR,getLastAccessedTime().toEpochMilli()); this.isNew=true; this.flushImmediateIfNecessary();
在flushImmediateIfNecessary方法中,如果redisFlushMode是IMMEDIATE模式,则会立即保存session进redis,但默认配置的是ON_SAVE,那是在哪里保存进redis的呢,我们回到最开始的过滤器doFilterInternal方法中,在finally中有一句
wrappedRequest.commitSession();
就是在这里将session存储进redis的,我们跟进去看看,核心语句为这句
SessionRepositoryFilter.this.sessionRepository.save(session);
session.saveDelta(); if(session.isNew()){ StringsessionCreatedKey=getSessionCreatedChannel(session.getId()); this.sessionRedisOperations.convertAndSend(sessionCreatedKey,session.delta); session.setNew(false); }
进入saveDelta,在这里进行了hash结构的设置
getSessionBoundHashOperations(sessionId).putAll(this.delta);
最后一行进行了过期时间的设置和把当前key加入set,读者自行查看
RedisOperationsSessionRepository.this.expirationPolicy .onExpirationUpdated(originalExpiration,this);
修改一些参数
实际业务中,可能需要修改一些参数才能达到我们业务的需求,最常见的需求就是修改session的过期时间了,在EnableRedisHttpSession注解中,已经提供了一些基本的配置如
maxInactiveIntervalInSeconds最大过期时间,默认30分钟
redisNamespace插入到redis的session命名空间,默认是spring:session
cleanupCron过期session清理任务,默认是1分钟清理一次
redisFlushMode刷新方式,其实在上面原理的flushImmediateIfNecessary方法中有用到,默认是ON_SAVE
redisNamespace是一定要修改的,这个不修改会影响别的项目,一般使用我们项目的名称加关键字session做key,表明这是这个项目的session信息。
不过这样的配置明显不够,对于最大过期时间来说,有可能需要加到配置文件中去,而不是写在代码中,但是这里没有提供占位符的功能,回到RedisOperationsSessionRepository的创建,最终配置的maxInactiveIntervalInSeconds还是要设置到这个bean中去的,我们可以把这个bean的创建过程覆盖,重写maxInactiveIntervalInSeconds的获取过程,就解决了,代码如下
@Autowired RedisTemplatesessionRedisTemplate; @Autowired ApplicationEventPublisherapplicationEventPublisher; @Value("${server.session.timeout}") privateintsessionTimeout=1800; @Primary//使用Primary来覆盖默认的Bean @Bean publicRedisOperationsSessionRepositorysessionRepository(){ RedisOperationsSessionRepositorysessionRepository=newRedisOperationsSessionRepository(sessionRedisTemplate); //这里要把原来的属性引用过来,避免出错,可以引用原来的类并复制属性;像redisNamespace,redisFlushMode都要复制过来 returnsessionRepository; }
还有一个就是redis的序列化问题,默认是使用的jdk的对象序列化,很容易出现加一个字段或减少一个字段出现不能反序列化,所以序列化方式是需要换的,如果项目中的缓存就已经使用了对象序列化的话,那就面要为其单独写一个redisTemplate并设置进去,在构建RedisOperationsSessionRepository的时候设置redisTemplate
还有一个就是生成在redis中的key值都是uuid的形式,根本没办法知道当前这个key是哪个用户在哪里登录的,我们其实可以修改它的key为userId_ip_time的形式,用来表明这个用户什么时间在哪个ip有登录过,我是这么玩的(没有在实际中使用过,虽然能改,但可能有坑):
经过前面的源码分析,创建session并保存到redis的是RedisOperationsSessionRepository的createSession方法,但是这里写死了RedisSession使用空的构造,而且RedisSession是final的内部类,访问权限为默认,构造的时候newMapSession也是默认的,最终那个id为使用UUID,看起来一点办法都没有,其实在这里创建完session,用户不一定是登录成功的状态,我们应该在登录成功才能修改session的key,好在RedisOperationsSessionRepository提供了一个方法findById,我们可以在这个上面做文章,先把RedisSession查出来,然后用反射得到MapSession,然后留意到MapSession是可以修改id的,它自己也提供了方法changeSessionId,我们完全可以在登录成功调用setId修改sessionId,然后再写回去,这个代码一定要和RedisSession在同包代码如下:
packageorg.springframework.session.data.redis; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.session.MapSession; importorg.springframework.stereotype.Component; importorg.springframework.util.ReflectionUtils; importjava.lang.reflect.Field; @Component publicclassSessionOperation{ @Autowired privateRedisOperationsSessionRepositoryredisOperationsSessionRepository; publicvoidloginSuccess(StringuserId){ StringsessionId=((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest().getSession().getId(); RedisOperationsSessionRepository.RedisSessionredisSession=redisOperationsSessionRepository.findById(sessionId); Fieldcached=ReflectionUtils.findField(RedisOperationsSessionRepository.RedisSession.class,"cached"); ReflectionUtils.makeAccessible(cached); MapSessionmapSession=(MapSession)ReflectionUtils.getField(cached,redisSession); mapSession.setId("userId:1"); redisOperationsSessionRepository.save(redisSession); } }
源码地址:https://gitee.com/sanri/example/tree/master/test-redis-session
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。