Spring Security Session 序列化策略分析

Spring Security Session 序列化策略分析:为什么不是 JSON?

一、前言

在分布式 session 管理中,序列化是核心环节。本文深入分析 Spring Security/Spring Session 的序列化策略选择逻辑

二、Spring Session 序列化演进史

版本 默认序列化器 特点
Spring Session 1.x JDK 序列化 简单直接
Spring Boot 1.x JDK 序列化 兼容性优先
Spring Boot 2.x Jackson (JSON) 性能+可读性
Spring Boot 3.x + Spring Session 2.x Jackson/Kryo 多选择

三、Spring Security 不默认用 JSON 的深层原因

3.1 安全性考量

java 复制代码
// JDK 序列化存在反序列化漏洞风险
// Spring Security 需要处理 Authentication 对象
public interface Authentication extends Serializable {
    Object getPrincipal();
    Object getCredentials();
    Collection<? extends GrantedAuthority> getAuthorities();
}

问题场景:

java 复制代码
// 如果使用 JSON,Authentication 的 credentials 可能包含敏感信息
{
  "principal": "user",
  "credentials": "password123",  // 敏感!
  "authenticated": true
}

Spring Security 的策略:

  • CredentialsContainer 接口强制在序列化前清除凭证
  • JDK 序列化可以控制哪些字段被序列化

3.2 类型完整性

复制代码
Authentication 接口体系:
├── AbstractAuthenticationToken
│   └── UsernamePasswordAuthenticationToken
├── TestingAuthenticationToken
├── RememberMeAuthenticationToken
└── 自定义实现(如 ProxyAuthenticationToken)

JSON 序列化痛点:

java 复制代码
// 多态类型反序列化问题
ObjectMapper mapper = new ObjectMapper();
mapper.readValue(json, Authentication.class);
// ❌ 无法正确反序列化为具体子类型

// 必须配置:
mapper.activateDefaultTyping(
    BasicPolymorphicTypeValidator.builder()
        .allowIfBaseType(Object.class)
        .allowIfSubType(Object.class)
        .build(),
    ObjectMapper.DefaultTyping.NON_FINAL,
    "@class"
);

3.3 性能与兼容性权衡

指标 JDK 序列化 JSON Kryo
序列化速度 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
序列化大小
反序列化速度 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
安全性
兼容性 严格 灵活

Spring 的选择逻辑:

  1. Spring Boot 1.x: JDK 序列化,兼容性优先(当时 JSON 库版本混乱)
  2. Spring Boot 2.x: 切换 Jackson,性能+生态成熟
  3. Spring Session 2.x: 多种序列化器可选,开发者自行选择

3.4 Session 数据特点

java 复制代码
// Spring Session 存储的典型数据
Session session = new MapSession();
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
// securityContext.authentication 是核心

Session 数据的特点:

  • 读多写少:session 创建后读取频繁
  • 对象复杂:嵌套 Authentication、GrantedAuthority 集合
  • 安全敏感:principal/credentials 需要特殊处理
  • 生命周期长:可能存在数小时甚至数天

3.5 Spring Session 的默认策略

java 复制代码
// Spring Session Redis 配置
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class RedisSessionConfig {
    // Spring Boot 自动配置会使用:
    // 1. JacksonJsonRedisSerializer (Spring Boot 2.x)
    // 2. 或开发者自定义的序列化器
}

开发者可以自定义:

java 复制代码
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
    // Kryo - 性能最优
    return new KryoRedisSerializer<>(Object.class);

    // 或 Hessian - 跨语言支持
    return new HessianRedisSerializer<>(Object.class);

    // 或自定义 Jackson - 可读性好
    return new Jackson2JsonRedisSerializer<>(Object.class);
}

四、实际项目中的最佳实践

4.1 推荐方案:Kryo 序列化

xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>5.6.0</version>
</dependency>
java 复制代码
@Configuration
public class KryoSessionConfig {

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new KryoRedisSerializer<>();
    }

    @Bean
    public HttpSessionIdResolver httpSessionIdResolver() {
        return new CookieHttpSessionIdResolver();
    }
}
java 复制代码
// KryoRedisSerializer 实现
public class KryoRedisSerializer<T> implements RedisSerializer<T> {

    private static final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
        Kryo kryo = new Kryo();
        // 不要求必须存在无参构造函数
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
        kryo.setRegistrationRequired(false);

        // 注册 Security 相关类
        kryo.register(Authentication.class);
        kryo.register(GrantedAuthority.class);
        kryo.register(AbstractAuthenticationToken.class);

        return kryo;
    });

    @Override
    public byte[] serialize(T obj) {
        if (obj == null) return null;
        Kryo kryo = kryoThreadLocal.get();
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             Output output = new Output(baos)) {
            kryo.writeClassAndObject(output, obj);
            output.flush();
            return baos.toByteArray();
        } catch (IOException e) {
            throw new SerializationException("kryo serialize error", e);
        }
    }

    @Override
    public T deserialize(byte[] bytes) {
        if (bytes == null || bytes.length == 0) return null;
        Kryo kryo = kryoThreadLocal.get();
        try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
             Input input = new Input(bais)) {
            return (T) kryo.readClassAndObject(input);
        } catch (Exception e) {
            throw new KryoSerializationException("序列化失败");
        }
    }
}

4.2 确保 Token 类可序列化

4.3 配置说明

yaml 复制代码
spring:
  session:
    store-type: redis
    redis:
      namespace: spring:session
      # 配置超时时间
    timeout: 30m

五、总结:为什么不是 JSON?

考量维度 JDK JSON Kryo Spring 选择
安全性 ⚠️ 历史原因
类型完整性 ⚠️
性能 ⚠️
可读性
跨语言 ⚠️
Spring 生态 早期默认 Spring Boot 2.x+ 可选 演进选择

核心结论:

  1. 历史原因:Spring 1.x/2.x 早期,JSON 库生态不成熟
  2. 安全性:Spring Security 优先考虑安全性,JDK 序列化更可控
  3. 类型系统:JDK 序列化能保持完整的类型信息
  4. 当前最佳实践:Spring Boot 2.x+ 推荐使用 Jackson 或 Kryo

最终建议:

  • Spring Boot 2.x+ 应用:使用默认 Jackson 或切换到 Kryo
  • 高性能场景:使用 Kryo
  • 跨语言场景:使用 Hessian 或 JSON

参考文档:

相关推荐
__万波__2 小时前
二十三种设计模式(十六)--迭代器模式
java·设计模式·迭代器模式
IT 行者2 小时前
Spring Boot 4.0 整合Spring Security 7 后的统一异常处理指南
spring boot·后端·spring
学博成3 小时前
在 Spring Boot 中使用 Kafka 并保证顺序性(Topic 分区为 100)的完整案例
spring boot·kafka
無欲無为4 小时前
Spring Boot 整合 RabbitMQ 详细指南:从入门到实战
spring boot·rabbitmq·java-rabbitmq
掘根4 小时前
【消息队列项目】客户端四大模块实现
开发语言·后端·ruby
NAGNIP10 小时前
多个 GitHub 账户SSH 密钥配置全攻略
后端
NAGNIP10 小时前
Windows命令行代码自动补全详细步骤
后端
.鸣11 小时前
set和map
java·学习
追逐时光者11 小时前
精选 8 款 .NET 开源、前后端分离的快速开发框架,提高开发生产效率!
后端·.net