Gitee
https://gitee.com/Lin_DH/system
介绍
【SpringBoot】30 Cookie、Session、Token https://blog.csdn.net/weixin_44088274/article/details/144241595
背景
Spring Session 是 Spring 的一个子项目,它提供了一种管理用户会话信息的方法,无论是在单服务的应用程序中,还是在分布式系统中,都能运用。Spring Session 提供了一种机制,可以将应用服务器的会话(HttpSession)抽象化,使得开发者可以在不同的环境下使用系统的 API 来管理会话。
Spring Session
原理
Spring Session 使支持集群会话变得简单,无需绑定到特定应用程序容器的解决方案。
- HttpSession:以通用的方式替代应用程序容器(如 Tomcat)中(Servlet 容器实现)的 HttpSession,实现了会话数据的外部存储,并支持在请求头(Header)中提供 sessionId,方便提供 RESTful API。
- WebSocket:提供在接收 WebSocket 消息时保持 HttpSession 活跃的能力。
- WebSession:允许以与应用程序容器无关的方式替换 Spring WebFlux 的 WebSession。
- 配置类:提供了几个关键配置类,如 RedisHttpSessionConfiguration 和 SpringHttpSessionConfiguration,用于配置 Redis 作为会话数据的存储源。
- SessionRepositoryFilter:SessionRepositoryFilter 是 Spring Session 的核心组件之一,它负责在请求处理之前和之后与会话存储进行交互,以确保会话数据的正确加载和保存。
组成
Spring Session Core:提供核心的 Spring 会话功能和 Api。
Spring Session Data Redis:提供 SessionRepository 和 ReactiveSessionRepository 的实现,支持 Redis 和配置。
Spring Session JDBC:提供由关系型数据库和配置支持所支持的 SessionRepository 实现。
Spring Session Hazelcast:提供由 Hazelcast 和配置支持所支持的 SessionRepository 实现。
Redis 实现分布式 Session
在分布式系统中,通常会将 Session 存储在 Redis 中来实现分布式 Session,这样可以在多台服务器之间共享 Session 数据。实现分布式 Session 通常使用 Redis 的 Hash 结构。
1)用户登录:当用户登录成功后,服务端会生成一个唯一的 SessionId(通常是一串随机字符串,如 UUID)。
2)存储 Session:将用户的会话信息(如用户ID,权限信息等)与 SessionId 关联,并存储在 Redis 中,Redis 使用 Hash 结构来存储这些数据,其中 SessionID 作为 Hash 的 Key,用户的会话信息作为 Hash 的 Value。
3)设置 Cookie:将 SessionId 通过 Cookie 发送到客户端浏览器,客户端每次请求时都需要携带上 Cookie。
4)请求处理:在客户端每次发起请求时,服务端会检查 Cookie 中的 SessionId,服务端用该 SessionId 从 Redis 中检索对应的 Session 数据。
5)认证与授权:通过检索到的 Session 数据,服务端可以验证用户身份,并根据用户的会话信息进行授权。
Session 过期:设置 Session 过期时间,当用户长时间不活动或者显式退出登录时,服务端会从 Redis 中删除对应的 Session 数据。
代码实现
依赖
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 实现对 Spring Session 使用 Redis 作为数据源的自动化配置 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- 实现对 Spring Data Redis 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!-- 去掉对 Lettuce 的依赖,因为 Spring Boot 优先使用 Lettuce 作为 Redis 客户端 -->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入 Jedis 的依赖,这样 Spring Boot 实现对 Jedis 的自动化配置 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
配置
application.yml
spring:
redis:
host: localhost
port: 6379
# password:
#Redis数据库号,默认为0
database: 0
#连接超时时间,单位为毫秒
timeout: 10
#对应 RedisProperties.Jedis 内部类
jedis:
pool:
#连接池最大连接数,默认为8,使用负数表示没有限制
max-active: 8
#默认连接池最大空闲的连接数,默认为8,使用负数表示没有限制
max-idle: 8
#默认连接池最小空闲的连接数,默认为0,允许设置0和正数
min-idle: 0
#连接池最大阻塞等待时间,单位为毫秒,默认为-1,表示不限制
max-wait: -1
添加配置类
SessionConfiguration.java
package com.lm.system.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* @author DUHAOLIN
* @date 2024/11/22
*/
@Configuration
@EnableRedisHttpSession // 自动化配置 Spring Session 使用 Redis 作为数据源
public class SessionConfiguration {
@Bean(name = "springSessionDefaultRedisSerializer")
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return RedisSerializer.json();
}
}
添加 @EnableRedisHttpSession 注解,开启自动化配置 Spring Session 使用 Redis 作为数据源。该注解有如下属性:
- maxInactiveIntervalInSeconds:Session 不活跃后的过期时间,默认为 1800 秒。
- redisNamespace:在 Redis Key 的统一前缀,默认为 “spring:session”。
- redisFlushMode:Redis 会话刷新模式。目前有两种,默认为 RedisFlushMode.ON_SAVE。
- RedisFlushMode.ON_SAVE:请求执行完成时,统一写入 Redis 存储。
- RedisFlushMode.IMMEDIATE:每次修改 Session 时,立即写入 Redis 存储。
- cleanupCron:清理 Redis Session 会话过期的任务执行 Cron 表达式,默认为 “0 * * * * *” 每分钟执行一次。虽说 Redis自带 Key 过期机制,但是默认该机制为惰性删除策略,实际过期的 Session 还保存在 Redis 的内存中。所以 Spring Session 通过定时任务,删除 Redis 中过期的 Session,使之尽快释放 Redis 的内存。
在 springSessionDefaultRedisSerializer() 方法中,定义了一个 Bean 名字为 springSessionDefaultRedisSerializer 的 RedisSerializer Bean,采用 JSON 序列化方式。默认情况下采用 Java 自带的序列化方式,可读性较差,所以需要进行替换。
配置 Redis 序列化
RedisConfig.java
package com.lm.system.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author DUHAOLIN
* @date 2024/11/13
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
//设置key序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置value序列化方式json,使用GenericJackson2JsonRedisSerializer替代默认序列化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
测试
SessionController.java
package com.lm.system.controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* @author DUHAOLIN
* @date 2024/11/22
*/
@RestController
@RequestMapping("session")
public class SessionController {
@PostMapping("set")
public void set(HttpSession session, @RequestParam("key") String key, @RequestParam("value") String value) {
session.setAttribute(key, value);
}
@GetMapping("getAll")
public Map<String, Object> getAll(HttpSession session) {
Map<String, Object> resultMap = new HashMap<>();
for(Enumeration<String> names = session.getAttributeNames(); names.hasMoreElements();) {
String key = names.nextElement();
Object value = session.getAttribute(key);
resultMap.put(key, value);
}
return resultMap;
}
}
效果图
启动程序后,进行查询,未查询到 Session 数据。
客户端访问完,到 Redis 客户端进行查询,则能够查询到 Spring Session 保存到 Redis 的 Session 数据。
每个 Session 对应 Redis 中 两个键值对。
- 头部:以 spring:session 开头,可以通过 @EnableRedisHttpSession 直接的 redisNamespace 属性进行配置。
- 结尾:以对应 Session 的 sessionId 结尾。
- 中间:中间分别是 session、expirations、sessions:expires 。
从下图该 session 的内容可以看出,其是一个 Redis Hash 数据结构。
客户端发起 set 请求,设置 key 和 value。
此时再次在 Redis 客户端进行查询,则该 Session 信息已经发生改变。
Redis 序列化
RedisSerializer
org.springframework.data.redis.serializer.RedisSerializer 接口,Redis 序列化接口,用于 Redis KEY 和 VALUE 的序列化。
RedisSerializer.java
public interface RedisSerializer<T> {
@Nullable
byte[] serialize(@Nullable T t) throws SerializationException;
@Nullable
T deserialize(@Nullable byte[] bytes) throws SerializationException;
}
定义了对象 和二进制数组的转换。
Redis Client 传递给 Redis Server 是传递的 KEY 和 VALUE 都是二进制值数组。
RedisSerializer 的实现类:
主要分为四类:JDK 序列化方式、String 序列化方式、JSON 序列化方式、XML 序列化方式。
JDK序列化方式
介绍
org.springframework.data.redis.serializer.JdkSerializationRedisSerializer ,默认情况下,RedisTemplate 使用该数据列化方式。
RedisTemplate#afterPropertiesSet() 方法,在 RedisTemplate 未设置序列化的情况下,使用 JdkSerializationRedisSerializer 作为序列化实现。在 Spring Boot 自动化配置 RedisTemplate Bean 对象时,就未设置。
绝大情况下,不会使用该序列化,如下图所示,写入的 KEY 前会携带 16 进制字符,而通过该 KEY 获取的 VALUE 也携带有 16 进制字符,获取的结果不方便阅读。
在 ObjectOutputStream#writeString(String str,boolean unshared) 代码中,实际是:标志位 + 字符串长度 + 字符串内容。
注:测试之前如果前面配置了序列化(RedisConfig.java)需要先注释 @Configuration 注解。
测试代码
RedisTest.java
package com.lm.system;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
/**
* @author DUHAOLIN
* @date 2024/11/29
*/
@SpringBootTest
@RunWith(value = SpringJUnit4ClassRunner.class)
public class RedisTest {
@Resource
private RedisTemplate redisTemplate;
@Test
public void test01() {
redisTemplate.opsForValue().set("name", "Jack");
}
}
效果图
String序列化方式
org.springframework.data.redis.serializer.StringRedisSerializer,字符串和二进制数组的直接转换。
StringRedisSerializer.java
private final Charset charset;
@Override
public String deserialize(@Nullable byte[] bytes) {
return (bytes == null ? null : new String(bytes, charset));
}
@Override
public byte[] serialize(@Nullable String string) {
return (string == null ? null : string.getBytes(charset));
}
绝大多数情况下,KEY 和 VALUE 使用的此序列化方式。而 VALUE 的序列化和反序列化,需要开发人员自己在逻辑调用 JSON 方法来序列化。
org.springframework.data.redis.serializer.GenericToStringSerializer ,使用 Spring ConversionService 实现 对象和 String 的转换,从而 String 和二进制数组的转换。
序列化的过程,首先 对象通过 ConversionService 转换成 String ,然后 String 再序列化成二进制数组。
JSON 序列化方式
1)org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer,使用 Jackson 实现 JSON 的序列化方式是支持所有类。
GenericJackson2JsonRedisSerializer.java
public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {
this(new ObjectMapper());
// simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need
// the type hint embedded for deserialization using the default typing feature.
mapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));
//<1>
if (StringUtils.hasText(classPropertyTypeName)) {
mapper.enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, classPropertyTypeName);
//<2>
} else {
mapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY);
}
}
<1> 如果传入 classPropertyTypeName 属性,使用传入对象的 classPropertyTypeName 属性对应的值,作为默认类型(Default Typing)。
<2> 如果未传入 classPropertyTypeName 属性,则使用传入对象的类全名,作为默认类型(Default Typing)。
2)org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer,使用 Jackson 实现 JSON 的序列化方式,并且显示指定 类型。
Jackson2JsonRedisSerializer.java
public class Jackson2JsonRedisSerializer<T> implements RedisSerializer<T> {
//...
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
/**
* 指定类型,和 <T> 要一致。
*/
private final JavaType javaType;
private ObjectMapper objectMapper = new ObjectMapper();
}
Jackson2JsonRedisSerializer 序列化类里已经声明了类型,所以序列化的 JSON 字符串,无需在存储一个 @class 属性,用于存储类型。
3)com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer,使用 FastJSON 实现 JSON 的序列化方式,和 GenericJackson2JsonRedisSerializer 一致。
4)com.alibaba.fastjson.support.spring.FastJsonRedisSerializer,使用 FastJSON 实现 JSON 的序列化方式,和 Jackson2JsonRedisSerializer 一致。
XML序列化方式
org.springframework.data.redis.serializer.OxmSerializer,使用 Spring OXM 实现将对象和 String 的转换,从而 String 和二进制数组的转换。
参考链接
【Spring Boot 分布式 Session 入门】https://www.iocoder.cn/Spring-Boot/Distributed-Session/?self