Tips:你的单例对象能被反射吗😊,能被反序列化吗😊,如果做不好防护,那么你的单例对象就不再是独一无二,而会多一个同胞兄弟了😄😄😄
01 单例模式简介
- 单例模式(Singleton Pattern)是一种创建型设计模式,旨在确保某个类只有一个实例,并提供一个全局访问点。
02 常用单例模式
饿汉式(Eager Initialization)
java
public class EagerSingleton implements Serializable {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {
// 私有构造函数
}
public static EagerSingleton getInstance() {
return instance;
}
}
懒汉式(Lazy Initialization)
java
public class LazySingleton implements Serializable {
private static LazySingleton instance;
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
双重检查锁定(Double-Checked Locking)
java
public class DoubleCheckedSingleton implements Serializable {
private static volatile DoubleCheckedSingleton instance;
private DoubleCheckedSingleton() {}
public static DoubleCheckedSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckedSingleton.class) {
if (instance == null) {
instance = new DoubleCheckedSingleton();
}
}
}
return instance;
}
}
静态内部类(Static Inner Class)
java
public class InnerClassSingleton implements Serializable {
private InnerClassSingleton() {}
private static class SingletonHolder {
private static final InnerClassSingleton instance = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return SingletonHolder.instance;
}
}
枚举(Enum)
java
public enum EnumSingleton implements Serializable {
INSTANCE;
public void doSomething() {
// 业务方法
}
}
03 缺陷
- 无法应对反射、反序列化的考验~~
java
public class MainTest {
public static void main(String[] args) throws Exception {
testReflectionAttack(LazySingleton.class, LazySingleton.getInstance());
testReflectionAttack(EagerSingleton.class, EagerSingleton.getInstance());
testReflectionAttack(DoubleCheckedSingleton.class, DoubleCheckedSingleton.getInstance());
testReflectionAttack(InnerClassSingleton.class, InnerClassSingleton.getInstance());
testReflectionAttack(EnumSingleton.class, EnumSingleton.INSTANCE);
testSerializationAttack(LazySingleton.getInstance());
testSerializationAttack(EagerSingleton.getInstance());
testSerializationAttack(DoubleCheckedSingleton.getInstance());
testSerializationAttack(InnerClassSingleton.getInstance());
testSerializationAttack(EnumSingleton.INSTANCE);
}
/**
* 测试反射攻击单例
*/
public static <T> void testReflectionAttack(Class<T> singletonClass, T originalInstance) {
System.out.println("\n=== 测试反射攻击: " + singletonClass.getSimpleName() + " ===");
try {
// 获取私有构造函数
Constructor<T> constructor = singletonClass.getDeclaredConstructor();
constructor.setAccessible(true); // 突破私有访问限制
// 尝试通过反射创建新实例
T reflectedInstance = constructor.newInstance();
System.out.println("原始实例: " + originalInstance.hashCode());
System.out.println("反射实例: " + reflectedInstance.hashCode());
System.out.println("是否相同: " + (originalInstance == reflectedInstance));
System.out.println("反射攻击: " + (originalInstance != reflectedInstance ? "成功 ✅" : "失败 ❌"));
} catch (Exception e) {
System.out.println("反射攻击失败: " + e.getMessage());
System.out.println("反射攻击: 被防护 ❌");
}
}
/**
* 测试序列化/反序列化攻击
*/
public static <T> void testSerializationAttack(T instance) {
System.out.println("\n=== 测试序列化攻击: " + instance.getClass().getSimpleName() + " ===");
try {
// 模拟序列化和反序列化
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(baos);
oos.writeObject(instance);
oos.close();
java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(baos.toByteArray());
java.io.ObjectInputStream ois = new java.io.ObjectInputStream(bais);
Object deserializedInstance = ois.readObject();
ois.close();
System.out.println("原始实例: " + instance.hashCode());
System.out.println("反序列化实例: " + deserializedInstance.hashCode());
System.out.println("是否相同: " + (instance == deserializedInstance));
System.out.println("反序列化攻击: " +
(instance != deserializedInstance ? "成功 ✅" : "失败 ❌"));
} catch (Exception e) {
System.out.println("序列化测试异常: " + e.getMessage());
}
}
}
输出结果:
shell
=== 测试反射攻击: LazySingleton ===
原始实例: 1029991479
反射实例: 670700378
是否相同: false
反射攻击: 成功 ✅
=== 测试反射攻击: EagerSingleton ===
原始实例: 728890494
反射实例: 1558600329
是否相同: false
反射攻击: 成功 ✅
=== 测试反射攻击: DoubleCheckedSingleton ===
原始实例: 445051633
反射实例: 1051754451
是否相同: false
反射攻击: 成功 ✅
=== 测试反射攻击: InnerClassSingleton ===
原始实例: 1147985808
反射实例: 2040495657
是否相同: false
反射攻击: 成功 ✅
=== 测试反射攻击: EnumSingleton ===
反射攻击失败: com.dhz.study.demo.design.singleton.EnumSingleton.<init>()
反射攻击: 被防护 ❌
=== 测试序列化攻击: LazySingleton ===
原始实例: 1029991479
反序列化实例: 920011586
是否相同: false
反序列化攻击: 成功 ✅
=== 测试序列化攻击: EagerSingleton ===
原始实例: 728890494
反序列化实例: 818403870
是否相同: false
反序列化攻击: 成功 ✅
=== 测试序列化攻击: DoubleCheckedSingleton ===
原始实例: 445051633
反序列化实例: 398887205
是否相同: false
反序列化攻击: 成功 ✅
=== 测试序列化攻击: InnerClassSingleton ===
原始实例: 1147985808
反序列化实例: 2047526627
是否相同: false
反序列化攻击: 成功 ✅
=== 测试序列化攻击: EnumSingleton ===
原始实例: 718231523
反序列化实例: 718231523
是否相同: true
反序列化攻击: 失败 ❌
安全性总结
实现方式 | 反射安全 | 反序列化安全 | 需要额外防护 |
---|---|---|---|
饿汉式 | ❌ | ❌ | ✅ |
懒汉式 | ❌ | ❌ | ✅ |
双重检查 | ❌ | ❌ | ✅ |
静态内部类 | ❌ | ❌ | ✅ |
枚举 | ✅ | ✅ | ❌ |
可以看到,除了枚举类型的单例,其他几种方式我们都可以通过反射和反序列化来创建新的实例使用,这显然是不合适的。
04 优化方案
对于反序列化而言,修复方式很简单,因为反序列化时,会调用对象的readResolve()方法,只要我们重写这个方法,就可以避免反序列化产生新的对象,以懒汉式实现的方式为例,以下是修复后的逻辑:
java
public class LazySingleton implements Serializable {
private static LazySingleton instance;
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
// 防止反序列化攻击
private Object readResolve() {
return instance;
}
}
当我们再次调用反序列测试用例,可以看到单例对象和反序列化对象已经是同一个对象了(此处略去调用过程)。
而反射的情况就比较复杂了,有同学可能会说,可以定义一个常量,让构造方法只初始化一次不就可以了吗,类似于下面的情况:
java
public class LazySingleton implements Serializable {
private static final long serialVersionUID = 1L;
// 初始化常量,默认false,单例初始化后改为true
private static boolean initialized = false;
private static LazySingleton instance;
private LazySingleton() {
if (initialized) {
throw new RuntimeException("单例模式禁止通过反射创建实例");
}
initialized = true;
}
public static synchronized LazySingleton getInstance() {
if (instance != null) {
return instance;
}
instance = new LazySingleton();
return instance;
}
// 防止反序列化攻击
private Object readResolve() {
return instance;
}
}
这里我们通过定义了常量initialized来控制构造方法只会初始化一次,当调用完getInstance()方法后,已经将initialized属性置为了true,再次通过反射来调用构造方法时,会抛异常出来,禁止调用。
但这也只是提高了反射的难度而已,并没有完全做到反射防护的情况,调用方完全可以在反射时先将initialized置为true,然后再调用构造方法,所以这种方案也不能完全解决以上问题。
那除了枚举就没有其他的方案来构造单例了吗,有的,下面我给大家推荐两种。
- 基于类加载机制的防护
java
public class InnerClassSingleton implements Serializable {
private static class SingletonHolder {
static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
private InnerClassSingleton() {
if (SingletonHolder.INSTANCE != null) {
throw new IllegalStateException("单例实例已经存在,禁止通过反射创建");
}
}
}
- 使用 RuntimePermission 的安全管理器方案
java
public final class SecurityManagerSingleton implements Serializable {
private static volatile SecurityManagerSingleton instance;
private SecurityManagerSingleton() {
// 安全检查:检查是否有反射访问权限
checkReflectionPermission();
}
public static SecurityManagerSingleton getInstance() {
if (instance == null) {
synchronized (SecurityManagerSingleton.class) {
if (instance == null) {
instance = AccessController.doPrivileged(
(PrivilegedAction<SecurityManagerSingleton>) () -> new SecurityManagerSingleton()
);
}
}
}
return instance;
}
/**
* 检查反射权限
*/
private static void checkReflectionPermission() {
SecurityManager securityManager = System.getSecurityManager();
if (securityManager != null) {
// 检查反射调用的权限
securityManager.checkPermission(new RuntimePermission("accessDeclaredMembers"));
securityManager.checkPermission(new RuntimePermission("reflectionFactoryAccess"));
} else {
// 如果没有安全管理器,则通过检查调用栈的方式来判断
checkCallStack();
}
}
/**
* 检查调用栈
*/
private static void checkCallStack() {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
// 检查调用栈中是否有反射相关的调用
for (int i = 0; i < Math.min(stackTrace.length, 8); i++) {
String className = stackTrace[i].getClassName();
String methodName = stackTrace[i].getMethodName();
if (className.startsWith("sun.reflect.") ||
className.startsWith("jdk.internal.reflect.") ||
methodName.equals("newInstance")) {
throw new SecurityException("检测到反射调用,禁止创建单例实例");
}
}
}
}
总结
当然了,我们平时的业务其实用不到这么复杂的实现逻辑,本文章只是单纯从技术的层面上和大家进行探讨,就当做技术交流,一般情况下使用枚举或其他几种单例就能够满足我们的业务需求。
码字不易,帮忙点赞收藏一下吧,感谢大家的支持! 😊😊😊