当前位置:首页 > 技术 > 正文内容

国密SM2、RSA与BCrypt加密技术实践:Java实现与Redis集成

访客 技术 2026年6月27日 2

在现代应用开发中,数据安全是至关重要的环节。本文将深入探讨三种常见的加密方法:国密SM2、RSA(结合Redis存储)以及BCrypt,并提供基于Java的详细实现和示例代码。

一、国密SM2算法实践

国密SM2是国家密码管理局颁布的公钥密码算法,基于椭圆曲线密码学,适用于数字签名、密钥交换和数据加密等场景,在中国境内的信息系统中推广使用。

1.1 依赖引入

首先,需要在项目的pom.xml文件中引入Bouncy Castle库,它提供了对SM2算法的全面支持。

pom.xml
        <!-- Bouncy Castle 提供国密SM2支持 -->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
            <version>1.70</version>
        </dependency>
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcpkix-jdk15on</artifactId<
            <version>1.70</version>
        </dependency>

1.2 SM2加解密与签名验签工具类

下面是一个封装SM2加解密、签名与验签操作的工具类。它负责生成密钥对,并提供方便的数据处理方法。

Sm2CipherUtils.java
package org.example.security.sm2;

import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Base64;

import java.math.BigInteger;
import java.security.*;

/**
 * SM2加密、解密、签名、验签辅助类
 */
public class Sm2CipherUtils {

    // 静态代码块,用于加载BouncyCastle加密服务提供者
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    // 获取SM2p256v1曲线参数,这是SM2的标准曲线
    private static final X9ECParameters SM2_CURVE_PARAMS = GMNamedCurves.getByName("sm2p256v1");
    // 基于曲线参数定义椭圆曲线域参数
    private static final ECDomainParameters SM2_DOMAIN_PARAMS = new ECDomainParameters(
            SM2_CURVE_PARAMS.getCurve(),
            SM2_CURVE_PARAMS.getG(),
            SM2_CURVE_PARAMS.getN()
    );

    /**
     * 生成SM2算法的密钥对。
     *
     * @return 包含公钥和私钥的非对称密钥对。
     * @throws NoSuchProviderException 如果无法找到Bouncy Castle提供者。
     * @throws NoSuchAlgorithmException 如果SM2算法不可用。
     * @throws InvalidAlgorithmParameterException 如果算法参数无效。
     */
    public static AsymmetricCipherKeyPair generateSm2KeyPair()
            throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException {
        ECKeyPairGenerator keyGen = new ECKeyPairGenerator();
        // 使用SM2域参数初始化密钥生成器
        ECKeyGenerationParameters genParams = new ECKeyGenerationParameters(SM2_DOMAIN_PARAMS, new SecureRandom());
        keyGen.init(genParams);
        return keyGen.generateKeyPair();
    }

    /**
     * 使用SM2公钥加密数据。
     *
     * @param plainTextBytes 待加密的原始字节数据。
     * @param sm2PublicKey SM2公钥参数。
     * @return 加密后的字节数组。
     * @throws Exception 加密过程中可能发生的异常。
     */
    public static byte[] encryptData(byte[] plainTextBytes, ECPublicKeyParameters sm2PublicKey) throws Exception {
        SM2Engine sm2Engine = new SM2Engine();
        // 以加密模式初始化引擎,并提供公钥和随机数
        sm2Engine.init(true, new ParametersWithRandom(sm2PublicKey, new SecureRandom()));
        return sm2Engine.processBlock(plainTextBytes, 0, plainTextBytes.length);
    }

    /**
     * 使用SM2私钥解密数据。
     *
     * @param encryptedBytes 待解密的加密字节数组。
     * @param sm2PrivateKey SM2私钥参数。
     * @return 解密后的原始字节数组。
     * @throws Exception 解密过程中可能发生的异常。
     */
    public static byte[] decryptData(byte[] encryptedBytes, ECPrivateKeyParameters sm2PrivateKey) throws Exception {
        SM2Engine sm2Engine = new SM2Engine();
        // 以解密模式初始化引擎,并提供私钥
        sm2Engine.init(false, sm2PrivateKey);
        return sm2Engine.processBlock(encryptedBytes, 0, encryptedBytes.length);
    }

    /**
     * 使用SM2私钥对数据进行签名。
     *
     * @param messageBytes 待签名的原始字节数据。
     * @param sm2PrivateKey SM2私钥参数。
     * @return 签名后的字节数组。
     * @throws Exception 签名过程中可能发生的异常。
     */
    public static byte[] signMessage(byte[] messageBytes, ECPrivateKeyParameters sm2PrivateKey) throws Exception {
        SM2Signer sm2Signer = new SM2Signer();
        // 使用私钥和空ID初始化签名器
        CipherParameters signParams = new ParametersWithID(sm2PrivateKey, new byte[0]);
        sm2Signer.init(true, signParams);
        sm2Signer.update(messageBytes, 0, messageBytes.length);
        return sm2Signer.generateSignature();
    }

