【Java】常见加密算法


一、核心概念

在学习加密算法前,必须明确以下核心概念,避免与编码、哈希等概念混淆,这也是面试中高频基础考点,同时直接影响加密方案选型。

======= 🌟 青柠来相伴,代码更简单。🌟 =======
📚 本文所有内容,我都整理在了 青柠合集 里。👇
🎯 搜索关注【青柠代码录】,即可查看所有合集文章 ~
======= 🌟 ================ 🌟 =======
  • 密钥:加密/解密过程中使用的“密码”,分为对称密钥(加密和解密使用同一密钥)和非对称密钥(分为公钥和私钥,成对存在),密钥的安全性直接决定加密效果,生产环境需严格管理密钥(禁止硬编码、定期更换)。
  • 单向加密(不可逆加密):仅支持加密,无法通过密文还原为明文,核心作用是数据校验(如判断数据是否被篡改),而非数据保密,典型代表:MD5、SHA系列、CRC32。
  • 对称加密(可逆加密):加密和解密使用同一密钥,算法效率高、速度快,适合大量数据加密(如文件加密、批量数据传输),但密钥传输和存储存在安全风险,典型代表:AES、DES、3DES。
  • 非对称加密(可逆加密):生成一对密钥(公钥和私钥),公钥可公开,私钥需保密;公钥加密的内容,仅对应私钥可解密,反之亦然;安全性高,但算法效率低,适合小数据加密、密钥交换,典型代表:RSA、ECC。
  • 编码与加密的区别(重点避坑):编码(如Base64、URL编码)仅做数据格式转换,无加密作用,可直接解码还原,仅用于解决数据传输兼容性(如二进制图片转字符串传输);加密是对数据进行混淆处理,需借助密钥才能还原,核心作用是数据保密,生产环境禁止用编码替代加密(如禁止用Base64存储用户密码)。
  • 哈希碰撞:不同的明文经过哈希算法(单向加密)处理后,得到相同的密文,这是单向加密算法的固有缺陷,不同算法的碰撞概率不同,生产环境需根据场景选择碰撞概率低的算法(如SHA-256优于MD5)。

二、编码算法(非加密)

编码算法核心作用是“格式转换”,无加密、无密钥、可直接解码,仅用于解决数据传输或存储的兼容性问题,不属于加密算法,开发中最常用的是Base64,其次是URL编码。

2.1 Base64

2.1.1 核心原理

Base64是一种基于64个可打印字符(A-Z、a-z、0-9、+、/)的编码方式,将8位二进制数据转换为6位二进制数据,再映射到对应的可打印字符,最终实现二进制数据到ASCII字符串的转换。

由于每3个8位二进制数(共24位)可转换为4个6位二进制数(共24位),因此Base64编码后的字符串长度,通常是原数据长度的1.33倍左右;若原数据长度不是3的倍数,会用“=”填充,确保编码后长度是4的倍数。

核心特点:无密钥、可逆、编码后数据可直接解码、不具备加密能力,仅用于格式转换,任何人拿到Base64编码后的字符串,都能直接解码得到原始数据。

2.1.2 优缺点分析

  • 优点:原生支持(Java、Vue3均无需第三方依赖)、实现简单、兼容性强,可解决二进制数据(如图片、文件)无法直接通过文本接口传输的问题。
  • 缺点:无加密作用,编码后数据可直接解码;编码后数据体积增大(约增加33%),不适合大量二进制数据的长期存储。

2.1.3 适用场景

  • 合法场景:1. 图片转字符串(如前端上传图片,后端接收Base64编码字符串,再解码为图片文件存储);2. 接口参数传输(如二进制数据通过HTTP接口传输,转换为Base64避免传输异常);3. 加密结果格式化(如将对称加密、非对称加密的二进制密文,转换为Base64字符串,方便存储和传输)。
  • 禁止场景:1. 单独存储敏感数据(如用户密码、手机号、身份证号);2. 替代加密算法实现数据保密(如仅用Base64处理敏感数据,等同于明文传输)。

2.1.4 实战代码

Java 后端实现

说明:Java 8及以上原生提供java.util.Base64工具类,无需引入额外依赖。

import java.util.Base64;

/**
 * Base64 编码/解码工具类(含异常处理、URL安全编码)
 * 注意:此类仅用于编码转换,不具备加密能力,禁止用于敏感数据保密
 */

public class Base64Util {

    // 标准Base64编码器(默认,用于普通场景)
    private static final Base64.Encoder STANDARD_ENCODER = Base64.getEncoder();
    // 标准Base64解码器
    private static final Base64.Decoder STANDARD_DECODER = Base64.getDecoder();
    // URL安全的Base64编码器(替换特殊字符,用于URL参数传输)
    private static final Base64.Encoder URL_ENCODER = Base64.getUrlEncoder().withoutPadding();
    // URL安全的Base64解码器
    private static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder();

    /**
     * 标准Base64编码(字符串→Base64字符串)
     * @param str 原始字符串(可为null,返回null)
     * @return Base64编码后的字符串
     */

    public static String encode(String str) {
        if (str == null || str.isEmpty()) {
            return null;
        }
        // 字符串转字节数组(默认UTF-8编码,生产环境建议明确指定编码,避免环境差异)
        byte[] originalBytes = str.getBytes(StandardCharsets.UTF_8);
        // 编码为Base64字符串
        return STANDARD_ENCODER.encodeToString(originalBytes);
    }

    /**
     * 标准Base64解码(Base64字符串→原始字符串)
     * @param base64Str Base64编码字符串(可为null,返回null)
     * @return 解码后的原始字符串
     */

    public static String decode(String base64Str) {
        if (base64Str == null || base64Str.isEmpty()) {
            return null;
        }
        try {
            // 解码为字节数组
            byte[] decodeBytes = STANDARD_DECODER.decode(base64Str);
            // 字节数组转字符串(UTF-8编码)
            return new String(decodeBytes, StandardCharsets.UTF_8);
        } catch (IllegalArgumentException e) {
            // 捕获非法Base64字符串异常(如格式错误、包含非法字符)
            System.err.println("Base64解码失败,非法Base64字符串:" + base64Str);
            return null;
        }
    }

    /**
     * URL安全的Base64编码(用于URL参数传输,避免特殊字符)
     * @param str 原始字符串
     * @return URL安全的Base64字符串(无=填充,+替换为-,/替换为_)
     */

    public static String encodeUrlSafe(String str) {
        if (str == null || str.isEmpty()) {
            return null;
        }
        byte[] originalBytes = str.getBytes(StandardCharsets.UTF_8);
        return URL_ENCODER.encodeToString(originalBytes);
    }

    /**
     * URL安全的Base64解码
     * @param urlSafeBase64Str URL安全的Base64字符串
     * @return 解码后的原始字符串
     */

    public static String decodeUrlSafe(String urlSafeBase64Str) {
        if (urlSafeBase64Str == null || urlSafeBase64Str.isEmpty()) {
            return null;
        }
        try {
            byte[] decodeBytes = URL_DECODER.decode(urlSafeBase64Str);
            return new String(decodeBytes, StandardCharsets.UTF_8);
        } catch (IllegalArgumentException e) {
            System.err.println("URL安全Base64解码失败,非法字符串:" + urlSafeBase64Str);
            return null;
        }
    }

    // 测试方法(生产环境可删除,用于本地验证)
    public static void main(String[] args) {
        String original = "敏感数据测试123!@#";
        // 标准编码解码
        String standardEncode = encode(original);
        String standardDecode = decode(standardEncode);
        System.out.println("标准Base64编码结果:" + standardEncode);
        System.out.println("标准Base64解码结果:" + standardDecode);

        // URL安全编码解码
        String urlSafeEncode = encodeUrlSafe(original);
        String urlSafeDecode = decodeUrlSafe(urlSafeEncode);
        System.out.println("URL安全Base64编码结果:" + urlSafeEncode);
        System.out.println("URL安全Base64解码结果:" + urlSafeDecode);

        // 测试非法Base64字符串解码
        String illegalBase64 = "123456"// 非标准Base64字符串
        String illegalDecode = decode(illegalBase64);
        System.out.println("非法Base64解码结果:" + illegalDecode);
    }
}

