【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(8, 24);
}
/**
* 字节数组转换为十六进制字符串(工具方法,私有)
* @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);

浙公网安备 33010602011771号