    /**
     * 使用SM2公钥验证数据签名。
     *
     * @param messageBytes 原始消息的字节数据。
     * @param signatureBytes 待验证的签名字节数组。
     * @param sm2PublicKey SM2公钥参数。
     * @return 如果签名有效则返回true,否则返回false。
     * @throws Exception 验签过程中可能发生的异常。
     */
    public static boolean verifySignature(byte[] messageBytes, byte[] signatureBytes, ECPublicKeyParameters sm2PublicKey) throws Exception {
        SM2Signer sm2Signer = new SM2Signer();
        // 使用公钥和空ID初始化签名器
        CipherParameters verifyParams = new ParametersWithID(sm2PublicKey, new byte[0]);
        sm2Signer.init(false, verifyParams);
        sm2Signer.update(messageBytes, 0, messageBytes.length);
        return sm2Signer.verifySignature(signatureBytes);
    }

    /**
     * 将Base64编码的公钥字符串转换为ECPublicKeyParameters对象。
     *
     * @param base64EncodedPublicKey Base64编码的公钥字符串。
     * @return ECPublicKeyParameters对象。
     * @throws Exception 转换过程中可能发生的异常。
     */
    public static ECPublicKeyParameters decodePublicKey(String base64EncodedPublicKey) throws Exception {
        byte[] publicKeyBytes = Base64.decode(base64EncodedPublicKey);
        ECPoint pubPoint = SM2_CURVE_PARAMS.getCurve().decodePoint(publicKeyBytes);
        return new ECPublicKeyParameters(pubPoint, SM2_DOMAIN_PARAMS);
    }

    /**
     * 将Base64编码的私钥字符串转换为ECPrivateKeyParameters对象。
     *
     * @param base64EncodedPrivateKey Base64编码的私钥字符串。
     * @return ECPrivateKeyParameters对象。
     * @throws Exception 转换过程中可能发生的异常。
     */
    public static ECPrivateKeyParameters decodePrivateKey(String base64EncodedPrivateKey) throws Exception {
        byte[] privateKeyBytes = Base64.decode(base64EncodedPrivateKey);
        BigInteger privateKeyValue = new BigInteger(1, privateKeyBytes);
        return new ECPrivateKeyParameters(privateKeyValue, SM2_DOMAIN_PARAMS);
    }

    /**
     * 将ECPublicKeyParameters对象转换为Base64编码的字符串。
     *
     * @param sm2PublicKey ECPublicKeyParameters对象。
     * @return Base64编码的公钥字符串。
     */
    public static String encodePublicKey(ECPublicKeyParameters sm2PublicKey) {
        return Base64.toBase64String(sm2PublicKey.getQ().getEncoded(true));
    }

    /**
     * 将ECPrivateKeyParameters对象转换为Base64编码的字符串。
     *
     * @param sm2PrivateKey ECPrivateKeyParameters对象。
     * @return Base64编码的私钥字符串。
     */
    public static String encodePrivateKey(ECPrivateKeyParameters sm2PrivateKey) {
        return Base64.toBase64String(sm2PrivateKey.getD().toByteArray());
    }
}

1.3 SM2 RESTful API控制器

为了方便地在Web应用中使用SM2功能,我们可以创建一个Spring Boot RESTful控制器,暴露密钥生成、加解密和签名验签接口。

Sm2EncryptionController.java
package org.example.security.sm2;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.springframework.web.bind.annotation.*;

import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

/**
 * SM2加密功能Web接口
 */
@RestController
@RequestMapping("/sm2")
@Tag(name = "SM2密码服务", description = "提供SM2密钥管理、数据加解密及签名验签功能")
public class Sm2EncryptionController {

    /**
     * 生成SM2算法的公私钥对。
     *
     * @return 包含Base64编码公钥和私钥字符串的Map。
     */
    @GetMapping("/generate-keypair")
    @Operation(summary = "生成SM2密钥对", description = "生成一对SM2公私钥,并以Base64字符串形式返回")
    public Map<String, String> generateSm2Keys() throws Exception {
        AsymmetricCipherKeyPair keyPair = Sm2CipherUtils.generateSm2KeyPair();
        ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
        ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic();

        Map<String, String> keys = new HashMap<>();
        keys.put("publicKey", Sm2CipherUtils.encodePublicKey(publicKey));
        keys.put("privateKey", Sm2CipherUtils.encodePrivateKey(privateKey));
        return keys;
    }

    /**
     * 使用SM2公钥加密指定数据。
     *
     * @param requestPayload 包含待加密数据和公钥的请求体。
     * @return Base64编码的加密后数据字符串。
     */
    @PostMapping("/encrypt-with-public-key")
    @Operation(summary = "使用公钥加密数据", description = "接收明文数据和Base64编码的SM2公钥,返回Base64编码的密文")
    public String encryptPayload(@RequestBody Sm2EncryptRequest requestPayload) throws Exception {
        ECPublicKeyParameters pubKey = Sm2CipherUtils.decodePublicKey(requestPayload.getPublicKey());
        byte[] encryptedOutput = Sm2CipherUtils.encryptData(requestPayload.getData().getBytes(), pubKey);
        return Base64.getEncoder().encodeToString(encryptedOutput);
    }

    /**
     * 使用SM2私钥解密指定数据。
     *
     * @param requestPayload 包含待解密数据和私钥的请求体。
     * @return 解密后的原始数据字符串。
     */
    @PostMapping("/decrypt-with-private-key")
    @Operation(summary = "使用私钥解密数据", description = "接收Base64编码的密文和SM2私钥,返回解密后的明文")
    public String decryptPayload(@RequestBody Sm2DecryptRequest requestPayload) throws Exception {
        ECPrivateKeyParameters privKey = Sm2CipherUtils.decodePrivateKey(requestPayload.getPrivateKey());
        byte[] decodedCipher = Base64.getDecoder().decode(requestPayload.getEncryptedData());
        byte[] decryptedOutput = Sm2CipherUtils.decryptData(decodedCipher, privKey);
        return new String(decryptedOutput);
    }