Vue3 前端实现

说明:浏览器原生支持window.btoa()和window.atob()方法,但仅支持ASCII字符,若包含中文等非ASCII字符,需先进行编码转换(encodeURIComponent),否则会出现乱码。

/**
 * Vue3 Base64 工具函数(原生实现,无需安装依赖)
 * 注意:仅用于编码转换,不具备加密能力,禁止单独处理敏感数据(如密码)
 */

export const base64Util = {
  /**
   * 标准Base64编码(支持中文)
   * @param {string} str - 原始字符串(可为空,返回空字符串)
   * @returns {string} Base64编码后的字符串
   */

  encode(str) {
    if (!str) return '';
    // 处理中文:先将中文编码为UTF-8,再进行Base64编码,避免乱码
    return window.btoa(unescape(encodeURIComponent(str)));
  },

  /**
   * 标准Base64解码(支持中文)
   * @param {string} base64Str - Base64编码字符串(可为空,返回空字符串)
   * @returns {string} 解码后的原始字符串
   */

  decode(base64Str) {
    if (!base64Str) return '';
    try {
      // 先解码Base64,再还原中文编码
      return decodeURIComponent(escape(window.atob(base64Str)));
    } catch (e) {
      console.error('Base64解码失败,非法字符串:', base64Str);
      return '';
    }
  },

  /**
   * URL安全的Base64编码(适配URL参数传输)
   * @param {string} str - 原始字符串
   * @returns {string} URL安全的Base64字符串
   */

  encodeUrlSafe(str) {
    if (!str) return '';
    const base64 = this.encode(str);
    // 替换特殊字符:+→-,/→_,去除=填充
    return base64.replace(/\+/g'-').replace(/\//g'_').replace(/=/g'');
  },

  /**
   * URL安全的Base64解码
   * @param {string} urlSafeBase64Str - URL安全的Base64字符串
   * @returns {string} 解码后的原始字符串
   */

  decodeUrlSafe(urlSafeBase64Str) {
    if (!urlSafeBase64Str) return '';
    // 还原特殊字符:-→+,_→/,补充=填充(确保长度是4的倍数)
    let base64Str = urlSafeBase64Str.replace(/-/g'+').replace(/_/g'/');
    while (base64Str.length % 4 !== 0) {
      base64Str += '=';
    }
    return this.decode(base64Str);
  }
};

// 测试(前端控制台可直接运行)
// const original = '敏感数据测试123!@#';
// console.log('标准编码:', base64Util.encode(original));
// console.log('标准解码:', base64Util.decode(base64Util.encode(original)));
// console.log('URL安全编码:', base64Util.encodeUrlSafe(original));
// console.log('URL安全解码:', base64Util.decodeUrlSafe(base64Util.encodeUrlSafe(original)));

2.2 URL编码

URL编码(也叫百分号编码),核心作用是将URL中的特殊字符(如空格、@、#、&等)转换为%加十六进制的形式,避免URL参数解析异常,同样不属于加密算法,可直接解码。

实战代码(Java+Vue3)

// Java 后端 URL编码/解码(原生实现,无依赖)
import java.net.URLEncoder;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

public class UrlEncodeUtil {

    /**
     * URL编码(UTF-8编码,适配所有字符)
     * @param str 原始字符串
     * @return URL编码后的字符串
     */

    public static String encode(String str) {
        if (str == null || str.isEmpty()) {
            return null;
        }
        try {
            // 明确指定UTF-8编码,避免环境差异导致的乱码
            return URLEncoder.encode(str, StandardCharsets.UTF_8.name());
        } catch (Exception e) {
            System.err.println("URL编码失败:" + e.getMessage());
            return null;
        }
    }

    /**
     * URL解码(UTF-8编码)
     * @param urlEncodedStr URL编码后的字符串
     * @return 解码后的原始字符串
     */

    public static String decode(String urlEncodedStr) {
        if (urlEncodedStr == null || urlEncodedStr.isEmpty()) {
            return null;
        }
        try {
            return URLDecoder.decode(urlEncodedStr, StandardCharsets.UTF_8.name());
        } catch (Exception e) {
            System.err.println("URL解码失败:" + e.getMessage());
            return null;
        }
    }

    public static void main(String[] args) {
        String original = "测试&参数=123 456";
        String encode = encode(original);
        String decode = decode(encode);
        System.out.println("URL编码结果:" + encode); // 输出:%E4%BC%81%E4%B8%9A%E7%BA%A7%E6%B5%8B%E8%AF%95%26%E5%8F%82%E6%95%B0%3D123+456
        System.out.println("URL解码结果:" + decode);
    }
}
// Vue3 前端 URL编码/解码(原生实现,无需依赖)
export const urlEncodeUtil = {
  // URL编码
  encode(str) {
    if (!str) return '';
    return encodeURIComponent(str);
  },
  // URL解码
  decode(urlEncodedStr) {
    if (!urlEncodedStr) return '';
    try {
      return decodeURIComponent(urlEncodedStr);
    } catch (e) {
      console.error('URL解码失败:', e);
      return '';
    }
  }
};

// 测试
// const original = '测试&参数=123 456';
// console.log('URL编码:', urlEncodeUtil.encode(original));
// console.log('URL解码:', urlEncodeUtil.decode(urlEncodeUtil.encode(original)));

三、单向散列加密算法(不可逆)

单向散列加密(又称哈希加密),核心特点是“不可逆”——仅能将明文转换为固定长度的密文(哈希值),无法通过密文还原为明文;相同的明文,经过同一算法处理,得到的哈希值完全相同;不同的明文,得到的哈希值大概率不同(存在碰撞风险)。

核心作用:数据校验(判断数据是否被篡改)、密码存储(无需存储明文,仅存储哈希值,即使哈希值泄露,攻击者也无法还原明文)、文件完整性校验(如下载文件后,对比哈希值判断文件是否被篡改)。

开发中最常用的单向散列加密算法:MD5、SHA-1、SHA-256、SHA-512,其中MD5和SHA-1因碰撞概率较高,生产环境需谨慎使用(禁止单独用于密码存储),SHA-256、SHA-512安全性更高,是目前主流选择。

3.1 MD5 加密

3.1.1 核心原理

MD5(Message-Digest Algorithm 5,消息摘要算法5),输出固定长度为128位(16字节)的哈希值,通常以32位十六进制字符串(0-9、a-f)形式展示(也可转换为16位十六进制字符串,截取32位的中间16位)。

底层逻辑:将明文按固定长度分组,每组512位,对每组进行多轮复杂的逻辑运算(填充、循环移位、异或等),最终输出128位的哈希值。由于运算过程不可逆,且哈希值长度固定,无法通过哈希值反推明文。

3.1.2 优缺点分析

  • 优点:运算速度极快(比SHA系列快很多)、实现简单、哈希值长度短(32位字符串),适合对性能要求高、非敏感数据的校验场景。
  • 缺点:碰撞概率较高(已被证明存在大量碰撞案例),安全性较低;易被彩虹表(预计算好的明文-哈希值对应表)破解,生产环境禁止单独用于密码存储。

3.1.3 适用场景

  • 推荐场景:1. 文件完整性校验(如用户下载文件后,对比本地计算的MD5哈希值与服务器提供的哈希值,判断文件是否完整、未被篡改);2. 接口参数签名(非敏感接口,用于判断参数是否被篡改,需配合时间戳、随机数使用,避免重放攻击);3. 非敏感数据的快速校验(如日志内容校验)。
  • 禁止场景:1. 单独存储用户密码、手机号、身份证号等敏感数据;2. 高安全性要求的校验场景(如支付接口签名)。

3.1.4 优化方案

若因历史项目、性能需求必须使用MD5,需通过以下方式提升安全性,避免被破解:

  • 加盐(Salt):在明文后拼接一段自定义的随机字符串(盐值),再进行MD5加密,盐值需保密(配置在配置中心,如Nacos、Apollo,禁止硬编码),且每个用户的盐值可不同(如用用户ID作为盐值的一部分),可有效抵御彩虹表破解。
  • 多次加密:对明文进行多次MD5加密(如加密3次、5次),增加破解难度,但会略微降低性能,需权衡性能与安全性。
  • 结合其他算法:如先对明文进行SHA-256加密,再对结果进行MD5加密,兼顾性能与安全性。

3.1.5 实战代码(Java后端,含加盐、多次加密)

说明:Java原生支持MD5算法(通过java.security.MessageDigest实现),无需第三方依赖。

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;

/**
 * MD5 加密工具类(生产版,含加盐、多次加密、异常处理)
 * 注意:MD5安全性较低,存在碰撞风险,禁止单独用于敏感数据(如密码)存储;
 * 若必须使用,需配合加盐、多次加密等优化方案,优先推荐使用SHA-256替代。
 */

public class MD5Util {
    // 加密算法标识(固定为"MD5")
    private static final String ALGORITHM = "MD5";
    // 自定义全局盐值(生产环境配置在Nacos/Apollo,禁止硬编码,定期更换)
    private static final String GLOBAL_SALT = "qingning_java_vue3_2026_encryption";
    // 多次加密次数(生产环境可配置,建议3-5次)
    private static final int ENCRYPT_TIMES = 3;

    /**
     * MD5基础加密(无盐,不推荐直接使用)
     * @param str 原始明文
     * @return 32位十六进制哈希值
     * @throws NoSuchAlgorithmException 算法不存在异常(理论上不会出现)
     */

    public static String md5Base(String str) throws NoSuchAlgorithmException {
        if (str == null || str.isEmpty()) {
            return null;
        }
        // 获取MD5算法实例
        MessageDigest md = MessageDigest.getInstance(ALGORITHM);
        // 将明文转换为字节数组(UTF-8编码)
        byte[] originalBytes = str.getBytes(StandardCharsets.UTF_8);
        // 执行加密运算,得到加密后的字节数组(128位,16字节)
        byte[] encryptBytes = md.digest(originalBytes);
        // 将字节数组转换为32位十六进制字符串
        return byteToHex(encryptBytes);
    }

    /**
     * MD5加盐加密(推荐使用,主流方案)
     * @param str 原始明文
     * @param userSalt 用户自定义盐值(可结合用户ID、手机号等,每个用户不同)
     * @return 32位十六进制哈希值
     */

    public static String md5WithSalt(String str, String userSalt) {
        if (str == null || str.isEmpty() || userSalt == null || userSalt.isEmpty()) {
            return null;
        }
        // 拼接明文、全局盐值、用户盐值(顺序可自定义,需前后端一致)
        String content = str + GLOBAL_SALT + userSalt;
        try {
            // 多次加密,提升安全性
            String encryptResult = content;
            for (int i = 0; i < ENCRYPT_TIMES; i++) {
                encryptResult = md5Base(encryptResult);
            }
            return encryptResult;
        } catch (NoSuchAlgorithmException e) {
            System.err.println("MD5加盐加密失败:" + e.getMessage());
            return null;
        }
    }

    /**
     * 获取16位MD5哈希值(截取32位的中间16位)
     * @param str 原始明文
     * @param userSalt 用户盐值
     * @return 16位十六进制哈希值
     */

    public static String md5WithSalt16(String str, String userSalt) {
        String md532 = md5WithSalt(str, userSalt);
        if (md532 == null) {
            return null;
        }
        // 截取32位字符串的第8位到第24位(共16位)
        return md532.substring(824);
    }

    /**
     * 字节数组转换为十六进制字符串(工具方法,私有)
     * @param bytes 加密后的字节数组
     * @return 十六进制字符串(32位)
     */

    private static String byteToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        // 遍历字节数组,将每个字节转换为两位十六进制数
        for (byte b : bytes) {
            // 转换为无符号整数(避免负数)
            int num = b & 0xFF;
            // 转换为十六进制字符串,若不足两位,前面补0
            String hex = Integer.toHexString(num);
            if (hex.length() < 2) {
                sb.append("0");
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    // 测试方法(生产环境可删除)
    public static void main(String[] args) {
        String original = "user123456"// 原始密码
        String userSalt = "user_10086"// 该用户的专属盐值(可存储在数据库,与用户信息关联)

        // 基础加密(无盐)
        try {
            String md5Base = md5Base(original);
            System.out.println("MD5基础加密(无盐)32位结果:" + md5Base);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        // 加盐加密(32位)
        String md5Salt32 = md5WithSalt(original, userSalt);
        System.out.println("MD5加盐加密(32位)结果:" + md5Salt32);

        // 加盐加密(16位)
        String md5Salt16 = md5WithSalt16(original, userSalt);
        System.out.println("MD5加盐加密(16位)结果:" + md5Salt16);

        // 测试相同明文+相同盐值,加密结果一致
        String md5Salt32Again = md5WithSalt(original, userSalt);
        System.out.println("相同明文+相同盐值,加密结果是否一致:" + md5Salt32.equals(md5Salt32Again)); // true

        // 测试不同明文,加密结果不同
        String original2 = "user654321";
        String md5Salt32Different = md5WithSalt(original2, userSalt);
        System.out.println("不同明文,加密结果是否不同:" + !md5Salt32.equals(md5Salt32Different)); // true
    }
}

3.2 SHA 系列加密

SHA(Secure Hash Algorithm,安全哈希算法)系列,是美国国家标准与技术研究院(NIST)发布的哈希算法,安全性远高于MD5,目前开发中最常用的是SHA-256和SHA-512,SHA-1因碰撞风险已被淘汰,不推荐使用。

SHA系列核心区别:哈希值长度不同,运算速度不同,安全性随哈希值长度增加而提升(SHA-512 > SHA-256 > SHA-1),但运算速度随哈希值长度增加而降低(SHA-512 < SHA-256 < SHA-1)。

3.2.1 SHA-256 加密

核心原理

SHA-256属于SHA-2系列(SHA-2包括SHA-224、SHA-256、SHA-384、SHA-512),输出固定长度为256位(32字节)的哈希值,通常以64位十六进制字符串形式展示。

底层逻辑与MD5类似,均为分组运算、多轮逻辑处理,但分组长度更长(SHA-256分组为512位,与MD5一致),运算轮次更多(64轮,MD5为16轮),碰撞概率极低(目前未发现实际碰撞案例),安全性远高于MD5。

优缺点分析

  • 优点:安全性高(碰撞概率极低)、实现简单、原生支持,兼顾安全性与性能,是目前密码存储、高安全性校验场景的首选。
  • 缺点:运算速度比MD5慢(但满足绝大多数场景需求),哈希值长度较长(64位字符串),存储和传输时占用略多空间。

适用场景

  • 核心场景:1. 用户密码存储(推荐,配合加盐机制,即使哈希值泄露,攻击者也无法还原明文);2. 接口签名(高安全性接口,如支付、登录、敏感数据查询接口);3. 敏感数据校验(如用户身份证号、手机号的校验);4. 重要文件完整性校验(如系统安装包、加密文件)。
  • 其他场景:数据脱敏后的校验、日志防篡改、区块链数据校验(部分区块链项目使用SHA-256)。

实战代码(Java后端+Vue3前端)

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;

/**
 * SHA-256 加密工具类(生产版,含加盐、异常处理,推荐使用)
 * 适用场景:密码存储、接口签名、敏感数据校验等高安全性需求场景
 */

public class SHA256Util {
    // 加密算法标识(固定为"SHA-256")
    private static final String ALGORITHM = "SHA-256";
    // 全局盐值(生产环境配置在Nacos/Apollo,禁止硬编码)
    private static final String GLOBAL_SALT = "qingning_sha256_global_salt_2026";
    // 加密次数(建议3次,平衡安全性与性能)
    private static final int ENCRYPT_TIMES = 3;

    /**
     * SHA-256基础加密(无盐,不推荐直接使用)
     * @param str 原始明文
     * @return 64位十六进制哈希值
     * @throws NoSuchAlgorithmException 算法不存在异常
     */

    public static String sha256Base(String str) throws NoSuchAlgorithmException {
        if (str == null || str.isEmpty()) {
            return null;
        }
        MessageDigest md = MessageDigest.getInstance(ALGORITHM);
        byte[] originalBytes = str.getBytes(StandardCharsets.UTF_8);
        byte[] encryptBytes = md.digest(originalBytes);
        return byteToHex(encryptBytes);
    }

    /**
     * SHA-256加盐加密(推荐方案)
     * @param str 原始明文
     * @param userSalt 用户专属盐值(如用户ID、手机号,每个用户不同,存储在数据库)
     * @return 64位十六进制哈希值
     */

    public static String sha256WithSalt(String str, String userSalt) {
        if (str == null || str.isEmpty() || userSalt == null || userSalt.isEmpty()) {
            return null;
        }
        // 拼接明文、全局盐值、用户盐值(顺序固定,前后端需一致)
        String content = str + GLOBAL_SALT + userSalt;
        try {
            String encryptResult = content;
            // 多次加密,提升安全性
            for (int i = 0; i< ENCRYPT_TIMES; i++) {
                encryptResult = sha256Base(encryptResult);
            }
            return encryptResult;
        } catch (NoSuchAlgorithmException e) {
            System.err.println("SHA-256加盐加密失败:" + e.getMessage());
            return null;
        }
    }

    /**
     * 字节数组转换为十六进制字符串(工具方法)
     * @param bytes 加密后的字节数组(256位,32字节)
     * @return 64位十六进制字符串
     */

    private static String byteToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            int num = b & 0xFF;
            String hex = Integer.toHexString(num);
            if (hex.length() < 2) {
                sb.append("0");
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    // 测试方法
    public static void main(String[] args) {
        String original = "user123456"// 原始密码
        String userSalt = "user_10086"// 用户专属盐值

        // 基础加密(无盐)
        try {
            String sha256Base = sha256Base(original);
            System.out.println("SHA-256基础加密(无盐)结果:" + sha256Base);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        // 加盐加密
        String sha256Salt = sha256WithSalt(original, userSalt);
        System.out.println("SHA-256加盐加密结果:" + sha256Salt);

        // 测试一致性
        String sha256SaltAgain = sha256WithSalt(original, userSalt);
        System.out.println("相同明文+相同盐值,加密结果一致:" + sha256Salt.equals(sha256SaltAgain)); // true

        // 测试不同明文
        String original2 = "user654321";
        String sha256SaltDifferent = sha256WithSalt(original2, userSalt);
        System.out.println("不同明文,加密结果不同:" + !sha256Salt.equals(sha256SaltDifferent)); // true
    }
}
/**
 * Vue3 SHA-256 工具函数(需引入crypto-js依赖,前端常用方案)
 * 说明:浏览器原生crypto API兼容性较差,推荐使用crypto-js第三方库(npm install crypto-js)
 * 适用场景:前端密码加密(如登录页,密码加密后再传输到后端,避免明文传输)
 */

import CryptoJS from 'crypto-js';

// 全局盐值(需与后端一致,建议从后端接口获取,避免前端硬编码)
const GLOBAL_SALT = 'qingning_sha256_global_salt_2026';
// 加密次数(与后端一致)
const ENCRYPT_TIMES = 3;

export const sha256Util = {
  /**
   * SHA-256基础加密(无盐)
   * @param {string} str - 原始明文
   * @returns {string} 64位十六进制哈希值
   */

  sha256Base(str) {
    if (!str) return '';
    // 使用crypto-js的SHA256方法,转换为十六进制字符串
    return CryptoJS.SHA256(str).toString(CryptoJS.enc.Hex);
  },

  /**
   * SHA-256加盐加密(与后端逻辑一致)
   * @param {string} str - 原始明文(如用户密码)
   * @param {string} userSalt - 用户专属盐值(从后端获取,如用户ID)
   * @returns {string} 64位十六进制哈希值
   */

  sha256WithSalt(str, userSalt) {
    if (!str || !userSalt) return '';
    let content = str + GLOBAL_SALT + userSalt;
    let encryptResult = content;
    // 多次加密,与后端次数一致
    for (let i = 0; i < ENCRYPT_TIMES; i++) {
      encryptResult = this.sha256Base(encryptResult);
    }
    return encryptResult;
  }
};

// 测试(需安装crypto-js依赖)
// const original = 'user123456';
// const userSalt = 'user_10086';
// console.log('SHA-256基础加密:', sha256Util.sha256Base(original));
// console.log('SHA-256加盐加密:', sha256Util.sha256WithSalt(original, userSalt));

3.2.2 SHA-512 加密

SHA-512同样属于SHA-2系列,输出固定长度为512位(64字节)的哈希值,通常以128位十六进制字符串形式展示,安全性比SHA-256更高,但运算速度更慢,适合核心敏感场景(如金融、支付、核心密码存储)。

实战代码(Java后端,核心代码)

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;

/**
 * SHA-512 加密工具类(高安全性,适合核心敏感场景)
 */

public class SHA512Util {
    private static final String ALGORITHM = "SHA-512";
    private static final String GLOBAL_SALT = "qingning_sha512_global_salt_2026";
    private static final int ENCRYPT_TIMES = 5// 加密次数可适当增加

    // 基础加密
    public static String sha512Base(String str) throws NoSuchAlgorithmException {
        if (str == null || str.isEmpty()) {
            return null;
        }
        MessageDigest md = MessageDigest.getInstance(ALGORITHM);
        byte[] originalBytes = str.getBytes(StandardCharsets.UTF_8);
        byte[] encryptBytes = md.digest(originalBytes);
        return byteToHex(encryptBytes);
    }

    // 加盐加密(核心方法)
    public static String sha512WithSalt(String str, String userSalt) {
        if (str == null || str.isEmpty() || userSalt == null || userSalt.isEmpty()) {
            return null;
        }
        String content = str + GLOBAL_SALT + userSalt;
        try {
            String encryptResult = content;
            for (int i = 0; i < ENCRYPT_TIMES; i++) {
                encryptResult = sha512Base(encryptResult);
            }
            return encryptResult;
        } catch (NoSuchAlgorithmException e) {
            System.err.println("SHA-512加盐加密失败:" + e.getMessage());
            return null;
        }
    }

    // 字节数组转十六进制字符串
    private static String byteToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            int num = b & 0xFF;
            String hex = Integer.toHexString(num);
            if (hex.length() < 2) {
                sb.append("0");
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    // 测试
    public static void main(String[] args) {
        String original = "admin@123456";
        String userSalt = "admin_001";
        String sha512Salt = sha512WithSalt(original, userSalt);
        System.out.println("SHA-512加盐加密结果(128位):" + sha512Salt);
    }
}

四、对称加密算法(可逆)

对称加密算法的核心特点:加密和解密使用同一密钥(对称密钥),算法效率高、运算速度快、加密强度高,适合大量数据加密(如文件加密、批量数据传输、数据库敏感字段加密),是开发中用于数据保密的核心算法。

核心痛点:密钥的传输和存储安全——由于加密和解密使用同一密钥,若密钥泄露,加密数据将被轻易破解;因此,对称密钥的管理(如密钥生成、传输、存储、更换)是对称加密的核心重点。

开发中最常用的对称加密算法:AES(Advanced Encryption Standard,高级加密标准),DES、3DES因安全性较低、效率不高,已逐渐被AES替代,本文重点讲解AES(主流),补充DES、3DES的核心知识点。

4.1 AES 加密(首选)

AES是目前全球公认的最安全、最高效的对称加密算法,被广泛应用于开发、金融、军事等领域,替代了传统的DES算法,支持三种密钥长度:128位、192位、256位,密钥长度越长,安全性越高,但运算速度越慢;开发中最常用的是128位(兼顾安全性与性能),256位适合核心敏感数据(如金融交易数据)。

4.1.1 核心原理

AES属于分组密码算法,将明文按固定长度分组(分组长度为128位,即16字节),对每组明文进行多轮加密运算(128位密钥对应10轮,192位对应12轮,256位对应14轮),每轮运算包括字节代换、行移位、列混合、轮密钥加四个步骤,最终输出与明文分组长度相同的密文;解密过程是加密过程的逆运算,使用同一密钥。

核心补充:AES加密需要三个核心参数——密钥(Key)、加密模式(Mode)、填充方式(Padding),三个参数必须与解密时完全一致,否则无法解密,这是开发中最容易踩坑的点。

4.1.2 核心参数详解

(1)密钥(Key)

密钥是对称加密的核心,AES支持128位、192位、256位密钥,对应密钥字符串长度(UTF-8编码)分别为16位、24位、32位(如128位密钥:“qingning12345678”,16个字符)。

要求:1. 密钥长度建议使用128位及以上;2. 密钥需随机生成(禁止手动设置简单密钥,如“12345678”);3. 密钥存储在配置中心(如Nacos、Apollo),禁止硬编码;4. 定期更换密钥(如每3个月更换一次);5. 密钥传输需加密(如通过非对称加密传输对称密钥)。

(2)加密模式(Mode)

加密模式决定了分组明文的加密方式,开发中最常用的是ECB模式和CBC模式,其他模式(如CTR、OFB)用于特殊场景,具体区别如下:

  • ECB模式(电子密码本模式):最简单的模式,将每一组明文独立加密,相同的明文分组会得到相同的密文分组,安全性较低(易被破解),适合小数据加密(如单个字符串),不适合大量数据、敏感数据加密。
  • CBC模式(密码分组链接模式):安全性高于ECB模式,加密前需指定初始向量(IV,Initial Vector),每个明文分组加密时,会与前一个密文分组进行异或运算,相同的明文分组会得到不同的密文分组,适合大量数据、敏感数据加密(如文件加密、数据库敏感字段加密),是开发中AES的首选模式。核心要求:初始向量(IV)需随机生成,长度与AES分组长度一致(128位,即16字节),IV无需保密,但需与解密时完全一致,且每次加密需使用不同的IV(避免相同明文+相同密钥+相同IV导致加密结果重复,降低安全性)。

(3)填充方式(Padding)

AES属于分组密码算法,仅能对固定长度(128位)的明文分组进行加密,若明文长度不是128位的整数倍,需通过填充方式补全,使每组长度满足128位;解密时需使用相同的填充方式,否则无法正确还原明文。开发中最常用的填充方式有两种,需前后端严格一致:

  • PKCS5Padding:最常用,填充长度为1-16字节(若明文长度是128位的整数倍,仍需填充16字节),填充内容为填充的字节数(如需要填充3字节,填充内容为0x03、0x03、0x03),兼容性强,Java、Vue3均原生支持。
  • NoPadding:无填充,要求明文长度必须是128位的整数倍,适合明文长度固定的场景(如固定长度的加密字段),使用场景较少,需手动处理明文长度补全。

4.1.3 优缺点分析

  • 优点:运算速度极快(比非对称加密快100-1000倍)、加密强度高(128位AES目前无法被暴力破解)、兼容性强(Java、Vue3、数据库均原生支持)、适合大量数据加密,是数据保密的首选。
  • 缺点:密钥管理难度大,加密和解密使用同一密钥,密钥泄露会导致所有加密数据被破解;密钥传输存在安全风险(需通过非对称加密等安全方式传输)。

4.1.4 适用场景

  • 核心场景:1. 数据库敏感字段加密(如用户手机号、身份证号、银行卡号等,加密后存储,避免明文泄露);2. 大量数据传输加密(如批量用户数据、文件传输,效率高于非对称加密);3. 本地文件加密(如系统配置文件、敏感日志文件);4. 接口敏感参数加密(如支付金额、订单号等核心参数,避免传输过程中被篡改、窃取)。
  • 其他场景:APP本地数据加密(如用户缓存信息)、消息队列敏感消息加密、第三方接口通信加密(需与第三方约定密钥、模式、填充方式)。

4.1.5 优化方案

  • 密钥管理(核心):1. 密钥随机生成(使用Java原生SecureRandom类生成,禁止手动设置简单密钥);2. 密钥分级存储(全局密钥存储在配置中心,用户级密钥存储在数据库,加密存储);3. 定期更换密钥(每3-6个月更换一次,更换时需对历史加密数据进行重新加密,避免旧密钥泄露导致数据风险);4. 密钥传输加密(如通过RSA非对称加密传输AES密钥,避免密钥明文传输)。
  • 安全优化:1. 优先使用CBC模式+PKCS5Padding填充方式,禁止使用ECB模式用于敏感数据;2. 初始向量(IV)随机生成,每次加密使用不同IV,IV可随密文一起传输(无需保密);3. 加密结果格式化(将二进制密文转换为Base64字符串,方便存储和传输);4. 补充异常处理(如密钥长度错误、IV长度错误、填充方式不匹配导致的解密异常)。

4.1.6 实战代码

Java 后端实现

说明:Java原生通过javax.crypto包实现AES加密,无需引入第三方依赖。

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;

/**
 * AES 加密工具类(生产版,CBC模式+PKCS5Padding,128位密钥)
 * 适用场景:数据库敏感字段加密、文件加密、接口敏感参数加密
 * 注意:密钥需配置在Nacos/Apollo,禁止硬编码;IV随机生成,随密文传输
 */

public class AESUtil {
    // 加密算法+模式+填充方式(固定,前后端需一致)
    private static final String ALGORITHM_MODE_PADDING = "AES/CBC/PKCS5Padding";
    // 加密算法(固定)
    private static final String ALGORITHM = "AES";
    // 密钥长度(128位,常用,可改为192、256位,需配置JDK权限)
    private static final int KEY_SIZE = 128;
    // IV长度(与AES分组长度一致,128位=16字节,固定)
    private static final int IV_SIZE = 16;

    /**
     * 随机生成AES密钥(128位),可用于初始化密钥,存储在配置中心
     * @return Base64编码后的密钥字符串(方便存储和传输)
     */

    public static String generateAesKey() {
        try {
            // 随机生成密钥,使用SecureRandom保证随机性
            KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);
            SecureRandom secureRandom = new SecureRandom();
            keyGenerator.init(KEY_SIZE, secureRandom);
            SecretKey secretKey = keyGenerator.generateKey();
            // 密钥转换为Base64字符串
            return Base64.getEncoder().encodeToString(secretKey.getEncoded());
        } catch (Exception e) {
            System.err.println("AES密钥生成失败:" + e.getMessage());
            throw new RuntimeException("AES密钥生成异常", e);
        }
    }

    /**
     * 随机生成IV(初始向量,16字节),每次加密需生成新的IV
     * @return Base64编码后的IV字符串(随密文一起传输)
     */

    public static String generateIV() {
        byte[] iv = new byte[IV_SIZE];
        SecureRandom secureRandom = new SecureRandom();
        secureRandom.nextBytes(iv);
        return Base64.getEncoder().encodeToString(iv);
    }

    /**
     * AES加密(CBC模式+PKCS5Padding)
     * @param plainText 原始明文
     * @param aesKeyBase64 Base64编码后的AES密钥
     * @param ivBase64 Base64编码后的初始向量(IV)
     * @return Base64编码后的密文(方便存储和传输)
     */

    public static String encrypt(String plainText, String aesKeyBase64, String ivBase64) {
        if (plainText == null || plainText.isEmpty() || aesKeyBase64 == null || ivBase64 == null) {
            return null;
        }
        try {
            // 1. 解码Base64密钥,转换为SecretKeySpec
            byte[] keyBytes = Base64.getDecoder().decode(aesKeyBase64);
            SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
            // 2. 解码Base64 IV,转换为IvParameterSpec
            byte[] ivBytes = Base64.getDecoder().decode(ivBase64);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
            // 3. 获取AES加密实例,初始化加密模式
            Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
            // 4. 执行加密,得到二进制密文
            byte[] encryptBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
            // 5. 密文转换为Base64字符串,返回
            return Base64.getEncoder().encodeToString(encryptBytes);
        } catch (Exception e) {
            System.err.println("AES加密失败:" + e.getMessage());
            return null;
        }
    }

    /**
     * AES解密(CBC模式+PKCS5Padding),需与加密参数完全一致
     * @param cipherTextBase64 Base64编码后的密文
     * @param aesKeyBase64 Base64编码后的AES密钥
     * @param ivBase64 Base64编码后的初始向量(IV)
     * @return 解密后的原始明文
     */

    public static String decrypt(String cipherTextBase64, String aesKeyBase64, String ivBase64) {
        if (cipherTextBase64 == null || cipherTextBase64.isEmpty() || aesKeyBase64 == null || ivBase64 == null) {
            return null;
        }
        try {
            // 1. 解码Base64密钥和IV
            byte[] keyBytes = Base64.getDecoder().decode(aesKeyBase64);
            byte[] ivBytes = Base64.getDecoder().decode(ivBase64);
            SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
            // 2. 获取AES解密实例,初始化解密模式
            Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
            // 3. 解码Base64密文,执行解密
            byte[] cipherBytes = Base64.getDecoder().decode(cipherTextBase64);
            byte[] decryptBytes = cipher.doFinal(cipherBytes);
            // 4. 解密后的字节数组转换为字符串,返回
            return new String(decryptBytes, StandardCharsets.UTF_8);
        } catch (Exception e) {
            System.err.println("AES解密失败:" + e.getMessage());
            return null;
        }
    }

    // 测试方法(生产环境可删除)
    public static void main(String[] args) {
        // 1. 生成AES密钥(仅初始化时生成,存储在配置中心)
        String aesKey = generateAesKey();
        System.out.println("AES密钥(Base64):" + aesKey);

        // 2. 生成IV(每次加密生成新的IV)
        String iv = generateIV();
        System.out.println("初始向量IV(Base64):" + iv);

        // 3. 原始明文(敏感数据)
        String plainText = "用户身份证号:110101199001011234";

        // 4. 加密
        String cipherText = encrypt(plainText, aesKey, iv);
        System.out.println("AES加密后(Base64):" + cipherText);

        // 5. 解密
        String decryptText = decrypt(cipherText, aesKey, iv);
        System.out.println("AES解密后:" + decryptText);

        // 测试参数不一致导致解密失败(故意修改IV)
        String wrongIv = generateIV();
        String wrongDecrypt = decrypt(cipherText, aesKey, wrongIv);
        System.out.println("IV错误时解密结果:" + wrongDecrypt); // null
    }
}

Vue3 前端实现

说明:前端使用crypto-js第三方库实现AES加密(npm install crypto-js),与后端保持算法、模式、填充方式一致(CBC+PKCS5Padding);补充异常处理、参数校验,适配前端表单提交、接口请求场景;IV随机生成,随加密参数一起传输给后端。

/**
 * Vue3 AES 工具函数(生产版,CBC模式+PKCS5Padding,与后端一致)
 * 依赖:crypto-js(npm install crypto-js)
 * 适用场景:前端敏感参数加密(如登录密码、身份证号),避免明文传输
 */

import CryptoJS from 'crypto-js';

// 加密算法+模式+填充方式(与后端一致,固定)
const ALGORITHM_MODE_PADDING = CryptoJS.mode.CBC;
const PADDING = CryptoJS.pad.Pkcs7; // PKCS5Padding与PKCS7Padding在AES中兼容
const ALGORITHM = 'AES';

/**
 * 随机生成IV(16字节,与后端一致)
 * @returns {string} Base64编码后的IV字符串
 */

export const generateIV = () => {
  // 生成16字节随机数,转换为Base64
  const iv = CryptoJS.lib.WordArray.random(16);
  return CryptoJS.enc.Base64.stringify(iv);
};

/**
 * AES加密(CBC模式+PKCS5Padding)
 * @param {string} plainText - 原始明文
 * @param {string} aesKeyBase64 - Base64编码后的AES密钥(从后端获取,避免前端硬编码)
 * @param {string} ivBase64 - Base64编码后的IV
 * @returns {string} Base64编码后的密文
 */

export const aesEncrypt = (plainText, aesKeyBase64, ivBase64) => {
  if (!plainText || !aesKeyBase64 || !ivBase64) return '';
  try {
    // 解码Base64密钥和IV,转换为crypto-js可识别的格式
    const key = CryptoJS.enc.Base64.parse(aesKeyBase64);
    const iv = CryptoJS.enc.Base64.parse(ivBase64);
    // 执行加密,返回Base64密文
    const cipherText = CryptoJS.AES.encrypt(plainText, key, {
      mode: ALGORITHM_MODE_PADDING,
      padding: PADDING,
      iv: iv
    });
    return cipherText.toString();
  } catch (e) {
    console.error('AES加密失败:', e);
    return '';
  }
};

/**
 * AES解密(CBC模式+PKCS5Padding)
 * @param {string} cipherTextBase64 - Base64编码后的密文
 * @param {string} aesKeyBase64 - Base64编码后的AES密钥
 * @param {string} ivBase64 - Base64编码后的IV
 * @returns {string} 解密后的原始明文
 */

export const aesDecrypt = (cipherTextBase64, aesKeyBase64, ivBase64) => {
  if (!cipherTextBase64 || !aesKeyBase64 || !ivBase64) return '';
  try {
    const key = CryptoJS.enc.Base64.parse(aesKeyBase64);
    const iv = CryptoJS.enc.Base64.parse(ivBase64);
    // 执行解密,转换为字符串
    const decryptResult = CryptoJS.AES.decrypt(cipherTextBase64, key, {
      mode: ALGORITHM_MODE_PADDING,
      padding: PADDING,
      iv: iv
    });
    return decryptResult.toString(CryptoJS.enc.Utf8);
  } catch (e) {
    console.error('AES解密失败:', e);
    return '';
  }
};

// 测试(需安装crypto-js依赖)
// const aesKey = 'xxx'; // 从后端接口获取Base64编码的密钥
// const iv = generateIV();
// const plainText = '用户身份证号:110101199001011234';
// const cipherText = aesEncrypt(plainText, aesKey, iv);
// console.log('AES加密结果:', cipherText);
// const decryptText = aesDecrypt(cipherText, aesKey, iv);
// console.log('AES解密结果:', decryptText);

4.2 DES、3DES 加密(了解即可)

DES(Data Encryption Standard,数据加密标准)是传统的对称加密算法,密钥长度为56位,安全性较低(可被暴力破解),目前已被AES替代;3DES(Triple DES,三重DES)是DES的改进版,通过对明文进行三次DES加密,密钥长度为168位,安全性高于DES,但运算速度慢于AES,仅用于历史项目兼容,新项目不推荐使用。

核心知识点

  • DES:密钥长度56位,分组长度64位,加密模式与AES类似(ECB、CBC等),填充方式支持PKCS5Padding,安全性低,新项目禁止使用。
  • 3DES:密钥长度168位(3个56位密钥),运算过程为“加密-解密-加密”,安全性高于DES,但运算速度慢,仅用于历史项目迁移、兼容旧系统。
  • 避坑点:新项目优先使用AES,禁止使用DES;若需兼容旧系统使用3DES,需做好密钥管理,定期更换密钥。

五、非对称加密算法(可逆)

非对称加密算法的核心特点:生成一对密钥(公钥和私钥),公钥可公开(如发布到服务器、客户端),私钥需严格保密(仅存储在服务器端,禁止泄露);公钥加密的内容,仅对应私钥可解密;私钥加密的内容(签名),仅对应公钥可验证;安全性高,但运算速度慢(比对称加密慢100-1000倍),适合小数据加密、密钥交换、身份认证。

核心解决的问题:对称加密的密钥传输安全——通过非对称加密传输对称密钥,避免对称密钥明文传输导致的泄露;同时用于身份认证(如Token签名、接口签名),确保数据来源合法。

开发中最常用的非对称加密算法:RSA(应用最广泛)。

5.1 RSA 加密(首选)

5.1.2 核心特点与优缺点分析

核心特点

  • 密钥成对存在:公钥公开,私钥保密,二者一一对应,公钥加密的内容仅私钥可解密,私钥签名的内容仅公钥可验签。
  • 不可逆性(相对):仅知道公钥,无法推导出私钥,安全性高。
  • 加密效率低:运算过程复杂,速度慢,仅适合小数据加密(如密钥、Token),不适合大量数据加密。

优缺点分析

  • 优点:安全性高(2048位RSA目前无法被暴力破解)、无需担心密钥传输安全(公钥可公开)、适合身份认证和密钥交换,是安全通信的核心算法。
  • 缺点:运算速度慢(比AES慢100-1000倍)、密钥长度长(2048位公钥字符串较长)、加密数据长度有限(如2048位RSA最多加密245字节,超过需分段加密,效率更低)。

5.1.3 适用场景(核心重点)

  • 核心场景1:密钥交换(解决对称加密密钥传输安全)—— 客户端生成AES对称密钥,用服务器公钥加密AES密钥,传输给服务器;服务器用私钥解密,获取AES密钥,后续双方用AES加密传输大量数据(主流通信模式:RSA+AES混合加密)。
  • 核心场景2:身份认证(签名与验签)—— 1. 接口签名:服务器用私钥对接口参数(如时间戳、随机数、参数值)进行签名,客户端用服务器公钥验签,确保接口参数未被篡改、数据来源合法;2. Token签名(如JWT):服务器用私钥对JWT进行签名,客户端用公钥验签,确保Token未被篡改、是服务器颁发的。
  • 其他场景:小数据加密(如用户验证码、核心配置参数)、数字证书(如HTTPS证书,基于RSA签名,确保网站身份合法)、电子签名(如合同电子签名,基于RSA私钥签名,确保签名人身份合法)。

5.1.4 优化方案

  • 密钥管理(核心):1. 密钥长度:优先使用2048位,核心敏感场景使用4096位,禁止使用1024位;2. 私钥保密:私钥仅存储在服务器端(如配置中心、加密存储的数据库),禁止传输、禁止泄露,定期更换密钥(每6-12个月);3. 公钥发布:公钥可公开(如客户端配置、接口返回),无需保密,但需确保公钥来源合法(避免被篡改)。
  • 性能优化:1. 混合加密:用RSA加密对称密钥,用AES加密大量数据,兼顾安全性与效率(主流方案);2. 分段加密:若需加密的数据超过RSA最大加密长度(如2048位RSA最大245字节),需分段加密,避免加密失败;3. 签名优化:对参数进行哈希处理(如SHA-256)后再用私钥签名,减少数据量,提升签名效率。
  • 安全避坑:1. 禁止用RSA加密大量数据(效率极低);2. 禁止私钥硬编码、禁止私钥传输;3. 验签时需严格校验签名,避免验签失败仍处理数据;4. 密钥更换时,需同步更新公钥(如客户端配置、数字证书),避免验签、解密失败。

5.1.5 实战代码

Java 后端实现(原生API,无第三方依赖,2048位密钥,签名+加密)

说明:Java原生通过java.security包实现RSA密钥生成、加密、解密、签名、验签。

import javax.crypto.Cipher;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * RSA 加密工具类(生产版,2048位密钥,支持加密/解密、签名/验签)
 * 适用场景:密钥交换、接口签名、Token签名、小数据加密
 * 注意:私钥严格保密,禁止硬编码、禁止传输;公钥可公开,需确保来源合法
 */

public class RSAUtil {
    // 加密算法(固定)
    private static final String ALGORITHM = "RSA";
    // 密钥长度(2048位,常用,可改为4096位)
    private static final int KEY_SIZE = 2048;
    // 签名算法(SHA-256+RSA,先哈希再签名,提升效率和安全性)
    private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";

    /**
     * 生成RSA密钥对(公钥+私钥),仅初始化时生成,私钥存储在配置中心
     * @return 密钥对数组,index 0:公钥(Base64字符串),index 1:私钥(Base64字符串)
     */

    public static String[] generateKeyPair() {
        try {
            // 生成密钥对生成器
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);
            SecureRandom secureRandom = new SecureRandom();
            keyPairGenerator.initialize(KEY_SIZE, secureRandom);
            // 生成密钥对
            KeyPair keyPair = keyPairGenerator.generateKeyPair();
            PublicKey publicKey = keyPair.getPublic();
            PrivateKey privateKey = keyPair.getPrivate();
            // 密钥转换为Base64字符串,便于存储和传输
            String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKey.getEncoded());
            String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKey.getEncoded());
            return new String[]{publicKeyBase64, privateKeyBase64};
        } catch (NoSuchAlgorithmException e) {
            System.err.println("RSA密钥对生成失败:" + e.getMessage());
            throw new RuntimeException("RSA密钥生成异常", e);
        }
    }

    /**
     * Base64编码的公钥字符串,转换为PublicKey对象
     * @param publicKeyBase64 Base64编码的公钥
     * @return PublicKey对象
     */

    public static PublicKey getPublicKey(String publicKeyBase64) {
        try {
            byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
            return keyFactory.generatePublic(keySpec);
        } catch (Exception e) {
            System.err.println("RSA公钥转换失败:" + e.getMessage());
            return null;
        }
    }

    /**
     * Base64编码的私钥字符串,转换为PrivateKey对象
     * @param privateKeyBase64 Base64编码的私钥
     * @return PrivateKey对象
     */

    public static PrivateKey getPrivateKey(String privateKeyBase64) {
        try {
            byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64);
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
            return keyFactory.generatePrivate(keySpec);
        } catch (Exception e) {
            System.err.println("RSA私钥转换失败:" + e.getMessage());
            return null;
        }
    }

    /**
     * RSA加密(公钥加密,私钥解密),支持分段加密(处理超过最大加密长度的数据)
     * @param plainText 原始明文
     * @param publicKeyBase64 Base64编码的公钥
     * @return Base64编码后的密文
     */

    public static String encrypt(String plainText, String publicKeyBase64) {
        if (plainText == null || plainText.isEmpty() || publicKeyBase64 == null) {
            return null;
        }
        PublicKey publicKey = getPublicKey(publicKeyBase64);
        if (publicKey == null) {
            return null;
        }
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8);
            // 分段加密:2048位RSA最大加密长度为245字节,超过需分段
            int maxEncryptSize = KEY_SIZE / 8 - 11// 固定公式,11字节用于填充
            byte[] encryptBytes = segmentEncrypt(cipher, plainBytes, maxEncryptSize);
            // 密文转换为Base64字符串
            return Base64.getEncoder().encodeToString(encryptBytes);
        } catch (Exception e) {
            System.err.println("RSA加密失败:" + e.getMessage());
            return null;
        }
    }

    /**
     * RSA解密(私钥解密,公钥加密),支持分段解密
     * @param cipherTextBase64 Base64编码的密文
     * @param privateKeyBase64 Base64编码的私钥
     * @return 解密后的原始明文
     */

    public static String decrypt(String cipherTextBase64, String privateKeyBase64) {
        if (cipherTextBase64 == null || cipherTextBase64.isEmpty() || privateKeyBase64 == null) {
            return null;
        }
        PrivateKey privateKey = getPrivateKey(privateKeyBase64);
        if (privateKey == null) {
            return null;
        }
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            byte[] cipherBytes = Base64.getDecoder().decode(cipherTextBase64);
            // 分段解密:2048位RSA最大解密长度为256字节
            int maxDecryptSize = KEY_SIZE / 8;
            byte[] decryptBytes = segmentDecrypt(cipher, cipherBytes, maxDecryptSize);
            return new String(decryptBytes, StandardCharsets.UTF_8);
        } catch (Exception e) {
            System.err.println("RSA解密失败:" + e.getMessage());
            return null;
        }
    }

    /**
     * RSA签名(私钥签名,公钥验签),先对明文进行SHA-256哈希,再签名
     * @param plainText 原始明文(如接口参数、Token)
     * @param privateKeyBase64 Base64编码的私钥
     * @return Base64编码后的签名
     */

    public static String sign(String plainText, String privateKeyBase64) {
        if (plainText == null || plainText.isEmpty() || privateKeyBase64 == null) {
            return null;
        }
        PrivateKey privateKey = getPrivateKey(privateKeyBase64);
        if (privateKey == null) {
            return null;
        }
        try {
            Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
            signature.initSign(privateKey);
            // 对明文进行哈希处理,再签名
            signature.update(plainText.getBytes(StandardCharsets.UTF_8));
            byte[] signBytes = signature.sign();
            return Base64.getEncoder().encodeToString(signBytes);
        } catch (Exception e) {
            System.err.println("RSA签名失败:" + e.getMessage());
            return null;
        }
    }

    /**
     * RSA验签(公钥验签,私钥签名),验证签名是否合法、数据是否被篡改
     * @param plainText 原始明文
     * @param signBase64 Base64编码后的签名
     * @param publicKeyBase64 Base64编码的公钥
     * @return true:验签成功(数据合法、未被篡改),false:验签失败
     */

    public static boolean verifySign(String plainText, String signBase64, String publicKeyBase64) {
        if (plainText == null || plainText.isEmpty() || signBase64 == null || publicKeyBase64 == null) {
            return false;
        }
        PublicKey publicKey = getPublicKey(publicKeyBase64);
        if (publicKey == null) {
            return false;
        }
        try {
            Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
            signature.initVerify(publicKey);
            signature.update(plainText.getBytes(StandardCharsets.UTF_8));
            byte[] signBytes = Base64.getDecoder().decode(signBase64);
            return signature.verify(signBytes);
        } catch (Exception e) {
            System.err.println("RSA验签失败:" + e.getMessage());
            return false;
        }
    }

    /**
     * 分段加密(工具方法,私有)
     * @param cipher 加密实例
     * @param data 待加密数据
     * @param maxSize 每段最大加密长度
     * @return 加密后的总字节数组
     * @throws Exception 加密异常
     */

    private static byte[] segmentEncrypt(Cipher cipher, byte[] data, int maxSize) throws Exception {
        int dataLength = data.length;
        // 计算分段数量
        int segmentCount = (dataLength + maxSize - 1) / maxSize;
        byte[] result = new byte[0];
        for (int i = 0; i < segmentCount; i++) {
            // 计算每段的起始位置和长度
            int start = i * maxSize;
            int length = Math.min(dataLength - start, maxSize);
            byte[] segment = new byte[length];
            System.arraycopy(data, start, segment, 0, length);
            // 每段加密,拼接结果
            byte[] encryptSegment = cipher.doFinal(segment);
            byte[] newResult = new byte[result.length + encryptSegment.length];
            System.arraycopy(result, 0, newResult, 0, result.length);
            System.arraycopy(encryptSegment, 0, newResult, result.length, encryptSegment.length);
            result = newResult;
        }
        return result;
    }

    /**
     * 分段解密(工具方法,私有)
     * @param cipher 解密实例
     * @param data 待解密数据
     * @param maxSize 每段最大解密长度
     * @return 解密后的总字节数组
     * @throws Exception 解密异常
     */

    private static byte[] segmentDecrypt(Cipher cipher, byte[] data, int maxSize) throws Exception {
        int dataLength = data.length;
        int segmentCount = (dataLength + maxSize - 1) / maxSize;
        byte[] result = new byte[0];
        for (int i = 0; i < segmentCount; i++) {
            int start = i * maxSize;
            int length = Math.min(dataLength - start, maxSize);
            byte[] segment = new byte[length];
            System.arraycopy(data, start, segment, 0, length);
            byte[] decryptSegment = cipher.doFinal(segment);
            byte[] newResult = new byte[result.length + decryptSegment.length];
            System.arraycopy(result, 0, newResult, 0, result.length);
            System.arraycopy(decryptSegment, 0, newResult, result.length, decryptSegment.length);
            result = newResult;
        }
        return result;
    }

    // 测试方法(生产环境可删除)
    public static void main(String[] args) {
        // 1. 生成RSA密钥对(仅初始化时生成)
        String[] keyPair = generateKeyPair();
        String publicKey = keyPair[0];
        String privateKey = keyPair[1];
        System.out.println("RSA公钥(Base64):" + publicKey);
        System.out.println("RSA私钥(Base64):" + privateKey);

        // 2. 测试加密/解密(小数据)
        String plainText = "AES密钥:qingning12345678"// 待加密的AES密钥
        String cipherText = encrypt(plainText, publicKey);
        System.out.println("RSA加密后(Base64):" + cipherText);
        String decryptText = decrypt(cipherText, privateKey);
        System.out.println("RSA解密后:" + decryptText);

        // 3. 测试签名/验签
        String sign = sign(plainText, privateKey);
        System.out.println("RSA签名(Base64):" + sign);
        boolean verifyResult = verifySign(plainText, sign, publicKey);
        System.out.println("RSA验签结果(合法):" + verifyResult); // true

        // 测试验签失败(修改明文)
        String wrongPlainText = "AES密钥:qingning87654321";
        boolean wrongVerifyResult = verifySign(wrongPlainText, sign, publicKey);
        System.out.println("RSA验签结果(非法):" + wrongVerifyResult); // false
    }
}

