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

参考文档:

相关推荐
一条Go5 分钟前
两套权限目录,三重闸门:Alioth 的多租户 RBAC 为什么这么拧
后端·程序员·github
Tokenfree22 分钟前
把多模态 OCR 接进业务系统:四千张运单,三个坑
后端
十碗饭吃不饱22 分钟前
后端运维:网络联通、进程查询、Java服务启动脚本
java·运维·网络
用户8593130613529 分钟前
我给 AI Agent 写了个下载器:293KB 的网页,不再吃掉 7 万 token
后端
T_Wizard29 分钟前
后端转 AI 应用开发,我先把这 12 个名词理清了:一张图看懂架构、RAG、Tool Calling 与 MCP
java·ai编程
杨航 AI42 分钟前
JEV的用法
java·前端·数据库
灿飞1 小时前
Kotlin与Java虚拟机七:空安全:把十亿美元的错误消灭在类型系统里
java·kotlin
phil61 小时前
沙箱核心功能系列 06:编排调度与资源管理
后端
小兔子1 小时前
if (system(cmd) != 0) 判不出命令失败:它返回的是等待状态,不是退出码
后端
艾迪的技术之路1 小时前
16k 条慢查询,128 张分表,我这么解决
后端·github