    /**
     * 使用SM2私钥对数据进行签名。
     *
     * @param requestPayload 包含待签名数据和私钥的请求体。
     * @return Base64编码的签名字符串。
     */
    @PostMapping("/sign-data")
    @Operation(summary = "数据签名", description = "使用SM2私钥对数据进行签名,返回Base64编码的签名")
    public String signData(@RequestBody Sm2SignRequest requestPayload) throws Exception {
        ECPrivateKeyParameters privKey = Sm2CipherUtils.decodePrivateKey(requestPayload.getDataPrivateKey());
        byte[] signatureOutput = Sm2CipherUtils.signMessage(requestPayload.getOriginalData().getBytes(), privKey);
        return Base64.getEncoder().encodeToString(signatureOutput);
    }

    /**
     * 使用SM2公钥验证数据签名。
     *
     * @param requestPayload 包含原始数据、签名和公钥的请求体。
     * @return 验证结果,true表示签名有效,false表示签名无效。
     */
    @PostMapping("/verify-signature")
    @Operation(summary = "验证签名", description = "使用SM2公钥验证数据的签名是否有效")
    public boolean verifyDataSignature(@RequestBody Sm2VerifyRequest requestPayload) throws Exception {
        ECPublicKeyParameters pubKey = Sm2CipherUtils.decodePublicKey(requestPayload.getSignaturePublicKey());
        byte[] decodedSignature = Base64.getDecoder().decode(requestPayload.getSignature());
        return Sm2CipherUtils.verifySignature(requestPayload.getOriginalData().getBytes(), decodedSignature, pubKey);
    }

    // 内部请求体类,便于Swagger文档生成
    static class Sm2EncryptRequest {
        private String data;
        private String publicKey;
        // Getters and Setters
        public String getData() { return data; }
        public void setData(String data) { this.data = data; }
        public String getPublicKey() { return publicKey; }
        public void setPublicKey(String publicKey) { this.publicKey = publicKey; }
    }

    static class Sm2DecryptRequest {
        private String encryptedData;
        private String privateKey;
        // Getters and Setters
        public String getEncryptedData() { return encryptedData; }
        public void setEncryptedData(String encryptedData) { this.encryptedData = encryptedData; }
        public String getPrivateKey() { return privateKey; }
        public void setPrivateKey(String privateKey) { this.privateKey = privateKey; }
    }

    static class Sm2SignRequest {
        private String originalData;
        private String dataPrivateKey;
        // Getters and Setters
        public String getOriginalData() { return originalData; }
        public void setOriginalData(String originalData) { this.originalData = originalData; }
        public String getDataPrivateKey() { return dataPrivateKey; }
        public void setDataPrivateKey(String dataPrivateKey) { this.dataPrivateKey = dataPrivateKey; }
    }

    static class Sm2VerifyRequest {
        private String originalData;
        private String signature;
        private String signaturePublicKey;
        // Getters and Setters
        public String getOriginalData() { return originalData; }
        public void setOriginalData(String originalData) { this.originalData = originalData; }
        public String getSignature() { return signature; }
        public void setSignature(String signature) { this.signature = signature; }
        public String getSignaturePublicKey() { return signaturePublicKey; }
        public void setSignaturePublicKey(String signaturePublicKey) { this.signaturePublicKey = signaturePublicKey; }
    }
}

二、RSA算法与Redis集成

RSA是一种广泛使用的非对称加密算法,因其安全性被广泛应用于数据加密、数字签名等领域。然而,相比于国密SM2,RSA在相同的安全强度下通常需要更长的密钥长度,且计算效率稍低。本节将展示RSA的Java实现,并演示如何将私钥安全地存储在Redis中,以便在分布式环境中进行加解密操作。

2.1 RSA密钥服务

以下工具类封装了RSA密钥的生成、公钥加密、私钥解密、私钥签名和公钥验签等核心功能。我们将密钥长度设置为2048位以提供足够的安全强度。

RsaKeyService.java
package org.example.security.rsa;

import javax.crypto.Cipher;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

/**
 * RSA密钥管理与加密/解密/签名/验签服务
 */
public class RsaKeyService {

    // RSA算法名称
    private static final String RSA_ALGORITHM_NAME = "RSA";
    // SHA1withRSA签名算法
    private static final String SIGNATURE_ALGORITHM_SHA1 = "SHA1withRSA";
    // SHA256withRSA签名算法
    private static final String SIGNATURE_ALGORITHM_SHA256 = "SHA256withRSA";

    // RSA密钥长度,推荐使用2048位
    private static final int RSA_KEY_LENGTH = 2048;