Vue3 前端实现(依赖crypto-js,与后端一致)

说明:前端使用crypto-js第三方库实现RSA加密、验签(前端通常仅需加密、验签,私钥存储在后端,禁止前端存储私钥);与后端保持密钥长度、签名算法一致。

/**
 * Vue3 RSA 工具函数(生产版,2048位密钥,支持加密、验签)
 * 依赖:crypto-js(npm install crypto-js)、crypto-js-rsa(npm install crypto-js-rsa)
 * 注意:前端仅存储公钥,禁止存储私钥;仅用于加密、验签,不涉及解密(解密在后端)
 */

import CryptoJS from 'crypto-js';
import { RSAKey } from 'crypto-js-rsa';

// 密钥长度(与后端一致,2048位)
const KEY_SIZE = 2048;
// 签名算法(与后端一致,SHA256withRSA)
const SIGNATURE_ALGORITHM = 'SHA256withRSA';

/**
 * RSA加密(公钥加密,后端私钥解密)
 * @param {string} plainText - 原始明文(如AES密钥、敏感小数据)
 * @param {string} publicKeyBase64 - Base64编码的RSA公钥(从后端获取)
 * @returns {string} Base64编码后的密文
 */

export const rsaEncrypt = (plainText, publicKeyBase64) => {
  if (!plainText || !publicKeyBase64) return '';
  try {
    // 初始化RSA公钥
    const rsaKey = new RSAKey();
    rsaKey.setPublicString(publicKeyBase64);
    // 加密,返回Base64密文(分段加密由crypto-js-rsa自动处理)
    const cipherText = rsaKey.encrypt(plainText, 'utf8');
    return cipherText;
  } catch (e) {
    console.error('RSA加密失败:', e);
    return '';
  }
};

