一、Java序列化与反序列化的工作原理

Java序列化就是把Java对象转换为字节序列的过程,反序列化则是把字节序列恢复为Java对象的过程。这就像把一本书变成电子文件(序列化),需要时再打印成纸质书(反序列化)。

// 技术栈:Java原生序列化
import java.io.*;

// 可序列化的用户类
class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;
    private transient String password; // transient修饰的字段不会被序列化
    
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
    
    @Override
    public String toString() {
        return "User{username='" + username + "', password='" + password + "'}";
    }
}

public class SerializationDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 序列化
        User user = new User("admin", "123456");
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(user);
        oos.close();
        byte[] bytes = bos.toByteArray();
        
        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
        ObjectInputStream ois = new ObjectInputStream(bis);
        User deserializedUser = (User) ois.readObject();
        System.out.println(deserializedUser); // 输出:User{username='admin', password='null'}
    }
}

从示例中可以看到,password字段因为被transient修饰,反序列化后变成了null。serialVersionUID用于版本控制,如果序列化和反序列化的类版本不一致,会抛出InvalidClassException。

二、常见的安全风险及攻击方式

1. 反序列化漏洞攻击

攻击者可以构造恶意的序列化数据,当程序反序列化这些数据时,会执行攻击者预设的危险操作。这就像收到一个看似普通的快递,打开后却发现是炸弹。

// 危险示例:可能被利用的类
class Dangerous implements Serializable {
    private String command;
    
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        Runtime.getRuntime().exec(command); // 反序列化时执行系统命令
    }
}

2. 数据篡改风险

序列化数据可以被中间人修改,导致反序列化后得到错误或危险的对象。比如把用户角色从"普通用户"改为"管理员"。

3. 拒绝服务攻击

攻击者可以发送特别构造的序列化数据,消耗大量服务器资源。例如包含深度嵌套的对象图,导致堆栈溢出。

三、防护措施与实践方案

1. 使用安全的替代方案

JSON和XML等格式比Java原生序列化更安全:

// 技术栈:Jackson JSON库
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonSerialization {
    public static void main(String[] args) throws IOException {
        User user = new User("admin", "123456");
        ObjectMapper mapper = new ObjectMapper();
        
        // 序列化为JSON
        String json = mapper.writeValueAsString(user);
        System.out.println(json); // 输出:{"username":"admin"}
        
        // 从JSON反序列化
        User deserialized = mapper.readValue(json, User.class);
        System.out.println(deserialized); // 输出:User{username='admin', password='null'}
    }
}

2. 白名单验证

只允许反序列化可信的类:

public class SecureObjectInputStream extends ObjectInputStream {
    private static final Set<String> ALLOWED_CLASSES = 
        Set.of("java.lang.String", "com.example.User");
    
    public SecureObjectInputStream(InputStream in) throws IOException {
        super(in);
    }
    
    protected Class<?> resolveClass(ObjectStreamClass desc) 
        throws IOException, ClassNotFoundException {
        if (!ALLOWED_CLASSES.contains(desc.getName())) {
            throw new InvalidClassException("Unauthorized deserialization attempt", desc.getName());
        }
        return super.resolveClass(desc);
    }
}

3. 加密和签名

对序列化数据进行加密和签名,防止篡改:

// 技术栈:Java加密体系
import javax.crypto.*;
import java.security.*;
import java.util.Base64;

public class SecureSerialization {
    public static void main(String[] args) throws Exception {
        // 生成密钥
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(256);
        SecretKey secretKey = keyGen.generateKey();
        
        User user = new User("admin", "123456");
        
        // 序列化+加密
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        CipherOutputStream cos = new CipherOutputStream(bos, cipher);
        ObjectOutputStream oos = new ObjectOutputStream(cos);
        oos.writeObject(user);
        oos.close();
        String encrypted = Base64.getEncoder().encodeToString(bos.toByteArray());
        
        // 解密+反序列化
        byte[] decoded = Base64.getDecoder().decode(encrypted);
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        CipherInputStream cis = new CipherInputStream(new ByteArrayInputStream(decoded), cipher);
        ObjectInputStream ois = new ObjectInputStream(cis);
        User decryptedUser = (User) ois.readObject();
        System.out.println(decryptedUser);
    }
}

四、应用场景与最佳实践

1. 适用场景

  • 远程方法调用(RMI)
  • 会话持久化
  • 深度复制对象
  • 缓存对象到磁盘

2. 不适用场景

  • 跨语言通信
  • 长期数据存储
  • 不受信的数据源

3. 最佳实践

  1. 尽量避免使用Java原生序列化
  2. 如果必须使用,实现严格的类白名单
  3. 对敏感字段使用transient修饰
  4. 保持serialVersionUID的一致性
  5. 考虑使用第三方安全库如SerialKiller

4. 关联技术:Kryo序列化

Kryo是高效的Java序列化框架,比Java原生序列化更快更安全:

// 技术栈:Kryo库
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;

public class KryoSerialization {
    public static void main(String[] args) throws Exception {
        Kryo kryo = new Kryo();
        kryo.register(User.class);
        
        User user = new User("admin", "123456");
        
        // 序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Output output = new Output(bos);
        kryo.writeObject(output, user);
        output.close();
        byte[] bytes = bos.toByteArray();
        
        // 反序列化
        Input input = new Input(new ByteArrayInputStream(bytes));
        User deserialized = kryo.readObject(input, User.class);
        input.close();
        System.out.println(deserialized);
    }
}

五、总结与建议

Java序列化虽然方便,但存在严重安全隐患。在必须使用的场景下,应采取严格的安全措施。对于新项目,建议优先考虑JSON、Protocol Buffers等更安全的替代方案。安全无小事,特别是在处理不受信任的数据源时,必须格外谨慎。