    /**
     * 生成RSA密钥对。
     *
     * @return 包含Base64编码公钥和私钥字符串的Map。
     */
    public static Map<String, String> generateRsaKeyPair() {
        KeyPairGenerator keyPairGen;
        try {
            keyPairGen = KeyPairGenerator.getInstance(RSA_ALGORITHM_NAME);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("初始化RSA密钥失败: 算法不可用", e);
        }
        SecureRandom secureRandom = new SecureRandom();
        // 初始化密钥生成器,设定密钥长度和随机源
        keyPairGen.initialize(RSA_KEY_LENGTH, secureRandom);
        KeyPair keyPair = keyPairGen.genKeyPair();

        // 获取公钥并进行Base64编码
        byte[] rawPublicKey = keyPair.getPublic().getEncoded();
        String encodedPublicKey = Base64.getEncoder().encodeToString(rawPublicKey);

        // 获取私钥并进行Base64编码
        byte[] rawPrivateKey = keyPair.getPrivate().getEncoded();
        String encodedPrivateKey = Base64.getEncoder().encodeToString(rawPrivateKey);

        Map<String, String> keyPairData = new HashMap<>();
        keyPairData.put("publicKey", encodedPublicKey);
        keyPairData.put("privateKey", encodedPrivateKey);
        return keyPairData;
    }

    /**
     * 使用RSA公钥加密数据。
     *
     * @param originalData 待加密的明文字符串。
     * @param encodedPublicKey Base64编码的RSA公钥字符串。
     * @return Base64编码的加密后数据字符串。
     * @throws Exception 加密操作中可能发生的异常。
     */
    public static String encryptWithPublicKey(String originalData, String encodedPublicKey) throws Exception {
        byte[] publicKeyBytes = Base64.getDecoder().decode(encodedPublicKey);
        X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM_NAME);
        PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);

        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] encryptedBytes = cipher.doFinal(originalData.getBytes());
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    /**
     * 使用RSA私钥解密数据。
     *
     * @param encodedEncryptedData Base64编码的密文字符串。
     * @param encodedPrivateKey Base64编码的RSA私钥字符串。
     * @return 解密后的明文字符串。
     * @throws Exception 解密操作中可能发生的异常。
     */
    public static String decryptWithPrivateKey(String encodedEncryptedData, String encodedPrivateKey) throws Exception {
        byte[] privateKeyBytes = Base64.getDecoder().decode(encodedPrivateKey);
        PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM_NAME);
        PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);

        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encodedEncryptedData));
        return new String(decryptedBytes);
    }

    /**
     * 使用RSA私钥对数据进行签名。
     *
     * @param messageBytes 待签名的原始字节数据。
     * @param encodedPrivateKey Base64编码的RSA私钥字符串。
     * @param signAlgoType 签名算法类型,"SHA1"或"SHA256",默认为SHA256。
     * @return Base64编码的签名字符串。
     * @throws Exception 签名操作中可能发生的异常。
     */
    public static String signData(byte[] messageBytes, String encodedPrivateKey, String signAlgoType) throws Exception {
        byte[] privateKeyBytes = Base64.getDecoder().decode(encodedPrivateKey);
        PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM_NAME);
        PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);

        String algorithm = "SHA1".equalsIgnoreCase(signAlgoType) ? SIGNATURE_ALGORITHM_SHA1 : SIGNATURE_ALGORITHM_SHA256;
        Signature signature = Signature.getInstance(algorithm);
        signature.initSign(privateKey);
        signature.update(messageBytes);
        byte[] signedBytes = signature.sign();
        return Base64.getEncoder().encodeToString(signedBytes);
    }

    /**
     * 使用RSA公钥验证数字签名。
     *
     * @param messageBytes 原始消息的字节数据。
     * @param encodedSignature Base64编码的签名字符串。
     * @param encodedPublicKey Base64编码的RSA公钥字符串。
     * @param signAlgoType 签名算法类型,"SHA1"或"SHA256",默认为SHA256。
     * @return 如果签名有效则返回true,否则返回false。
     * @throws Exception 验签操作中可能发生的异常。
     */
    public static boolean verifySignature(byte[] messageBytes, String encodedSignature, String encodedPublicKey, String signAlgoType) throws Exception {
        byte[] publicKeyBytes = Base64.getDecoder().decode(encodedPublicKey);
        X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM_NAME);
        PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);

        String algorithm = "SHA1".equalsIgnoreCase(signAlgoType) ? SIGNATURE_ALGORITHM_SHA1 : SIGNATURE_ALGORITHM_SHA256;
        Signature signature = Signature.getInstance(algorithm);
        signature.initVerify(publicKey);
        signature.update(messageBytes);
        return signature.verify(Base64.getDecoder().decode(encodedSignature));
    }
}

2.2 RSA RESTful API控制器

此控制器为RSA操作提供RESTful接口,例如生成密钥对、使用公钥加密、私钥解密以及签名和验签。它与Redis的集成体现在实际应用中,私钥会从Redis中获取。

RsaCryptoController.java
package org.example.security.rsa;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.example.util.redis.RedisOperationHelper; // 假设Redis工具类包名
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * RSA加密模块的Web接口
 */
@RestController
@RequestMapping("/rsa")
@Tag(name = "RSA密码服务", description = "提供RSA密钥管理、数据加解密及签名验签功能")
public class RsaCryptoController {

    private final RedisOperationHelper redisHelper; // 假设通过构造器注入

    public RsaCryptoController(RedisOperationHelper redisHelper) {
        this.redisHelper = redisHelper;
    }