/**
 * RSA验签(公钥验签,后端私钥签名)
 * @param {string} plainText - 原始明文(如接口参数、Token)
 * @param {string} signBase64 - Base64编码后的签名(后端返回)
 * @param {string} publicKeyBase64 - Base64编码的RSA公钥
 * @returns {boolean} true:验签成功,false:验签失败
 */

export const rsaVerifySign = (plainText, signBase64, publicKeyBase64) => {
  if (!plainText || !signBase64 || !publicKeyBase64) return false;
  try {
    const rsaKey = new RSAKey();
    rsaKey.setPublicString(publicKeyBase64);
    // 验签:先对明文进行SHA256哈希,再验证签名
    const hash = CryptoJS.SHA256(plainText).toString(CryptoJS.enc.Hex);
    return rsaKey.verify(hash, signBase64);
  } catch (e) {
    console.error('RSA验签失败:', e);
    return false;
  }
};

// 测试(需安装crypto-js、crypto-js-rsa依赖)
// const publicKey = 'xxx'; // 从后端接口获取Base64编码的公钥
// const plainText = 'AES密钥:qingning12345678';
// const cipherText = rsaEncrypt(plainText, publicKey);
// console.log('RSA加密结果:', cipherText);
//
// // 验签测试(signBase64从后端获取)
// const signBase64 = 'xxx';
// const verifyResult = rsaVerifySign(plainText, signBase64, publicKey);
// console.log('RSA验签结果:', verifyResult);
posted @ 2026-03-26 10:46  青柠代码录  阅读(78)  评论(0)    收藏  举报