尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

Java国密SM4-CBC加密实战:基于BouncyCastle的完整实现与避坑指南

Java国密SM4-CBC加密实战:基于BouncyCastle的完整实现与避坑指南
📅 发布时间:2026/6/30 23:22:03

1. 项目概述与背景

最近在做一个金融相关的项目,对接方明确要求使用国密算法SM4对传输数据进行加密。说实话,第一次接到这个需求时,我脑子里第一反应是AES,毕竟平时用得太多了。但国密SM4作为我们国家自主设计的商用密码算法,在金融、政务、物联网这些对数据安全有特定合规要求的领域,已经是硬性标准了。网上搜了一圈,发现关于Java实现SM4的完整、可落地的教程,尤其是涉及具体模式和第三方库使用的,要么语焉不详,要么代码跑不通,坑点不少。所以,我决定把这次从零开始,使用BouncyCastle这个强大的加密库实现SM4-CBC模式的全过程记录下来,附上能直接跑的代码,希望能帮你省下几个小时甚至几天的摸索时间。

SM4是一种分组密码算法,和AES类似,分组长度是128位(16字节),密钥长度也是128位。它主要包含加解密算法和密钥扩展算法。CBC(Cipher Block Chaining,密码分组链接)模式则是我们最常用的工作模式之一,它能有效防止同样的明文块加密成同样的密文块,安全性比ECB模式高得多。在Java标准库(JCE)中,并没有直接提供SM4的实现,这就是为什么我们需要借助BouncyCastle(一个提供了大量密码学算法实现的Java库)的原因。接下来,我会手把手带你完成环境配置、核心代码编写、以及调试过程中会遇到的那些“坑”。

2. 环境准备与BouncyCastle集成

2.1 为什么选择BouncyCastle?

在开始写代码之前,我们得先解决“用什么”的问题。Java原生不支持SM4,主流的选择有两个:一个是使用国内一些厂商提供的国密算法库(JAR包),另一个就是BouncyCastle。我选择BouncyCastle主要基于以下几点考虑:

  1. 广泛认可与活跃度:BouncyCastle是一个历史悠久、社区活跃、经过广泛审计的开源密码学库,在业界有极高的声誉。使用它,在代码安全性和可维护性上更有保障。
  2. 算法齐全:它几乎囊括了所有常用的加密算法(包括国密SM2, SM3, SM4)、数字签名、证书处理等,一站式解决,不需要引入多个来源不明的依赖。
  3. 跨平台与易集成:作为一个纯Java库,它不依赖本地代码,部署简单,兼容性好。通过Maven或Gradle引入依赖即可,非常方便。

相比之下,一些特定的国密算法JAR包可能更新不及时、文档缺失,或者存在潜在的兼容性问题。因此,对于大多数需要合规且追求稳定性的项目,BouncyCastle是更优解。

2.2 项目依赖配置

如果你使用Maven,在你的pom.xml文件中添加以下依赖。这里我们使用BouncyCastle的“bcprov-jdk15to18”,它兼容JDK 1.5到1.8及更高版本(实际支持到当前主流JDK)。

<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15to18</artifactId> <version>1.72</version> <!-- 请检查并使用最新稳定版本 --> </dependency>

如果你使用Gradle,则在build.gradle的dependencies块中添加:

implementation 'org.bouncycastle:bcprov-jdk15to18:1.72'

添加依赖后,建议刷新一下你的项目,确保依赖库被正确下载。接下来,我们需要在代码中动态注册BouncyCastle作为安全提供者,或者通过JVM参数静态注册。为了代码的清晰和可移植性,我通常在程序启动时动态注册。