    /**
     * 生成RSA密钥对,并将私钥存储到Redis。
     *
     * @return 包含公钥和Redis中私钥标识符(keyId)的Map。
     */
    @GetMapping("/generate-and-store-keypair")
    @Operation(summary = "生成RSA密钥对并存储私钥到Redis", description = "生成一对RSA公私钥,将私钥存储到Redis,返回公钥和Redis KeyId")
    public Map<String, String> generateAndStoreRsaKeys() {
        Map<String, String> keyPair = RsaKeyService.generateRsaKeyPair();
        String publicKey = keyPair.get("publicKey");
        String privateKey = keyPair.get("privateKey");

        // 使用UUID作为Redis中的Key来存储私钥,并设置过期时间
        String keyId = UUID.randomUUID().toString();
        redisHelper.setCacheObject("rsa:privateKey:" + keyId, privateKey, 10L, TimeUnit.MINUTES);

        return Map.of("publicKey", publicKey, "keyId", keyId);
    }

    /**
     * 使用RSA公钥加密数据。
     *
     * @param request 请求体,包含待加密数据和公钥。
     * @return Base64编码的加密后数据字符串。
     */
    @PostMapping("/encrypt-with-public-key")
    @Operation(summary = "使用公钥加密数据", description = "接收明文数据和Base64编码的RSA公钥,返回Base64编码的密文")
    public String encryptPayload(@RequestBody RsaEncryptRequest request) throws Exception {
        return RsaKeyService.encryptWithPublicKey(request.getData(), request.getPublicKey());
    }

    /**
     * 使用存储在Redis中的私钥解密数据。
     *
     * @param request 请求体,包含加密数据和Redis中私钥的keyId。
     * @return 解密后的原始数据字符串。
     */
    @PostMapping("/decrypt-with-stored-private-key")
    @Operation(summary = "使用Redis私钥解密数据", description = "接收Base64编码的密文和Redis KeyId,从Redis获取私钥解密")
    public String decryptPayload(@RequestBody RsaDecryptRequest request) throws Exception {
        String privateKey = redisHelper.getCacheObject("rsa:privateKey:" + request.getKeyId());
        if (privateKey == null) {
            throw new IllegalArgumentException("私钥已过期或不存在");
        }
        return RsaKeyService.decryptWithPrivateKey(request.getEncryptedData(), privateKey);
    }

    /**
     * 使用存储在Redis中的私钥对数据进行签名。
     *
     * @param request 请求体,包含待签名数据、Redis中私钥的keyId和签名算法类型。
     * @return Base64编码的签名字符串。
     */
    @PostMapping("/sign-data-with-stored-private-key")
    @Operation(summary = "使用Redis私钥签名数据", description = "接收原始数据、Redis KeyId和签名类型,从Redis获取私钥进行签名")
    public String signData(@RequestBody RsaSignRequest request) throws Exception {
        String privateKey = redisHelper.getCacheObject("rsa:privateKey:" + request.getKeyId());
        if (privateKey == null) {
            throw new IllegalArgumentException("私钥已过期或不存在");
        }
        return RsaKeyService.signData(request.getOriginalData().getBytes(), privateKey, request.getSignType());
    }

    /**
     * 使用RSA公钥验证数据签名。
     *
     * @param request 请求体,包含原始数据、签名、公钥和签名算法类型。
     * @return 验证结果,true表示签名有效,false表示签名无效。
     */
    @PostMapping("/verify-signature-with-public-key")
    @Operation(summary = "使用公钥验证签名", description = "接收原始数据、签名、公钥和签名类型,验证签名的有效性")
    public boolean verifyDataSignature(@RequestBody RsaVerifyRequest request) throws Exception {
        return RsaKeyService.verifySignature(request.getOriginalData().getBytes(), request.getSignature(), request.getPublicKey(), request.getSignType());
    }

    // 内部请求体类,便于Swagger文档生成
    static class RsaEncryptRequest {
        private String data;
        private String publicKey;
        // Getters and Setters
        public String getData() { return data; }
        public void setData(String data) { this.data = data; }
        public String getPublicKey() { return publicKey; }
        public void setPublicKey(String publicKey) { this.publicKey = publicKey; }
    }

    static class RsaDecryptRequest {
        private String encryptedData;
        private String keyId; // Redis key ID for private key
        // Getters and Setters
        public String getEncryptedData() { return encryptedData; }
        public void setEncryptedData(String encryptedData) { this.encryptedData = encryptedData; }
        public String getKeyId() { return keyId; }
        public void setKeyId(String keyId) { this.keyId = keyId; }
    }

    static class RsaSignRequest {
        private String originalData;
        private String keyId; // Redis key ID for private key
        private String signType;
        // Getters and Setters
        public String getOriginalData() { return originalData; }
        public void setOriginalData(String originalData) { this.originalData = originalData; }
        public String getKeyId() { return keyId; }
        public void setKeyId(String keyId) { this.keyId = keyId; }
        public String getSignType() { return signType; }
        public void setSignType(String signType) { this.signType = signType; }
    }

    static class RsaVerifyRequest {
        private String originalData;
        private String signature;
        private String publicKey;
        private String signType;
        // Getters and Setters
        public String getOriginalData() { return originalData; }
        public void setOriginalData(String originalData) { this.originalData = originalData; }
        public String getSignature() { return signature; }
        public void setSignature(String signature) { this.signature = signature; }
        public String getPublicKey() { return publicKey; }
        public void setPublicKey(String publicKey) { this.publicKey = publicKey; }
        public String getSignType() { return signType; }
        public void setSignType(String signType) { this.signType = signType; }
    }
}

