目录

    Phoenix监控平台技术解析(六):数据加解密体系——AES、DES、SM4 国密算法的统一抽象

    前几篇我们走过了 HTTP 通信通道、WebSocket 长连接、服务端引导流程和客户端重连机制。在这些数据流动的背后,始终有一个角色在默默守护——加解密体系。第二篇中我们见过 MsgPayloadUtils 的加密压缩流程,第三篇中提到 WebSocket 通道上的数据“同样是加密传输的”。但加密到底是怎么做的?密钥从哪来?三种算法之间怎么切换?今天我们就来拆解 Phoenix 的数据加解密架构——一个用接口、枚举和继承搭建的“密码学抽象层”。


    一、为什么要做统一抽象?

    Phoenix 支持三种对称加密算法:AESDESSM4(国密)。用户可以根据自己的合规要求和安全策略,在配置文件中选择其中一种。

    这里有一个很现实的工程问题:如果加密逻辑散落在各个调用点——HTTP 发送时调一下 AesEncryptUtils.encrypt(),WebSocket 发送时再调一下——那当用户切换加密算法时,所有调用点都要改。更糟糕的是,如果某个调用点忘了改,就会出现“一半数据用 AES 加密,一半用 DES 加密”的灾难。

    Phoenix 的解法很经典:用一层抽象,隔离算法选择和算法实现。调用方只需要说“给我加密”,具体用哪种算法,由配置决定、由抽象层分发。调用方不需要、也不应该关心底层用的是 AES 还是 SM4。

    二、四层架构:从接口到配置

    整个加解密体系可以分为四层,从上到下依次是:

    ┌─────────────────────────────────┐
    │         调用层(业务代码)         │   SecureUtils.encrypt() / decrypt()
    ├─────────────────────────────────┤
    │         路由层(算法分发)         │   SecurerEnums.valueOf(SECRET_TYPE)
    ├─────────────────────────────────┤
    │         实现层(具体算法)         │   AesEncryptUtils / DesEncryptUtils / Sm4EncryptUtils
    ├─────────────────────────────────┤
    │         初始化层(配置加载)       │   InitSecure(静态块 + 反射)
    └─────────────────────────────────┘
    

    每一层只依赖它下面那一层,职责清晰、互不干扰。接下来我们从底层开始,逐层往上拆解。

    三、ISecurer:加解密的契约

    一切从一个接口开始:

    public interface ISecurer {
    
        String encrypt(String str, Charset charset);
    
        String encrypt(byte[] arry);
    
        String decrypt(String str, Charset charset);
    
        byte[] decrypt(String str);
    }
    

    四个方法,两对对称操作:

    方法 输入 输出 用途
    encrypt(String, Charset) 明文字符串 + 字符集 密文字符串(Base64) 常规文本加密
    encrypt(byte[]) 原始字节数组 密文字符串(Base64) 压缩后的二进制数据加密
    decrypt(String, Charset) 密文字符串 + 字符集 明文字符串 常规文本解密
    decrypt(String) 密文字符串 原始字节数组 需要解压的数据解密

    为什么需要两组重载?回忆一下第二篇博客中 MsgPayloadUtils 的逻辑:小数据直接加密(字符串进、字符串出),大数据先 Gzip 压缩成字节数组再加密(字节数组进、字符串出)。对称地,解密时也需要两条路径——直接解密成字符串,或者解密成字节数组再解压。

    这个接口虽然只有四个方法,但它定义了整个加解密体系的契约——任何加密算法,只要实现了这四个方法,就可以无缝接入 Phoenix 的通信管线。

    四、SecurerEnums:枚举实现接口的精妙设计

    在 Java 中,枚举可以实现接口——这是一个很多人知道但不常用的特性。Phoenix 正是利用这个特性,把三种算法的路由做得异常简洁:

    public enum SecurerEnums implements ISecurer {
    
        DES {
            @Override
            public String encrypt(String str, Charset charset) {
                return DesEncryptUtils.encrypt(str, charset);
            }
    
            @Override
            public String encrypt(byte[] arry) {
                return DesEncryptUtils.encrypt(arry);
            }
    
            @Override
            public String decrypt(String str, Charset charset) {
                return DesEncryptUtils.decrypt(str, charset);
            }
    
            @Override
            public byte[] decrypt(String str) {
                return DesEncryptUtils.decrypt(str);
            }
        },
    
        AES {
            @Override
            public String encrypt(String str, Charset charset) {
                return AesEncryptUtils.encrypt(str, charset);
            }
            // ... 同样的四个方法,委托给 AesEncryptUtils
        },
    
        SM4 {
            @Override
            public String encrypt(String str, Charset charset) {
                return Sm4EncryptUtils.encrypt(str, charset);
            }
            // ... 同样的四个方法,委托给 Sm4EncryptUtils
        }
    }
    

    每个枚举常量都是 ISecurer 的一个完整实现——它不直接做加解密,而是委托给对应的工具类。这种设计有几个值得玩味的点:

    第一,枚举天然是单例SecurerEnums.AES 在 JVM 中只存在一份,不需要 static final 变量,不需要单例模式,Java 语言规范直接保证。

    第二,枚举名即配置值。用户在配置文件里写 monitoring.secure.encryption-algorithm-type=aes,代码里用 SecurerEnums.valueOf("AES") 就能拿到对应的实现。枚举名和配置值之间的映射是零代码的——不需要 if-else,不需要 MapvalueOf() 天然完成了这件事。

    第三,编译期安全。如果有人在配置文件里写了 monitoring.secure.encryption-algorithm-type=rsaSecurerEnums.valueOf("RSA") 会抛出 IllegalArgumentException——在运行的最早时机暴露配置错误。

    为什么不直接在枚举里写加解密逻辑,而要委托给独立的工具类?因为关注点分离。枚举负责“路由”——根据算法名字找到对应的实现;工具类负责“执行”——真正的加解密操作。如果把所有逻辑堆在枚举里,SecurerEnums 会变成一个几百行的巨型文件,可读性和可维护性都会下降。

    五、三个工具类:殊途同归的实现

    5.1 AesEncryptUtils

    public class AesEncryptUtils extends InitSecure {
    
        public static String encrypt(String str, Charset charset) {
            byte[] key = Base64.getDecoder().decode(SECRET_KEY_AES);
            return SecureUtil.aes(key).encryptBase64(str, charset);
        }
    
        public static String encrypt(byte[] arry) {
            byte[] key = Base64.getDecoder().decode(SECRET_KEY_AES);
            return SecureUtil.aes(key).encryptBase64(arry);
        }
    
        public static String decrypt(String str, Charset charset) {
            byte[] key = Base64.getDecoder().decode(SECRET_KEY_AES);
            return SecureUtil.aes(key).decryptStr(str, charset);
        }
    
        public static byte[] decrypt(String str) {
            byte[] key = Base64.getDecoder().decode(SECRET_KEY_AES);
            return SecureUtil.aes(key).decrypt(str);
        }
    }
    

    5.2 DesEncryptUtils

    public class DesEncryptUtils extends InitSecure {
    
        public static String encrypt(String str, Charset charset) {
            byte[] key = Base64.getDecoder().decode(SECRET_KEY_DES);
            return SecureUtil.des(key).encryptBase64(str, charset);
        }
    
        public static String decrypt(String str, Charset charset) {
            byte[] key = Base64.getDecoder().decode(SECRET_KEY_DES);
            return SecureUtil.des(key).decryptStr(str, charset);
        }
        // ... 其余两个方法结构相同
    }
    

    5.3 Sm4EncryptUtils

    public class Sm4EncryptUtils extends InitSecure {
    
        public static String encrypt(String str, Charset charset) {
            byte[] key = Base64.getDecoder().decode(SECRET_KEY_SM4);
            return SmUtil.sm4(key).encryptBase64(str, charset);
        }
    
        public static String decrypt(String str, Charset charset) {
            byte[] key = Base64.getDecoder().decode(SECRET_KEY_SM4);
            return SmUtil.sm4(key).decryptStr(str, charset);
        }
        // ... 其余两个方法结构相同
    }
    

    三个工具类的代码结构几乎一模一样,差异只在三个地方:

    AES DES SM4
    密钥字段 SECRET_KEY_AES SECRET_KEY_DES SECRET_KEY_SM4
    Hutool 工厂方法 SecureUtil.aes(key) SecureUtil.des(key) SmUtil.sm4(key)
    底层算法 AES/ECB/PKCS5Padding DES/ECB/PKCS5Padding SM4/ECB/PKCS5Padding

    有几个共性值得关注:

    第一,全部继承 InitSecure。三个工具类都 extends InitSecure,这意味着它们可以直接访问父类的 static final 字段——SECRET_KEY_AESSECRET_KEY_DESSECRET_KEY_SM4。继承在这里不是为了多态,而是为了共享静态常量

    第二,密钥是 Base64 编码存储的。配置文件中的密钥是 Base64 字符串(如 pqdOWnEkA3AQKyb7L2ewAA==),使用时先 Base64.getDecoder().decode() 还原成原始字节数组。为什么不直接存二进制?因为配置文件是文本格式,不适合存二进制数据。Base64 是将二进制安全地编码为可打印 ASCII 字符的标准方式。

    第三,底层全部委托给 Hutool。Phoenix 没有自己实现任何加密算法——AES 和 DES 用 Hutool 的 SecureUtil,SM4 用 Hutool 的 SmUtil。密码学算法的实现极其容易出错(时序攻击、填充预言攻击、不安全的随机数……),能用久经考验的开源库就绝不自己造轮子。这是一种非常务实的工程态度。

    第四,输出统一为 Base64。所有加密方法的返回值都是 Base64 字符串(encryptBase64),而不是原始字节数组。这确保了加密后的数据可以安全地放入 JSON 字段中传输——JSON 不支持二进制数据,但 Base64 字符串没问题。

    六、InitSecure:反射驱动的静态初始化

    三个工具类都继承了 InitSecure,那密钥到底是从哪来的?答案藏在 InitSecure 的静态初始化块中。

    6.1 四个静态常量

    public class InitSecure {
    
        public static final String SECRET_TYPE;        // 加解密类型,如 "AES"
        public static final String SECRET_KEY_DES;     // DES 密钥(Base64)
        public static final String SECRET_KEY_AES;     // AES 密钥(Base64)
        public static final String SECRET_KEY_SM4;     // SM4 密钥(Base64)
    }
    

    四个 public static final 字段,在 static 块中完成赋值。final 意味着一旦初始化完成就不可更改——加密算法和密钥在运行期间是固定的,不允许中途切换。这是合理的——如果加密方式在运行时改变,已经用旧算法加密的数据就解不出来了。

    6.2 反射加载配置

    static {
        TimeInterval timer = DateUtil.timer();
        try {
            // 通过反射加载 MonitoringSecureProperties 实例
            Object secureProps = loadSecureProperties();
            if (secureProps != null) {
                SECRET_TYPE = getSecretType(secureProps);
                if (StringUtils.equalsIgnoreCase(SecurerEnums.DES.name(), SECRET_TYPE)) {
                    SECRET_KEY_DES = getSecretKey(secureProps, "getDes");
                } else {
                    SECRET_KEY_DES = null;
                }
                if (StringUtils.equalsIgnoreCase(SecurerEnums.AES.name(), SECRET_TYPE)) {
                    SECRET_KEY_AES = getSecretKey(secureProps, "getAes");
                } else {
                    SECRET_KEY_AES = null;
                }
                if (StringUtils.equalsIgnoreCase(SecurerEnums.SM4.name(), SECRET_TYPE)) {
                    SECRET_KEY_SM4 = getSecretKey(secureProps, "getSm4");
                } else {
                    SECRET_KEY_SM4 = null;
                }
                log.info("初始化加解密配置成功!加解密类型:{}", StringUtils.defaultIfBlank(SECRET_TYPE, "无"));
            } else {
                SECRET_TYPE = null;
                SECRET_KEY_DES = null;
                SECRET_KEY_AES = null;
                SECRET_KEY_SM4 = null;
                log.warn("未加载到 MonitoringSecureProperties 配置,加解密功能将被禁用!");
            }
        } finally {
            String betweenDay = timer.intervalPretty();
            log.info("初始化加解密配置耗时:{}", betweenDay);
        }
    }
    

    这段代码有一个非常重要的设计决策:只加载当前选中算法的密钥。如果配置了 encryption-algorithm-type=aes,那么只有 SECRET_KEY_AES 会被赋值,SECRET_KEY_DESSECRET_KEY_SM4 都是 null

    为什么不把三个密钥都加载进来?两个原因:

    1. 安全性:内存中持有的密钥越少越好。只加载用得到的那个,即使发生内存转储(heap dump),攻击者也只能看到一个密钥。
    2. Fail-Fast:如果代码因为 bug 不小心调用了错误算法的工具类(比如配置的是 AES,但代码调了 DesEncryptUtils),会立即因为 SECRET_KEY_DESnull 而抛异常——而不是悄悄用错误的密钥加密出一堆垃圾数据。

    6.3 为什么用反射?

    loadSecureProperties() 方法的实现非常有意思:

    private static Object loadSecureProperties() {
        try {
            Class<?> configLoader = Class.forName("com.gitee.pifeng.monitoring.plug.core.ConfigLoader");
            Method getMonitoringProperties = configLoader.getMethod("getMonitoringProperties");
            setAccessibleQuietly(getMonitoringProperties);
            Object monitoringProps = getMonitoringProperties.invoke(null);
            if (monitoringProps == null) {
                return null;
            }
            Method getSecure = monitoringProps.getClass().getMethod("getSecure");
            setAccessibleQuietly(getSecure);
            return getSecure.invoke(monitoringProps);
        } catch (ClassNotFoundException e) {
            log.error("未找到 ConfigLoader 类!");
            return null;
        } catch (Exception e) {
            log.error("通过反射加载 MonitoringSecureProperties 实例时发生异常!", e);
            return null;
        }
    }
    

    为什么要用反射而不是直接 ConfigLoader.getMonitoringProperties().getSecure()

    这是一个模块解耦的问题。InitSecure 位于 phoenix-common-core 模块,ConfigLoader 位于 phoenix-client-core 模块。在 Maven 的依赖关系中,phoenix-client-core 依赖 phoenix-common-core,反过来不行——如果 phoenix-common-core 直接引用 ConfigLoader,就会形成循环依赖。

    反射打破了编译期的依赖关系——InitSecure 在编译时不需要知道 ConfigLoader 的存在,只在运行时通过字符串类名去查找它。如果 ConfigLoader 不在 classpath 上,Class.forName() 会抛 ClassNotFoundException,被 catch 后安静地返回 null——加密功能被禁用,但程序不会崩溃。

    6.4 declare():提前触发初始化

    public static void declare() {
        // 仅用于触发 static 块执行,无需任何逻辑
        log.trace("触发 InitSecure 静态初始化(若尚未执行)!");
    }
    

    这个“空方法”看似多余,实则意义重大。Java 的类加载机制是懒加载的——一个类只有在被首次主动使用时才会被加载和初始化。如果不调用 declare()InitSecure 的静态块会在第一次加密操作时才执行。

    问题在于,第一次加密操作通常发生在某个定时任务线程中——心跳包要发送了,需要加密,触发 InitSecure 类加载,然后反射加载配置、初始化密钥……这一系列操作会让第一次心跳发送变得异常缓慢。

    所以 Monitor.start() 在启动流程的第 4 步就主动调用了 InitSecure.declare()

    // 4.初始化加解密配置
    InitSecure.declare();
    // 5.运行双向数据交换器
    DataExchanger.run();
    

    在任何数据交换发生之前,就把加解密配置初始化好——密钥已经在内存中就位,后续的加密操作不会有“冷启动”延迟。这种“提前预热”的思想,在对延迟敏感的系统中非常常见。

    七、SecureUtils:面向调用方的统一门面

    业务代码不直接和 SecurerEnumsAesEncryptUtils 打交道——它们通过 SecureUtils 这个门面(Facade)完成所有加解密操作:

    public class SecureUtils extends InitSecure {
    
        public static String encrypt(String str, Charset charset) {
            // 没选择加解密类型,则不加密
            if (StringUtils.isBlank(SECRET_TYPE)) {
                return str;
            }
            return SecurerEnums.valueOf(StringUtils.upperCase(SECRET_TYPE)).encrypt(str, charset);
        }
    
        public static String encrypt(byte[] arry) {
            if (StringUtils.isBlank(SECRET_TYPE)) {
                return Base64.getEncoder().encodeToString(arry);
            }
            return SecurerEnums.valueOf(StringUtils.upperCase(SECRET_TYPE)).encrypt(arry);
        }
    
        public static String decrypt(String str, Charset charset) {
            if (StringUtils.isBlank(SECRET_TYPE)) {
                return str;
            }
            return SecurerEnums.valueOf(StringUtils.upperCase(SECRET_TYPE)).decrypt(str, charset);
        }
    
        public static byte[] decrypt(String str) {
            if (StringUtils.isBlank(SECRET_TYPE)) {
                return Base64.getDecoder().decode(str);
            }
            return SecurerEnums.valueOf(StringUtils.upperCase(SECRET_TYPE)).decrypt(str);
        }
    }
    

    SecureUtils 做了两件事:

    第一,优雅降级。如果 SECRET_TYPE 为空(用户没有配置加密算法),加密方法直接返回原文,解密方法也直接返回原文。这意味着加密是可选的——不配置就不加密,数据以明文传输。这种设计在开发和调试阶段非常友好——你可以在开发环境关闭加密,方便用抓包工具观察数据内容。

    注意字节数组的降级处理有所不同:加密时返回 Base64.getEncoder().encodeToString(arry)——因为调用方期望得到的是一个字符串,直接返回字节数组类型不对。解密时返回 Base64.getDecoder().decode(str)——对称地,把 Base64 字符串还原回字节数组。即使不加密,数据格式的转换仍然要保持一致。

    第二,算法路由SecurerEnums.valueOf(StringUtils.upperCase(SECRET_TYPE)) 这一行完成了从配置字符串到算法实现的映射。valueOf() 是枚举的标准方法——传入枚举常量的名字,返回对应的枚举实例。由于 SecurerEnums 实现了 ISecurer 接口,拿到枚举实例后直接调用 encrypt()decrypt() 即可。

    整个调用链条如下:

    SecureUtils.encrypt("hello", UTF-8)
      → SECRET_TYPE = "AES"
      → SecurerEnums.valueOf("AES")  // 拿到 SecurerEnums.AES 枚举实例
      → SecurerEnums.AES.encrypt("hello", UTF-8)  // 接口调用
      → AesEncryptUtils.encrypt("hello", UTF-8)   // 委托给工具类
      → SecureUtil.aes(key).encryptBase64("hello", UTF-8)  // Hutool 底层实现
      → "9FUrFVEs..."  // 返回 Base64 密文
    

    五次方法调用,从抽象层层穿透到底层实现,每一层都只做一件事——职责单一,层次分明。

    八、密钥生成:SecureUtils.generateKey()

    Phoenix 还提供了密钥生成能力:

    public static String generateKey(String algorithm, int keySize) {
        return Base64.getEncoder().encodeToString(KeyUtil.generateKey(algorithm, keySize).getEncoded());
    }
    

    底层用 Hutool 的 KeyUtil.generateKey() 调用 JDK 的 KeyGenerator 生成指定算法和长度的密钥,再 Base64 编码成字符串。生成的密钥可以直接填入配置文件的 monitoring.secure.aes.key 等字段中。

    九、配置体系:从 properties 到内存

    用户通过配置文件选择加密算法和密钥。Phoenix 支持两种配置方式——properties 文件和 Spring Boot 的 YAML/properties 配置。

    9.1 Properties 文件配置

    # 加密算法类型:aes / des / sm4,不填写则不加密
    monitoring.secure.encryption-algorithm-type=aes
    # AES 密钥(Base64 编码)
    monitoring.secure.aes.key=pqdOWnEkA3AQKyb7L2ewAA==
    # DES 密钥(Base64 编码)
    monitoring.secure.des.key=W21AB4W2nmg=
    # SM4 密钥(Base64 编码)
    monitoring.secure.sm4.key=UKW5kIkKmBHmL+yLV54tMg==
    

    9.2 Spring Boot Starter 配置

    phoenix:
      monitoring:
        secure:
          encryption-algorithm-type: aes
          aes:
            key: pqdOWnEkA3AQKyb7L2ewAA==
    

    9.3 配置属性类

    配置值被加载到一组简洁的属性类中:

    public class MonitoringSecureProperties {
        private SecurerEnums encryptionAlgorithmType;  // 加密算法类型
        private MonitoringSecureDesProperties des;      // DES 配置
        private MonitoringSecureAesProperties aes;      // AES 配置
        private MonitoringSecureSm4Properties sm4;      // SM4 配置
    }
    

    每个算法的配置属性类只有一个字段——key(密钥):

    public class MonitoringSecureAesProperties {
        // 注意:这个属性名称不能随意改动,因为在 InitSecure 用了反射来获取这个属性
        private String key;
    }
    

    注释里那行提醒很重要——因为 InitSecure 通过反射调用 getKey() 方法获取密钥,属性名改了反射就找不到了。这是反射解耦的代价:编译器不再替你检查字段名的一致性

    注意 encryptionAlgorithmType 的类型是 SecurerEnums——不是 String,而是枚举本身。这意味着配置值在加载阶段就会被校验——如果写了一个不存在的算法名,Spring Boot 在启动时就会报错,而不是等到第一次加密时才发现。

    十、调用全景:一条数据的加密之旅

    让我们把所有组件串起来,看看一条心跳数据从明文到密文的完整旅程。以 HTTP 通道为例:

    心跳包明文 JSON(约 200 字节)
        │
        ▼
    MsgPayloadUtils.encryptPayload(json)
        │
        ├── ZipUtils.isNeedGzip(json)  →  200B < 64KB  →  不压缩
        │
        ├── SecureUtils.encrypt(json, UTF-8)
        │     │
        │     ├── SECRET_TYPE = "AES"(非空,走加密路径)
        │     │
        │     ├── SecurerEnums.valueOf("AES")
        │     │     │
        │     │     └── SecurerEnums.AES.encrypt(json, UTF-8)
        │     │           │
        │     │           └── AesEncryptUtils.encrypt(json, UTF-8)
        │     │                 │
        │     │                 ├── Base64.decode("pqdOWnEkA3AQKyb7L2ewAA==")  →  密钥字节数组
        │     │                 │
        │     │                 └── SecureUtil.aes(key).encryptBase64(json, UTF-8)
        │     │                       │
        │     │                       └── "9FUrFVEsBR3Mjw..."  ← AES/ECB/PKCS5Padding + Base64
        │     │
        │     └── 返回密文字符串
        │
        └── new CiphertextPackage("9FUrFVEsBR3Mjw...", false)
              │
              └── .toJsonString()  →  {"ciphertext":"9FUrFVEsBR3Mjw...","isUnGzipEnabled":false}
    

    接收方的解密是完全对称的逆操作——解析 JSON、取出密文、解密、返回明文。发送方和接收方必须使用相同的算法和密钥,否则解密失败。这也是为什么 Phoenix 的所有节点(客户端、代理端、服务端、UI 端)的配置文件中都写着同样的加密配置。

    十一、设计复盘

    “接口 + 枚举”的策略模式

    经典的策略模式需要一个接口、多个实现类、一个上下文持有策略引用。Phoenix 用枚举替代了实现类和上下文——枚举既是策略的容器(SecurerEnums.AES),又是策略的注册表(SecurerEnums.valueOf("AES")),还是策略的单例保证。一个枚举干了三件事,代码量极小,但功能完整。

    如果将来需要添加一种新算法(比如 SM3),只需要:

    1. SecurerEnums 中加一个枚举常量
    2. 写一个 Sm3EncryptUtils 工具类
    3. InitSecure 中加一个 SECRET_KEY_SM3 字段和对应的加载逻辑
    4. 在配置属性类中加一个 MonitoringSecureSm3Properties

    核心的调用链——SecureUtilsSecurerEnums → 工具类——完全不需要修改。这就是抽象的价值。

    继承用于共享常量,而非多态

    AesEncryptUtils extends InitSecure 这种继承关系,不是为了实现多态(你不会把 AesEncryptUtils 的引用赋给 InitSecure 类型的变量),而是为了让子类能直接访问父类的 protected/public static final 常量。这是一种实用但略显“非正统”的继承用法——在正统的 OOP 设计中,这种场景应该用组合或静态导入。但在这个具体场景中,继承让代码更简洁——工具类直接写 SECRET_KEY_AES 而不是 InitSecure.SECRET_KEY_AES,读起来更自然。

    加密可选:零配置即零加密

    SecureUtils 的降级逻辑让加密变成了一个完全可选的功能——不填 encryption-algorithm-type,数据就以明文传输。这种设计让 Phoenix 在不同环境下有不同的表现:

    • 开发环境:不配加密,方便调试和抓包
    • 测试环境:配 DES(速度快,强度够用)
    • 生产环境:配 AES 或 SM4(强度高,满足合规要求)

    用户不需要改一行代码,只需要修改一行配置,就能在三种安全级别之间切换。

    十二、小结

    本篇拆解了 Phoenix 的数据加解密体系。回顾核心要点:

    • ISecurer 接口:定义了四个方法(字符串/字节数组 × 加密/解密),是所有算法的统一契约
    • SecurerEnums 枚举:实现 ISecurer 接口,每个枚举常量(DES、AES、SM4)委托给对应的工具类,用 valueOf() 实现配置到实现的零代码映射
    • 三个工具类AesEncryptUtilsDesEncryptUtilsSm4EncryptUtils,继承 InitSecure 获取密钥,底层委托 Hutool 完成真正的加解密
    • InitSecure:通过反射从 ConfigLoader 加载配置,在静态块中初始化密钥;反射解决了模块间的循环依赖问题;declare() 方法实现提前预热
    • SecureUtils:面向调用方的统一门面,实现算法路由和优雅降级(不配置则不加密)
    • 配置体系:支持 properties 和 Spring Boot 两种配置方式,MonitoringSecureProperties 用枚举类型字段实现编译期校验

    整个体系的设计哲学可以用一句话概括:算法可切换,调用不感知。无论底层用的是 AES、DES 还是 SM4,上层的 MsgPayloadUtilsSenderDataExchanger 看到的始终是同一个 SecureUtils.encrypt() 接口——它们不知道也不需要知道数据到底是怎么被加密的。这种“信息隐藏”正是面向对象设计的精髓所在。

    下一篇,我们将深入 Phoenix 的消息压缩策略——看看 Gzip 压缩是如何与加密协同工作的,以及那个 64KB 阈值背后的设计考量。


    项目地址
    https://gitcode.com/monitoring-platform/phoenix
    https://gitee.com/monitoring-platform/phoenix
    https://github.com/709343767/phoenix

    欢迎关注微信公众号获取更多技术干货

    end
  1. 作者: 锋哥 (联系作者)
  2. 发表时间: 2026-03-31 14:35
  3. 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  4. 转载声明:如果是转载博主转载的文章,请附上原文链接
  5. 公众号转载:请在文末添加作者公众号二维码(公众号二维码见右边,欢迎关注)
  6. 评论

    站长头像 知录

    你一句春不晚,我就到了真江南!

    文章0
    浏览0

    文章分类

    标签云