import java.security.Security; public class Sm4CbcDemo { static { // 动态添加BouncyCastle提供者 if (Security.getProvider("BC") == null) { Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); } } // ... 后续代码 }

把这个静态块放在你的主类或者工具类里,确保在调用加密解密方法前,BouncyCastle提供者已经被成功注册到JVM中。你可以通过Security.getProviders()来验证是否添加成功。

注意:有些极端严格的环境(例如某些容器化部署)可能对动态注册安全提供者有限制。如果遇到NoSuchAlgorithmException异常,可以尝试在JVM启动参数中静态注册:-Djava.security.properties=/path/to/your/java.security,并在该文件中添加security.provider.N=org.bouncycastle.jce.provider.BouncyCastleProvider。但动态注册对99%的场景都够用了。

3. SM4-CBC加密核心实现详解

环境搭好了,我们进入正题。实现一个完整的SM4-CBC加密解密,需要明确几个关键要素:密钥(Key)、初始化向量(IV)、以及填充方式(Padding)。CBC模式要求每个明文块在加密前,先与前一个密文块进行异或操作,第一个块则需要与IV进行异或。因此,IV的作用至关重要,它不需要保密,但必须不可预测,通常随机生成,并在解密时使用相同的IV。

3.1 密钥与IV的生成与管理

SM4的密钥是128位,即16个字节。IV的长度应与分组长度一致,也是128位(16字节)。绝对不要使用固定的密钥和IV!对于生产环境,密钥应从安全的密钥管理系统获取,IV则应每次加密时随机生成。

import java.security.SecureRandom; import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.IvParameterSpec; public class KeyIvGenerator { /** * 生成一个随机的128位(16字节)SM4密钥。 * @return 生成的密钥字节数组 */ public static byte[] generateSm4Key() { byte[] key = new byte[16]; // 128 bits new SecureRandom().nextBytes(key); return key; } /** * 生成一个随机的128位(16字节)初始化向量(IV)。 * @return 生成的IV字节数组 */ public static byte[] generateIv() { byte[] iv = new byte[16]; // 128 bits new SecureRandom().nextBytes(iv); return iv; } /** * 将字节数组转换为SecretKeySpec对象。 * @param keyBytes 密钥字节数组(必须为16字节) * @return SecretKeySpec */ public static SecretKeySpec toSm4KeySpec(byte[] keyBytes) { if (keyBytes.length != 16) { throw new IllegalArgumentException("SM4 key must be 16 bytes (128 bits) long."); } // “SM4”是BouncyCastle注册的算法名称 return new SecretKeySpec(keyBytes, "SM4"); } /** * 将字节数组转换为IvParameterSpec对象。 * @param ivBytes IV字节数组(必须为16字节) * @return IvParameterSpec */ public static IvParameterSpec toIvSpec(byte[] ivBytes) { if (ivBytes.length != 16) { throw new IllegalArgumentException("SM4 IV must be 16 bytes (128 bits) long."); } return new IvParameterSpec(ivBytes); } }

这里使用了java.security.SecureRandom来生成密码学安全的随机数,这比java.util.Random安全得多。生成的密钥和IV都是字节数组,在实际传输或存储时,我们通常将其进行Base64或Hex(十六进制)编码。

实操心得:密钥管理是安全的核心。这个示例代码在内存中生成密钥,仅用于演示。真实项目中,密钥必须妥善保管,例如使用硬件安全模块(HSM)、云服务商的密钥管理服务(KMS),或者至少是加密后存储在配置中心。绝对禁止将硬编码的密钥提交到代码仓库!

3.2 完整的加密与解密工具类

下面是一个整合了加密、解密、以及编码功能的完整工具类。我们使用PKCS7Padding填充方式(在BC中常表示为PKCS5Padding,因为PKCS#5和PKCS#7在分组密码的填充上本质相同),算法名称为SM4/CBC/PKCS5Padding。

import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.IvParameterSpec; import java.security.Security; import java.util.Base64; public class Sm4CbcUtil { // 算法名称/模式/填充 private static final String ALGORITHM_NAME = "SM4"; private static final String ALGORITHM_NAME_CBC_PADDING = "SM4/CBC/PKCS5Padding"; // 编码器 private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder(); private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); static { // 确保BouncyCastle提供者被注册 if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { Security.addProvider(new BouncyCastleProvider()); } } /** * SM4-CBC加密(输出Base64编码字符串) * @param plaintext 明文文本 * @param keyBytes 密钥字节数组(16字节) * @param ivBytes 初始化向量字节数组(16字节) * @return Base64编码的密文字符串 */ public static String encryptToBase64(String plaintext, byte[] keyBytes, byte[] ivBytes) throws Exception { // 1. 参数校验 validateKeyAndIv(keyBytes, ivBytes); // 2. 创建密钥和IV规范 SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM_NAME); IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes); // 3. 获取并初始化Cipher实例(加密模式) Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_CBC_PADDING, BouncyCastleProvider.PROVIDER_NAME); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); // 4. 执行加密 byte[] ciphertextBytes = cipher.doFinal(plaintext.getBytes("UTF-8")); // 5. 返回Base64编码结果 return BASE64_ENCODER.encodeToString(ciphertextBytes); } /** * SM4-CBC解密(输入Base64编码字符串) * @param ciphertextBase64 Base64编码的密文字符串 * @param keyBytes 密钥字节数组(16字节) * @param ivBytes 初始化向量字节数组(16字节) * @return 解密后的明文文本 */ public static String decryptFromBase64(String ciphertextBase64, byte[] keyBytes, byte[] ivBytes) throws Exception { // 1. 参数校验 validateKeyAndIv(keyBytes, ivBytes); // 2. 创建密钥和IV规范 SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM_NAME); IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes); // 3. 获取并初始化Cipher实例(解密模式) Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_CBC_PADDING, BouncyCastleProvider.PROVIDER_NAME); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); // 4. Base64解码并执行解密 byte[] ciphertextBytes = BASE64_DECODER.decode(ciphertextBase64); byte[] plaintextBytes = cipher.doFinal(ciphertextBytes); // 5. 返回明文字符串 return new String(plaintextBytes, "UTF-8"); } /** * 校验密钥和IV长度 */ private static void validateKeyAndIv(byte[] keyBytes, byte[] ivBytes) { if (keyBytes == null || keyBytes.length != 16) { throw new IllegalArgumentException("Invalid SM4 key. Key must be 16 bytes (128 bits)."); } if (ivBytes == null || ivBytes.length != 16) { throw new IllegalArgumentException("Invalid IV. IV must be 16 bytes (128 bits) for CBC mode."); } } /** * 便捷方法:使用随机生成的密钥和IV进行加密,并返回所有必要信息(用于演示) * @param plaintext 明文 * @return 包含Base64编码的密钥、IV和密文的数组 [keyBase64, ivBase64, ciphertextBase64] */ public static String[] encryptWithRandomKeyIv(String plaintext) throws Exception { byte[] key = KeyIvGenerator.generateSm4Key(); byte[] iv = KeyIvGenerator.generateIv(); String ciphertext = encryptToBase64(plaintext, key, iv); return new String[]{ BASE64_ENCODER.encodeToString(key), BASE64_ENCODER.encodeToString(iv), ciphertext }; } }

这个工具类提供了最核心的encryptToBase64和decryptFromBase64方法。注意Cipher.getInstance()的第二个参数,我们显式指定了提供者为BouncyCastleProvider.PROVIDER_NAME(即“BC”),这是一个好习惯,可以避免在环境中存在多个提供者时出现算法找不到的意外。

3.3 一个完整的测试示例

让我们写一个main方法来测试上面的工具类是否工作正常。

public class MainTest { public static void main(String[] args) { try { String originalText = "这是一段需要加密的敏感数据,Hello SM4!"; System.out.println("=== 测试1:使用随机密钥和IV ==="); String[] result = Sm4CbcUtil.encryptWithRandomKeyIv(originalText); System.out.println("随机密钥(Base64): " + result[0]); System.out.println("随机IV(Base64): " + result[1]); System.out.println("加密后密文(Base64): " + result[2]); // 解密 byte[] key = Base64.getDecoder().decode(result[0]); byte[] iv = Base64.getDecoder().decode(result[1]); String decryptedText = Sm4CbcUtil.decryptFromBase64(result[2], key, iv); System.out.println("解密后明文: " + decryptedText); System.out.println("解密是否成功: " + originalText.equals(decryptedText)); System.out.println("\n=== 测试2:使用固定密钥和IV(仅用于演示和理解) ==="); // 警告:生产环境切勿使用固定值! String fixedKeyBase64 = "C7h3pLkq9Mf2jE5NcR1TgA=="; // 一个示例的16字节Base64 String fixedIvBase64 = "V1mHq9Kz8Xb5LwP0cR2TdQ=="; // 一个示例的16字节Base64 byte[] fixedKey = Base64.getDecoder().decode(fixedKeyBase64); byte[] fixedIv = Base64.getDecoder().decode(fixedIvBase64); String ciphertext2 = Sm4CbcUtil.encryptToBase64(originalText, fixedKey, fixedIv); System.out.println("使用固定密钥IV加密结果: " + ciphertext2); String decryptedText2 = Sm4CbcUtil.decryptFromBase64(ciphertext2, fixedKey, fixedIv); System.out.println("使用固定密钥IV解密结果: " + decryptedText2); System.out.println("解密是否成功: " + originalText.equals(decryptedText2)); } catch (Exception e) { e.printStackTrace(); } } }

运行这个测试,你应该能看到加密和解密过程成功完成,并且解密后的文本与原始文本一致。这验证了我们整个流程的正确性。

4. 关键参数、模式选择与进阶话题

4.1 工作模式与填充模式的选择

我们选择了CBC模式,这是最经典和常用的模式之一。除了CBC,你可能还会听到ECB、CFB、OFB、CTR、GCM等模式。

  • ECB(电子密码本):绝对不要用于加密有意义的数据!它将每个明文块独立加密,相同的明文块会产生相同的密文块,不能隐藏数据模式,安全性很低。通常只用于加密密钥本身。
  • CBC(密码分组链接):需要IV,安全性好,是许多标准(如早期的TLS)的默认选择。但它不能并行加密(解密可以并行)。
  • CTR(计数器模式):将块密码转换为流密码,可以并行加解密,不需要填充。在很多现代协议中很受欢迎。
  • GCM(Galois/Counter Mode):提供了加密和完整性校验(认证),是当前TLS 1.3等协议推荐的模式,性能也更好。

对于SM4,BouncyCastle同样支持这些模式。例如,你可以使用SM4/GCM/NoPadding。选择哪种模式取决于你的具体需求(是否需要认证、性能要求、与对接方的协议等)。CBC模式因其经典和广泛的兼容性,仍然是很多国密对接场景中的首选。

关于填充,我们使用了PKCS5Padding(在BC中等同于PKCS7Padding)。它的作用是当明文长度不是分组长度的整数倍时,自动填充至整倍数,并在解密后自动移除填充。如果数据长度总是分组的整数倍,或者你使用像CTR这样的流模式,则可以使用NoPadding。

4.2 编码与传输的注意事项

加密后的结果是二进制字节数组,直接通过网络传输或存储到文本字段(如JSON、XML、数据库VARCHAR)中会出问题。因此,我们需要进行编码。Base64是最常用的编码方式,它将二进制数据转换为由64个字符(A-Z, a-z, 0-9, +, /)组成的字符串,末尾可能用=补足。

在我们的工具类中,加密后输出Base64字符串,解密时输入Base64字符串,这样非常便于在JSON等文本协议中传输。对应的,密钥和IV在配置或传输时,也应以Base64或Hex字符串的形式存在。

// 编码示例 String base64Encoded = Base64.getEncoder().encodeToString(byteArray); byte[] decodedBytes = Base64.getDecoder().decode(base64EncodedString); // Hex编码(也可用,更易读但体积更大) // 可以使用Apache Commons Codec的Hex类,或者自己简单实现。

注意事项:Base64编码会使数据体积增大约33%。如果传输的数据量非常大,需要考虑这个开销。但在大多数API交互中,这点开销是可以接受的。

4.3 与其它语言/平台的互通性

这是一个非常实际的问题。你的Java服务加密的数据,可能需要被一个Python、Go或者C#的服务解密,反之亦然。要实现互通,必须保证以下几点完全一致:

  1. 算法:SM4。
  2. 模式:CBC。
  3. 密钥长度:128位(16字节)。
  4. IV长度:128位(16字节),且值相同。
  5. 填充方式:PKCS#7(在大多数平台上叫PKCS7Padding,在Java/BC中常用PKCS5Padding指代,它们对于16字节分组的填充是相同的)。
  6. 数据编码:通常约定使用Base64或Hex。

例如,在Python中,你可以使用cryptography库或gmssl库(专门为国密算法设计)来实现SM4-CBC解密。你需要将从Java端获得的Base64编码的密钥、IV和密文,在Python端进行Base64解码,然后使用相同的参数进行解密。务必在联调前,双方用一组固定的测试向量验证加解密结果是否一致。

5. 常见问题、异常排查与性能优化

5.1 常见异常与解决方案

在实际集成时,你几乎一定会遇到下面这些异常。这里我把它整理成一个速查表。

异常信息可能原因解决方案
java.security.NoSuchAlgorithmException: Cannot find any provider supporting SM4/CBC/PKCS5Padding1. BouncyCastle JAR包未引入。
2. BouncyCastle提供者未成功注册到JVM。
1. 检查pom.xml/build.gradle依赖是否正确,项目是否成功下载了JAR包。
2. 确保在调用加密代码之前执行了Security.addProvider(new BouncyCastleProvider())。可以在main方法第一行或静态块中执行。
java.security.InvalidKeyException: Illegal key size or default parameters使用了非128位的密钥。检查生成或传入的密钥字节数组长度是否为16。使用keyBytes.length确认。
javax.crypto.IllegalBlockSizeException: Input length not multiple of 16 bytes1. 解密时,密文长度不是16字节的整数倍(对于CBC模式,密文长度必须是分组长度的整数倍)。
2. 可能使用了NoPadding但数据长度不对。
1. 检查密文在传输或存储过程中是否被截断或修改。确保Base64解码后的密文字节数组长度是16的倍数。
2. 确认加密和解密使用的填充模式一致。
javax.crypto.BadPaddingException: Given final block not properly padded这是最常见的异常之一。原因可能有:
1. 密钥错误。
2. IV错误。
3. 密文被损坏(传输、解码错误)。
4. 加密和解密使用的填充模式不一致。
1.首先核对密钥和IV:确保用于解密的密钥和IV与加密时使用的完全一致(字节对字节)。打印或日志记录它们的Base64值进行比对。
2. 检查Base64解码过程是否正确,密文字符串是否有空格、换行等杂音。
3. 如果是网络传输,确认没有发生字符集转换问题。
解密后得到乱码1. 密钥/IV错误,但凑巧能通过填充验证(概率极低但可能)。
2. 明文字符集与解密后转换字符集不一致。
1. 同样优先核对密钥和IV。
2. 在加密和解密时,明确指定字符集,如plaintext.getBytes("UTF-8")和new String(plaintextBytes, "UTF-8")。确保两端一致。

5.2 性能考量与最佳实践

  1. Cipher对象复用:Cipher对象的初始化(init方法)开销相对较大。如果在高并发场景下频繁进行加解密操作,可以考虑使用ThreadLocal或对象池来复用已初始化的Cipher实例,尤其是当密钥和IV固定时(例如用于解密特定渠道的请求)。

    public class CipherPool { private static final ThreadLocal<Cipher> encryptCipherHolder = new ThreadLocal<>(); private static final ThreadLocal<Cipher> decryptCipherHolder = new ThreadLocal<>(); // ... 根据密钥和IV初始化并存储Cipher实例 }
  2. 输入输出流处理大文件:如果需要加密大文件,千万不要用cipher.doFinal(byte[])一次性读入内存。应该使用CipherInputStream和CipherOutputStream进行流式处理。

    try (FileInputStream fis = new FileInputStream("input.txt"); FileOutputStream fos = new FileOutputStream("encrypted.dat"); CipherOutputStream cos = new CipherOutputStream(fos, cipher)) { byte[] buffer = new byte[8192]; int n; while ((n = fis.read(buffer)) != -1) { cos.write(buffer, 0, n); } }
  3. 密钥安全:再次强调,密钥的安全是根本。除了使用HSM/KMS,在代码中如果必须配置,也应从环境变量或配置中心获取,并确保配置存储本身是加密的。禁止在日志中打印完整的密钥。

  4. IV的管理:CBC模式的IV不需要保密,但必须不可预测且唯一。每次加密都应使用新的随机IV。通常将IV和密文一起传输(例如,将IV拼接在密文前面)。解密方先取出前16字节作为IV,剩余部分作为密文。

    // 加密端:IV + 密文 byte[] combined = new byte[iv.length + ciphertext.length]; System.arraycopy(iv, 0, combined, 0, iv.length); System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length); String result = Base64.getEncoder().encodeToString(combined); // 解密端:拆分 byte[] combined = Base64.getDecoder().decode(combinedBase64); byte[] iv = Arrays.copyOfRange(combined, 0, 16); byte[] ciphertext = Arrays.copyOfRange(combined, 16, combined.length);

5.3 国密算法检测与合规性

在一些严格的项目验收或等保测评中,可能需要证明你确实使用了国密算法。除了代码审查,你还可以通过一些工具来验证。例如,使用国密算法检测工具对编译后的程序或运行时的通信包进行分析,确认其中包含SM4的算法标识和调用。在开发过程中,确保你的BouncyCastle依赖来源可靠,并且没有其他组件意外替换或干扰了算法实现。

我个人在项目上线前,会编写一套完整的单元测试和集成测试,测试用例不仅包括功能正确性,还包括使用标准的SM4测试向量(可以从国家密码管理局的相关文档中找到)进行验证,确保算法的实现完全符合标准。这是证明合规性最直接有效的方法。

相关新闻

  • 2026昆明公司注销超全攻略:材料清单、避坑误区、办理流程
  • SENAITE LIMS:开源实验室信息管理系统完整实战手册
  • ai-vi-1

最新新闻

  • Meta Quest 播放软件《下一代视频播放器》NEXt-Gen Video Player 下载和使用教程
  • 【论文复现】存在测距误差的WSN无锚点分布式自定位,《WSN中存在测距误差的无锚点分布式自定位方法》
  • 抖音监控助手:实时追踪博主动态与直播推送的终极指南
  • VisualGGPK2完整指南:快速掌握《流放之路》游戏资源管理技巧
  • Spark SQL 优化:从 Catalyst 优化器到数据倾斜治理,大数据查询的性能调优路径
  • 魔兽争霸3终极优化教程:如何三步解决现代硬件兼容性问题

日新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

周新闻

  • Windows字体自定义终极方案:No!! MeiryoUI完全指南
  • Deepin Boot Maker:告别命令行,3分钟制作Linux启动盘的智能解决方案
  • Plain Craft Launcher 2:重新定义你的Minecraft游戏体验

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号