2.3 Redis配置与工具类

为了将RSA私钥存储在Redis中,我们需要配置Redis连接并提供一个方便操作Redis的工具类。

2.3.1 Redis依赖

引入Spring Data Redis Starter。

pom.xml
        <!-- Spring Data Redis 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
2.3.2 应用配置

application.yml中配置Redis连接信息。

application.yml
spring:
  data:
    redis:
      database: 5 # Redis数据库索引,默认为0
      host: localhost # Redis服务器地址
      port: 6379 # Redis服务器端口
      password: your-redis-password # Redis访问密码,请替换为实际密码
2.3.3 Redis配置类

配置RedisTemplate的序列化器,确保数据在Redis中的存储和读取正确。

AppRedisConfig.java
package org.example.config.redis;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Spring Boot Redis配置类,定制RedisTemplate
 */
@Configuration
public class AppRedisConfig {

    /**
     * 配置并创建RedisTemplate bean。
     * 该方法设置了键和值的序列化方式,以确保数据在Redis中能够正确存储和解析。
     *
     * @param connectionFactory Redis连接工厂,由Spring Boot自动配置提供。
     * @return 配置好的RedisTemplate实例。
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        // 使用StringRedisSerializer序列化键,确保键是可读的字符串
        template.setKeySerializer(new StringRedisSerializer());
        // 使用GenericJackson2JsonRedisSerializer序列化值,将Java对象序列化为JSON格式
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        // 也可以配置HashKey和HashValue的序列化
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        template.afterPropertiesSet(); // 应用所有属性设置
        return template;
    }
}
2.3.4 Redis操作辅助类

一个通用的Redis工具类,封装了常用的Redis数据类型操作,方便Service层调用。

RedisOperationHelper.java
package org.example.util.redis;

import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * Spring Redis操作辅助类
 */
@Component
public class RedisOperationHelper {

    private final RedisTemplate<String, Object> redisTemplate;

    // 通过构造器注入RedisTemplate
    public RedisOperationHelper(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 存储基本对象(字符串、数字、POJO等)。
     *
     * @param key   缓存键。
     * @param value 缓存值。
     * @param <T>   值类型。
     */
    public <T> void setStringValue(final String key, final T value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 存储基本对象并设置过期时间。
     *
     * @param key      缓存键。
     * @param value    缓存值。
     * @param timeout  过期时间。
     * @param timeUnit 时间单位。
     * @param <T>      值类型。
     */
    public <T> void setStringValue(final String key, final T value, final Long timeout, final TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置键的过期时间。
     *
     * @param key     Redis键。
     * @param timeout 超时时长。
     * @param unit    时间单位。
     * @return true表示设置成功,false表示失败。
     */
    public boolean setKeyExpiration(final String key, final long timeout, final TimeUnit unit) {
        Boolean result = redisTemplate.expire(key, timeout, unit);
        return result != null && result;
    }

    /**
     * 获取键的剩余过期时间。
     *
     * @param key Redis键。
     * @return 剩余过期时间(秒),-1表示永不过期,-2表示键不存在。
     */
    public long getKeyTtl(final String key) {
        Long ttl = redisTemplate.getExpire(key);
        return ttl != null ? ttl : -2; // -2表示键不存在,-1表示永不过期
    }

    /**
     * 检查Redis中是否存在指定键。
     *
     * @param key 键。
     * @return true表示存在,false表示不存在。
     */
    public boolean checkKeyExists(String key) {
        Boolean exists = redisTemplate.hasKey(key);
        return exists != null && exists;
    }

    /**
     * 获取缓存的基本对象。
     *
     * @param key 缓存键。
     * @param <T> 值类型。
     * @return 缓存键对应的值。
     */
    public <T> T getCacheObject(final String key) {
        ValueOperations<String, T> valueOps = (ValueOperations<String, T>) redisTemplate.opsForValue();
        return valueOps.get(key);
    }

    /**
     * 删除单个对象。
     *
     * @param key 要删除的键。
     * @return true表示删除成功,false表示失败。
     */
    public boolean deleteKey(final String key) {
        Boolean result = redisTemplate.delete(key);
        return result != null && result;
    }

    /**
     * 删除多个对象。
     *
     * @param keys 要删除的键集合。
     * @return 成功删除的数量。
     */
    public Long deleteMultipleKeys(final Collection<String> keys) {
        return redisTemplate.delete(keys);
    }

    /**
     * 缓存List数据。
     *
     * @param key      缓存键。
     * @param dataList 待缓存的List数据。
     * @param <T>      List元素类型。
     * @return 成功推入List的元素数量。
     */
    public <T> long setCacheList(final String key, final List<T> dataList) {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList.toArray());
        return count != null ? count : 0;
    }

    /**
     * 获取缓存的List对象。
     *
     * @param key 缓存键。
     * @param <T> List元素类型。
     * @return 缓存键对应的List数据。
     */
    public <T> List<T> getCacheList(final String key) {
        ListOperations<String, T> listOps = (ListOperations<String, T>) redisTemplate.opsForList();
        return listOps.range(key, 0, -1);
    }

    /**
     * 缓存Set数据。
     *
     * @param key     缓存键。
     * @param dataSet 待缓存的Set数据。
     * @param <T>     Set元素类型。
     * @return 成功添加的元素数量。
     */
    public <T> Long setCacheSet(final String key, final Set<T> dataSet) {
        SetOperations<String, T> setOps = (SetOperations<String, T>) redisTemplate.opsForSet();
        return setOps.add(key, dataSet.toArray());
    }

    /**
     * 获取缓存的Set对象。
     *
     * @param key 缓存键。
     * @param <T> Set元素类型。
     * @return 缓存键对应的Set数据。
     */
    public <T> Set<T> getCacheSet(final String key) {
        SetOperations<String, T> setOps = (SetOperations<String, T>) redisTemplate.opsForSet();
        return setOps.members(key);
    }

    /**
     * 缓存Map数据。
     *
     * @param key     缓存键。
     * @param dataMap 待缓存的Map数据。
     * @param <T>     Map值类型。
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获取缓存的Map对象。
     *
     * @param key 缓存键。
     * @param <T> Map值类型。
     * @return 缓存键对应的Map数据。
     */
    public <String, T> Map<String, T> getCacheMap(final String key) {
        return (Map<String, T>) redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据。
     *
     * @param key   Redis键。
     * @param hKey  Hash键。
     * @param value 值。
     * @param <T>   值类型。
     */
    public <T> void setHashValue(final String key, final String hKey, final T value) {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据。
     *
     * @param key  Redis键。
     * @param hKey Hash键。
     * @param <T>  值类型。
     * @return Hash中的值。
     */
    public <T> T getHashValue(final String key, final String hKey) {
        HashOperations<String, String, T> hashOps = (HashOperations<String, String, T>) redisTemplate.opsForHash();
        return hashOps.get(key, hKey);
    }

    /**
     * 获取Hash中的多个数据。
     *
     * @param key   Redis键。
     * @param hKeys Hash键集合。
     * @param <T>   值类型。
     * @return Hash对象集合。
     */
    public <T> List<T> getMultiHashValues(final String key, final Collection<Object> hKeys) {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 删除Hash中的某条数据。
     *
     * @param key  Redis键。
     * @param hKey Hash键。
     * @return true表示删除成功,false表示失败。
     */
    public boolean deleteHashValue(final String key, final String hKey) {
        Long result = redisTemplate.opsForHash().delete(key, hKey);
        return result != null && result > 0;
    }

    /**
     * 获取所有匹配给定模式的键。
     *
     * @param pattern 键的匹配模式。
     * @return 匹配的键集合。
     */
    public Set<String> getKeysByPattern(final String pattern) {
        return redisTemplate.keys(pattern);
    }
}

2.4 RSA与Redis集成测试

此测试类演示了RSA密钥生成、私钥存储到Redis、公钥加密、从Redis获取私钥解密,以及签名和验签的完整流程。

RsaKeyRedisIntegrationTest.java
package org.example.test;

import org.example.security.rsa.RsaKeyService;
import org.example.util.redis.RedisOperationHelper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * RSA密钥与Redis集成测试类
 */
@SpringBootTest
public class RsaKeyRedisIntegrationTest {

    @Autowired
    private RedisOperationHelper redisOps;

    /**
     * RSA数据加密、解密,并集成私钥Redis存储。
     *
     * @throws Exception 如果加密解密过程出错。
     */
    @Test
    public void testEncryptDecryptWithRedisPrivateKey() throws Exception {
        // 1. 生成RSA密钥对
        Map<String, String> rsaKeys = RsaKeyService.generateRsaKeyPair();
        String rsaPublicKey = rsaKeys.get("publicKey");
        String rsaPrivateKey = rsaKeys.get("privateKey");
        System.out.println("--- 生成的RSA密钥对 ---");
        System.out.println("公钥: " + rsaPublicKey);
        System.out.println("私钥: " + rsaPrivateKey);

        // 2. 待加密的原始数据
        String originalMessage = "transactionId=ORD12345&amount=500.00¤cy=USD";
        System.out.println("\n--- 加密与解密演示 ---");
        System.out.println("原始数据: " + originalMessage);

        // 3. 将私钥存储到Redis,并设定一个唯一标识符(keyId)和过期时间
        String privateKeyId = "rsa_priv_key_" + UUID.randomUUID().toString();
        redisOps.setStringValue(privateKeyId, rsaPrivateKey, 5L, TimeUnit.MINUTES);
        System.out.println("私钥已存储到Redis,Key ID: " + privateKeyId);

        // 4. 使用公钥加密数据
        String encryptedMessage = RsaKeyService.encryptWithPublicKey(originalMessage, rsaPublicKey);
        System.out.println("加密后数据: " + encryptedMessage);

        // 5. 从Redis中获取私钥进行解密
        String retrievedPrivateKey = redisOps.getCacheObject(privateKeyId);
        Assertions.assertNotNull(retrievedPrivateKey, "私钥未从Redis中成功检索到");
        String decryptedMessage = RsaKeyService.decryptWithPrivateKey(encryptedMessage, retrievedPrivateKey);
        System.out.println("解密后数据: " + decryptedMessage);

        // 6. 验证解密结果是否与原始数据一致
        Assertions.assertEquals(originalMessage, decryptedMessage, "解密后的数据与原始数据不匹配");
        System.out.println("加密解密流程验证成功!");
    }

    /**
     * RSA数据签名和验签。
     *
     * @throws Exception 如果签名验签过程出错。
     */
    @Test
    public void testRsaSignAndVerify() throws Exception {
        // 1. 生成RSA密钥对
        Map<String, String> rsaKeys = RsaKeyService.generateRsaKeyPair();
        String rsaPublicKey = rsaKeys.get("publicKey");
        String rsaPrivateKey = rsaKeys.get("privateKey");
        System.out.println("\n--- 签名与验签演示 ---");
        System.out.println("公钥: " + rsaPublicKey);
        System.out.println("私钥: " + rsaPrivateKey);

        // 2. 待签名数据
        String dataToSign = "documentHash=ABCDEFG12345×tamp=1678886400";
        System.out.println("待签名数据: " + dataToSign);

        // 3. 使用私钥进行签名,采用SHA256withRSA算法
        String signature = RsaKeyService.signData(dataToSign.getBytes(), rsaPrivateKey, "SHA256");
        System.out.println("数字签名结果: " + signature);

        // 4. 使用公钥验证签名
        boolean isSignatureValid = RsaKeyService.verifySignature(dataToSign.getBytes(), signature, rsaPublicKey, "SHA256");
        System.out.println("数字签名验证结果: " + isSignatureValid);

        // 5. 验证结果应为true
        Assertions.assertTrue(isSignatureValid, "签名验证失败");
        System.out.println("签名验签流程验证成功!");

        // 尝试篡改数据后验签,应失败
        String tamperedData = "documentHash=CHANGED_HASH×tamp=1678886400";
        boolean isTamperedSignatureValid = RsaKeyService.verifySignature(tamperedData.getBytes(), signature, rsaPublicKey, "SHA256");
        Assertions.assertFalse(isTamperedSignatureValid, "篡改数据后签名验证不应成功");
        System.out.println("篡改数据后签名验证结果: " + isTamperedSignatureValid + " (预期为false)");
    }
}

三、BCrypt密码哈希

BCrypt是一种密码哈希函数,它设计为比MD5或SHA系列等传统哈希算法更慢,以抵御彩虹表攻击和暴力破解。它内置了"盐"(salt)的生成和迭代计算的"成本因子"(cost factor),提供了更高的安全性,尤其适合用于存储用户密码。

3.1 Sa-Token依赖引入

本例将利用Sa-Token提供的BCrypt工具类,该库在权限认证领域广泛应用,并内置了便捷的加密工具。

        <!-- Sa-Token 权限认证,包含BCrypt工具 -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot3-starter</artifactId>
            <version>1.37.0</version>
        </dependency>

3.2 BCrypt哈希测试

以下测试代码展示了如何使用BCrypt对密码进行哈希、验证,并演示了不同成本因子对哈希强度和计算时间的影响。

BcryptPasswordHasherTest.java
package org.example.test;

import cn.dev33.satoken.secure.BCrypt;
import org.junit.jupiter.api.Test;

/**
 * BCrypt密码哈希功能测试类
 */
public class BcryptPasswordHasherTest {

    @Test
    public void testBcryptPasswordHashing() {
        // 定义一个明文密码
        String plainPassword = "mySecretPassword123";
        System.out.println("--- BCrypt密码哈希演示 ---");
        System.out.println("原始明文密码: " + plainPassword);

        // 1. 使用默认成本因子(10)生成盐并哈希密码
        String defaultSalt = BCrypt.gensalt();
        String hashedDefault = BCrypt.hashpw(plainPassword, defaultSalt);
        System.out.println("\n默认成本因子(10)哈希:");
        System.out.println("生成的盐值: " + defaultSalt);
        System.out.println("哈希后的密码: " + hashedDefault);

        // 验证默认哈希
        boolean isDefaultMatch = BCrypt.checkpw(plainPassword, hashedDefault);
        System.out.println("验证结果 (默认哈希): " + isDefaultMatch); // 应为true
        assert isDefaultMatch : "默认哈希验证失败";


        // 2. 使用更高的成本因子(例如12)生成盐并哈希密码
        // 成本因子越高,计算越慢,密码更难被暴力破解,但同时也增加了服务器的计算负担
        int highCostFactor = 12; // 成本因子通常在4到31之间,默认是10
        String strongerSalt = BCrypt.gensalt(highCostFactor);
        String hashedStrong = BCrypt.hashpw(plainPassword, strongerSalt);
        System.out.println("\n高成本因子 (" + highCostFactor + ") 哈希:");
        System.out.println("生成的盐值: " + strongerSalt);
        System.out.println("哈希后的密码: " + hashedStrong);

        // 验证高成本因子哈希
        boolean isStrongMatch = BCrypt.checkpw(plainPassword, hashedStrong);
        System.out.println("验证结果 (高成本因子哈希): " + isStrongMatch); // 应为true
        assert isStrongMatch : "高成本因子哈希验证失败";


        // 3. 演示密码不匹配的情况
        String incorrectPassword = "wrongPassword";
        boolean isIncorrectMatch = BCrypt.checkpw(incorrectPassword, hashedDefault);
        System.out.println("\n验证结果 (使用错误密码): " + isIncorrectMatch); // 应为false
        assert !isIncorrectMatch : "错误密码验证不应成功";

        System.out.println("\n总结: BCrypt通过自动生成盐和可配置的成本因子,提供了强大的密码哈希能力,有效增强了密码存储的安全性。");
    }
}

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。