RSA加密从原理到实践:Java后端与Vue前端全栈案例解析
一、RSA 加密核心原理与应用场景
(一)非对称加密体系核心概念
在当今数字化时代,信息安全至关重要,数据在传输和存储过程中面临诸多风险,如被窃取、篡改或伪造。加密技术作为保障信息安全的关键手段,可将原始数据转化为密文,只有授权方拥有特定密钥才能还原数据,从而有效防止数据泄露和恶意篡改。
加密技术主要分为对称加密和非对称加密两大体系。对称加密如 AES、DES 等算法,加密和解密使用相同密钥,其优势是加密速度快,适合处理大量数据,但密钥管理存在难题,在不安全环境中分发密钥时,一旦泄露,数据安全将受严重威胁。例如,在企业内部通信中,若使用对称加密,需通过安全渠道(如专人送达、加密邮件等)将密钥传递给接收方,这增加了管理复杂性和安全风险。
RSA 作为非对称加密的典型代表,巧妙解决了对称加密中密钥配送的安全隐患。它通过公钥加密、私钥解密的机制实现安全通信。在 RSA 加密体系中,公钥可公开分发,任何人都能获取并用于加密数据;私钥则由接收方妥善保管,只有持有私钥的人才能解密数据。这种特性使得数据在传输过程中无需传输私钥,降低了密钥泄露风险。例如,用户在网上购物时,向电商平台提交支付信息,电商平台会将公钥发送给用户,用户使用公钥对支付信息加密后传输,即使传输过程中信息被截获,由于没有私钥,攻击者也无法解密获取真实支付信息。
RSA 加密广泛应用于多个领域。在数据传输加密方面,如 HTTPS 协议中,RSA 用于保护数据在网络传输过程中的安全性,确保数据不被窃取或篡改,让用户能够安全地访问网页,进行购物、支付、登录等操作。在数字签名领域,RSA 可验证数据的完整性和来源真实性,防止数据被篡改或伪造。例如,软件开发者使用私钥对软件进行签名,用户下载软件后,可使用开发者的公钥验证签名,确认软件是否被篡改。在身份认证场景中,RSA 可用于验证用户身份,确保只有合法用户才能访问特定资源。比如,银行系统中,用户使用 RSA 密钥对进行身份认证,保证用户账户安全。在用户登录、支付信息传输等场景中,RSA 更是发挥着关键作用,为用户的隐私和财产安全提供坚实保障。
(二)数学基础与密钥生成原理
RSA 加密算法基于数论,涉及多个重要数学概念和复杂运算,其安全性依赖于大整数分解的困难性。
- 质数选择与模数计算:在 RSA 加密中,首先要选择两个大质数\(p\)和\(q\),这两个质数越大,加密安全性越高。在实际生产环境中,为抵御当前计算能力下的破解风险,通常使用 2048 位或 4096 位密钥,对应的质数\(p\)和\(q\)也非常大。选择好\(p\)和\(q\)后,计算模数\(n = p * q\)。\(n\)的长度在 RSA 加密中至关重要,它直接决定了加密的安全性。因为 RSA 加密的安全性基于对\(n\)进行因式分解的困难性,\(n\)越大,分解难度呈指数级增长,使得攻击者难以通过分解\(n\)来获取\(p\)和\(q\),从而保障了加密的安全性。例如,若\(p\)和\(q\)是两个 1024 位的大质数,它们的乘积\(n\)就是一个 2048 位的大数,对这样的大数进行因式分解,在当前计算能力下几乎是不可能的。
- 欧拉函数与互质校验:计算欧拉函数\(\varphi(n)\)是 RSA 密钥生成的关键步骤。根据欧拉函数的定义,对于两个质数\(p\)和\(q\),\(\varphi(n) = (p - 1)(q - 1)\)。欧拉函数\(\varphi(n)\)表示小于\(n\)且与\(n\)互质的正整数的个数,它在 RSA 算法中用于确定公钥和私钥的指数。在选择公钥\(e\)时,需要满足\(1 < e < \varphi(n)\)且\(gcd(e, \varphi(n)) = 1\),即\(e\)与\(\varphi(n)\)互质。\(gcd(e, \varphi(n))\)表示\(e\)和\(\varphi(n)\)的最大公约数,只有当最大公约数为 1 时,\(e\)和\(\varphi(n)\)才互质。例如,若\(\varphi(n) = 100\),则\(e\)不能选择 5、10 等与 100 有除 1 以外公约数的数,而可以选择 3、7 等与 100 互质的数。选择一个合适的公钥\(e\)对于加密的安全性和效率都非常重要,在实际应用中,通常会选择一个较小的质数作为\(e\),如 65537,这样可以在保证安全性的同时提高加密效率。
- 私钥推导与模反元素:通过扩展欧几里得算法求解私钥\(d\),满足\(ed \equiv 1 \ mod \ \varphi(n)\),即\(ed\)除以\(\varphi(n)\)的余数为 1,\(d\)就是\(e\)在模\(\varphi(n)\)下的乘法逆元。扩展欧几里得算法不仅能求出两个整数\(a\)和\(b\)的最大公约数\(d\),还能找到整数\(x\)和\(y\),使得\(ax + by = d\)。在 RSA 中,将\(a\)设为\(e\),\(b\)设为\(\varphi(n)\),通过扩展欧几里得算法求出\(x\)(即私钥\(d\))和\(y\),满足\(ed - k\varphi(n) = 1\)(\(k\)为某个整数)。例如,已知\(e = 7\),\(\varphi(n) = 10\),通过扩展欧几里得算法可求出\(d = 3\),因为\(7 * 3 = 21\),\(21\)除以\(10\)的余数为\(1\),满足\(ed \equiv 1 \ mod \ \varphi(n)\)。生成的公钥为\((n, e)\),私钥为\((n, d)\),在加密和解密过程中,公钥用于加密,私钥用于解密,二者相互配合,实现数据的安全传输和存储。
(三)加密解密核心公式
RSA 加密和解密过程基于特定数学公式,这些公式是 RSA 算法的核心,理解它们对于掌握 RSA 加密原理至关重要。
- 加密过程:加密时,使用公钥\((n, e)\)对明文\(m\)进行加密,加密公式为\(c = m^e \ mod \ n\),其中\(c\)为密文。在实际应用中,明文\(m\)需转换为小于\(n\)的整数。例如,若公钥\((n, e) = (1001, 7)\),明文\(m = 10\),则密文\(c = 10^7 \ mod \ 1001\)。计算\(10^7 = 10000000\),\(10000000\)除以\(1001\)的余数为\(990\),所以密文\(c = 990\)。这个过程通过将明文\(m\)进行\(e\)次幂运算后再对\(n\)取模,得到密文\(c\),实现了明文的加密。由于\(n\)通常是一个非常大的数,对\(m^e\)进行取模运算可以将结果限制在一个合理范围内,便于存储和传输,同时也增加了破解的难度。
- 解密过程:解密时,使用私钥\((n, d)\)对密文\(c\)进行解密,解密公式为\(m = c^d \ mod \ n\)。例如,若私钥\((n, d) = (1001, 287)\),密文\(c = 990\),则明文\(m = 990^{287} \ mod \ 1001\)。通过计算\(990^{287}\)并对\(1001\)取模,可得到明文\(m = 10\),成功还原出原始明文。解密过程是加密过程的逆运算,通过私钥\(d\)对密文\(c\)进行\(d\)次幂运算后再对\(n\)取模,恢复出原始明文\(m\)。
在实际应用中,由于 RSA 算法对输入长度有限制,当处理大整数运算和较长数据时,需要进行数据分段处理。例如,若要加密一篇很长的文档,不能直接将整个文档作为明文进行加密,而是需要将文档分成多个小块,每个小块分别进行加密,然后再将加密后的小块组合起来。在解密时,也需要按照相应的顺序对每个小块进行解密,最后将解密后的小块合并成原始文档。这样可以确保数据在符合 RSA 算法输入长度限制的前提下,实现安全的加密和解密。
二、前端 Vue 项目 RSA 加密实战(基于 jsencrypt 库)
(一)开发环境准备
- 依赖安装与工程配置:在 Vue 项目中,为实现 RSA 加密功能,需借助jsencrypt库进行基础的 RSA 加密操作,同时引入encryptlong库解决长文本分段加密问题。使用 npm 包管理器进行安装,在项目根目录下的终端中执行以下命令:
npm install jsencrypt encryptlong --save
安装完成后,这些库将被添加到项目的node_modules目录中,并在package.json文件中记录依赖信息。这两个库的引入为前端 RSA 加密提供了强大的支持,jsencrypt库基于 JavaScript 实现了 RSA 加密算法,可方便地进行公钥加密和私钥解密操作;encryptlong库则在jsencrypt库的基础上进行扩展,专门用于处理长文本的分段加解密,确保在加密较长数据时也能高效、安全地进行。
2. 全局工具类封装:为方便在整个项目中使用 RSA 加密功能,在src/utils/rsa.js文件中创建加密工具类,封装相关加密方法。以下是具体实现代码:
import JSEncrypt from 'jsencrypt';
import EncryptLong from 'encryptlong';
// 创建一个JSEncrypt实例,用于基本的RSA加密操作
const encryptor = new JSEncrypt();
// 创建一个EncryptLong实例,用于处理长文本的RSA加密
const longEncryptor = new EncryptLong();
// 设置公钥的方法,接收一个公钥字符串作为参数
export function setPublicKey(publicKey) {
encryptor.setPublicKey(publicKey);
longEncryptor.setPublicKey(publicKey);
}
// 加密普通数据的方法,接收要加密的数据作为参数
export function encryptData(data) {
return encryptor.encrypt(data);
}
// 加密长文本数据的方法,接收要加密的长文本数据作为参数
export function encryptLongData(data) {
return longEncryptor.encryptLong(data);
}
在上述代码中,首先导入jsencrypt和encryptlong库。然后创建JSEncrypt和EncryptLong实例,分别用于常规加密和长文本加密。setPublicKey方法用于设置公钥,该公钥将用于后续的加密操作,通过调用encryptor.setPublicKey和longEncryptor.setPublicKey方法,将传入的公钥设置到相应的加密实例中。encryptData方法利用encryptor实例对普通数据进行加密,直接调用encryptor.encrypt方法即可完成加密操作并返回加密后的结果。encryptLongData方法则使用longEncryptor实例对长文本数据进行加密,调用longEncryptor.encryptLong方法实现长文本的分段加密,并返回加密后的结果。这样,在项目的其他部分,只需导入rsa.js并调用相应方法,即可轻松实现 RSA 加密功能,大大提高了代码的复用性和可维护性。
(二)登录场景加密实现
- 组件集成与公钥获取:在登录组件中,实现 RSA 加密的第一步是获取后端生成的公钥,以便后续使用公钥对用户输入的密码进行加密。通过发送 HTTP 请求到后端提供的接口来获取公钥,假设后端接口为/api/getPublicKey。在 Vue 组件的created钩子函数中发送请求获取公钥,并使用setPublicKey方法将获取到的公钥设置到加密工具类中,具体代码如下:
<template>
<div>
<h2>登录</h2>
<form @submit.prevent="handleSubmit">
<label for="username">用户名:</label>
<input type="text" id="username" v-model="username" required>
<br>
<label for="password">密码:</label>
<input type="password" id="password" v-model="password" required>
<br>
<button type="submit">登录</button>
</form>
</div>
</template>
<script>
import { setPublicKey, encryptData } from '@/utils/rsa';
export default {
data() {
return {
username: '',
password: '',
publicKey: ''
};
},
created() {
this.fetchPublicKey();
},
methods: {
async fetchPublicKey() {
try {
const response = await fetch('/api/getPublicKey');
const data = await response.json();
this.publicKey = data.publicKey;
setPublicKey(this.publicKey);
} catch (error) {
console.error('获取公钥失败:', error);
}
},
handleSubmit() {
// 后续处理登录逻辑,包括密码加密和提交表单等
}
}
};
</script>
在上述代码中,首先在组件的data函数中定义了username、password和publicKey三个数据属性,分别用于存储用户输入的用户名、密码和从后端获取的公钥。在created钩子函数中调用fetchPublicKey方法,该方法通过fetch函数发送 HTTP 请求到/api/getPublicKey接口获取公钥。如果请求成功,将返回的公钥存储到publicKey属性中,并调用setPublicKey方法将公钥设置到加密工具类中,以便后续使用。如果请求过程中出现错误,将在控制台打印错误信息。
2. 密码加密与参数处理:获取公钥并设置到加密工具类后,在用户点击登录按钮时,对用户输入的密码进行加密处理。使用encryptData方法对密码进行加密,由于加密后的结果是 Base64 编码的字符串,在某些情况下可能需要对特殊字符进行处理,以确保数据在传输过程中的完整性。例如,在将加密后的密码作为参数发送到后端时,可能需要对其进行 URL 编码,防止特殊字符导致参数解析错误。以下是处理密码加密和参数处理的代码:
handleSubmit() {
const encryptedPassword = encryptData(this.password);
// 对加密后的密码进行URL编码,处理特殊字符
const encodedPassword = encodeURIComponent(encryptedPassword);
// 构造登录请求参数,将用户名和加密后的密码作为参数
const loginData = {
username: this.username,
password: encodedPassword
};
// 发送登录请求到后端
fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(loginData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('登录成功');
// 处理登录成功后的逻辑,如跳转到首页等
} else {
console.error('登录失败:', data.message);
}
})
.catch(error => {
console.error('登录请求失败:', error);
});
}
在上述代码中,handleSubmit方法首先调用encryptData方法对用户输入的密码进行加密,得到加密后的密码encryptedPassword。然后使用encodeURIComponent函数对加密后的密码进行 URL 编码,将特殊字符转换为适合在 URL 中传输的格式,得到encodedPassword。接着构造loginData对象,包含用户名和编码后的加密密码。最后通过fetch函数发送 POST 请求到/api/login接口,将loginData作为请求体发送到后端。在请求成功后,根据后端返回的数据判断登录是否成功,如果成功则进行相应的处理,如跳转到首页;如果失败则在控制台打印错误信息。如果请求过程中出现错误,也将在控制台打印错误信息。通过这样的处理,确保了用户密码在传输过程中的安全性,防止密码明文泄露,提高了系统的安全性。
(三)长文本加密方案(文件传输场景)
- 分段加密原理:在进行 RSA 加密时,单次加密的长度受到密钥长度的限制。以常见的 1024 位密钥为例,最多只能加密 117 字节的数据。这是因为 RSA 加密基于数学运算,在进行加密时,需要将明文转换为一个大整数,然后使用公钥对这个大整数进行运算得到密文。由于密钥长度决定了运算的范围,当明文长度超过一定限制时,就无法直接进行加密。例如,对于 1024 位密钥,其对应的模数\(n\)是一个 1024 位的大整数,在进行加密运算\(c = m^e \ mod \ n\)(其中\(c\)为密文,\(m\)为明文,\(e\)为公钥指数)时,如果明文\(m\)对应的大整数超过了一定范围,就会导致运算错误。
当需要加密长文本数据时,如文件传输场景中,文件内容可能包含大量字节,远远超过 117 字节的限制。此时,就需要将长文本分割为多个符合加密长度限制的块,然后对每个块进行单独加密,最后将加密后的块拼接起来,形成完整的密文。在分割长文本时,需要确保每个块的长度小于或等于 117 字节,同时要考虑到文本的完整性,避免在分割时破坏文本的语义。在拼接加密后的块时,也要按照正确的顺序进行拼接,以保证解密时能够还原出原始的长文本。例如,假设有一个长度为 500 字节的长文本,需要将其分割为 5 个块,每个块长度为 100 字节(小于 117 字节),分别对这 5 个块进行加密,得到 5 个加密后的块,然后将这 5 个块按照顺序拼接起来,形成最终的密文。
2. encryptlong 库实战应用:encryptlong库为解决长文本加密问题提供了便捷的方案,它能够自动处理长文本的分段逻辑,确保数据在加密和解密过程中的完整性和安全性。在实际应用中,只需调用encryptLongData方法即可完成长文本的加密操作。假设要加密一个包含文件内容的长字符串fileContent,以下是使用encryptlong库进行加密的代码示例:
import { encryptLongData } from '@/utils/rsa';
// 假设fileContent是包含文件内容的长字符串
const fileContent = '这里是文件的具体内容,可能非常长...';
const encryptedFileContent = encryptLongData(fileContent);
console.log('加密后的文件内容:', encryptedFileContent);
在上述代码中,首先导入encryptLongData方法,该方法封装在@/utils/rsa.js文件中。然后定义一个包含文件内容的长字符串fileContent,模拟要加密的文件内容。接着调用encryptLongData方法对fileContent进行加密,得到加密后的文件内容encryptedFileContent。最后在控制台打印加密后的文件内容,以便查看加密结果。encryptlong库内部会自动根据 RSA 加密的长度限制,将fileContent分割为合适的块进行加密,并在解密时按照相应的规则进行分段解密,还原出原始的文件内容,大大简化了长文本加密的实现过程,提高了开发效率和数据的安全性。
三、后端 Java 项目 RSA 解密实战(原生实现与 Hutool 工具对比)
(一)Java 原生 RSA 工具类开发
- 密钥对生成与格式处理:在 Java 中,使用KeyPairGenerator类生成 RSA 密钥对。为了生成符合 PKCS#8 格式的私钥和 X.509 格式的公钥,需要进行一系列的转换和处理。以下是生成密钥对并进行格式处理的代码示例:
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RSAUtil {
public static String[] generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
// 将公钥转换为X.509格式的Base64编码字符串
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKey.getEncoded());
String publicKeyBase64 = Base64.getEncoder().encodeToString(x509EncodedKeySpec.getEncoded());
// 将私钥转换为PKCS#8格式的Base64编码字符串
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());
String privateKeyBase64 = Base64.getEncoder().encodeToString(pkcs8EncodedKeySpec.getEncoded());
return new String[]{publicKeyBase64, privateKeyBase64};
}
}
在上述代码中,首先通过KeyPairGenerator.getInstance("RSA")获取 RSA 密钥对生成器实例,然后调用initialize(2048)方法初始化密钥对生成器,指定密钥长度为 2048 位,这是目前 RSA 加密中常用的密钥长度,能够提供较高的安全性。接着调用generateKeyPair()方法生成密钥对keyPair,其中包含公钥和私钥。
为了将公钥转换为 X.509 格式,创建X509EncodedKeySpec对象,传入公钥的字节数组publicKey.getEncoded(),然后使用Base64.getEncoder().encodeToString(x509EncodedKeySpec.getEncoded())将其转换为 Base64 编码的字符串,得到 X.509 格式的公钥。
对于私钥,创建PKCS8EncodedKeySpec对象,传入私钥的字节数组privateKey.getEncoded(),同样使用 Base64 编码将其转换为字符串,得到 PKCS#8 格式的私钥。最后,将生成的公钥和私钥以字符串数组的形式返回,方便后续使用。
2. 密文解密核心逻辑:在 Java 中,使用Cipher类进行 RSA 解密操作。由于 RSA 加密对数据长度有限制,当密文长度超过一定范围时,需要进行分段解密。以下是实现分段解密的核心代码:
import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class RSAUtil {
// 省略密钥对生成方法
public static String decrypt(String encryptedData, String privateKeyBase64) throws Exception {
byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
int inputLen = encryptedBytes.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
while (inputLen - offSet > 0) {
if (inputLen - offSet > 256) {
cache = cipher.doFinal(encryptedBytes, offSet, 256);
} else {
cache = cipher.doFinal(encryptedBytes, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * 256;
}
byte[] decryptedBytes = out.toByteArray();
out.close();
return new String(decryptedBytes, "UTF-8");
}
}
在上述代码中,首先将 Base64 编码的私钥字符串privateKeyBase64解码为字节数组privateKeyBytes,然后创建PKCS8EncodedKeySpec对象,传入私钥字节数组,通过KeyFactory生成私钥对象privateKey。
接着获取Cipher实例,指定加密模式为RSA/ECB/PKCS1Padding,这是 RSA 加密中常用的模式和填充方式。ECB(电子密码本模式)是一种简单的加密模式,它将明文分成固定长度的块,然后对每个块进行独立加密;PKCS1Padding是一种填充方式,用于在明文长度不足时进行填充,确保每个块都能被正确加密。
将 Base64 编码的密文encryptedData解码为字节数组encryptedBytes,然后进行分段解密。定义inputLen为密文的长度,offSet为偏移量,初始值为 0。在循环中,根据密文剩余长度判断是否需要分段处理。如果剩余长度大于 256 字节(这是根据 RSA 加密的特性和实际测试得出的经验值,不同的密钥长度和加密模式可能会有所不同),则每次处理 256 字节;否则处理剩余的字节。通过cipher.doFinal方法进行解密,将解密后的字节写入ByteArrayOutputStream中。最后,将ByteArrayOutputStream中的数据转换为字节数组,并使用UTF-8编码将其转换为字符串返回,得到解密后的明文。
(二)Hutool 工具类简化实现
- 依赖引入与快速初始化:Hutool 是一个功能强大的 Java 工具库,其中的 RSA 工具类极大地简化了 RSA 加密和解密的操作。首先在pom.xml文件中添加 Hutool 的依赖:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
添加依赖后,使用 Hutool 的 RSA 工具类进行解密操作只需几行代码。以下是使用 Hutool 进行解密的示例:
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
public class HutoolRSAUtil {
public static String decrypt(String encryptedData, String privateKeyBase64) {
RSA rsa = new RSA(privateKeyBase64, null);
return rsa.decryptStr(encryptedData, KeyType.PrivateKey);
}
}
在上述代码中,首先创建RSA对象,传入 Base64 编码的私钥字符串privateKeyBase64,公钥参数为null,因为这里只进行解密操作,不需要公钥。然后调用rsa.decryptStr方法,传入加密数据encryptedData和密钥类型KeyType.PrivateKey,表示使用私钥进行解密。该方法会自动处理解密过程中的各种细节,包括密钥解析、解密操作和结果转换,返回解密后的字符串,大大简化了代码的编写。
2. 密钥对生成优化:Hutool 的 RSA 工具类同样简化了密钥对的生成过程。以下是使用 Hutool 生成 RSA 密钥对的示例:
import cn.hutool.crypto.asymmetric.RSA;
public class HutoolRSAUtil {
// 省略解密方法
public static String[] generateKeyPair() {
RSA rsa = new RSA();
return new String[]{rsa.getPublicKeyBase64(), rsa.getPrivateKeyBase64()};
}
}
在上述代码中,创建RSA对象rsa,该对象在创建时会自动生成 RSA 密钥对。然后通过rsa.getPublicKeyBase64()和rsa.getPrivateKeyBase64()方法分别获取 Base64 编码的公钥和私钥字符串,最后将它们以字符串数组的形式返回。与 Java 原生实现相比,Hutool 的密钥对生成方法更加简洁,无需手动处理密钥格式转换等复杂操作,提高了开发效率。
(三)接口设计与安全增强
- 公钥获取接口:为了让前端能够获取公钥进行加密操作,需要在后端提供一个公钥获取接口。使用 Spring Boot 框架创建一个简单的 RESTful 接口:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.NoSuchAlgorithmException;
@RestController
public class RSAController {
@GetMapping("/api/getPublicKey")
public String getPublicKey() throws NoSuchAlgorithmException {
// 这里可以使用Java原生方法或Hutool工具生成公钥
// 为了示例简单,这里假设已经有生成好的公钥
String[] keyPair = RSAUtil.generateKeyPair();
return "{\"publicKey\":\"" + keyPair[0] + "\"}";
}
}
在上述代码中,使用@RestController注解将该类标记为一个 RESTful 风格的控制器。@GetMapping("/api/getPublicKey")注解表示该方法处理 HTTP GET 请求,路径为/api/getPublicKey。在方法内部,调用RSAUtil.generateKeyPair()方法生成 RSA 密钥对(这里的RSAUtil可以是前面实现的 Java 原生工具类或 Hutool 工具类),然后返回包含公钥的 JSON 字符串。在实际应用中,为了提高性能,可以考虑对生成的公钥进行缓存,避免每次请求都重新生成。同时,为了确保接口的安全性,需要对请求进行身份验证和授权,防止非法获取公钥。例如,可以使用 Spring Security 框架进行身份验证和授权,只有经过认证的用户才能访问该接口。
2. 解密接口防重放攻击:在设计解密接口时,为了防止重放攻击,需要添加时间戳校验和 Nonce 机制。以下是一个示例:
import cn.hutool.core.util.StrUtil;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@RestController
public class DecryptionController {
private static final Map<String, Boolean> nonceMap = new ConcurrentHashMap<>();
private static final long TIMESTAMP_EXPIRATION = 60 * 1000; // 时间戳有效期为60秒
@PostMapping("/api/decrypt")
public String decrypt(@RequestBody Map<String, Object> request) {
String encryptedData = (String) request.get("encryptedData");
String timestamp = (String) request.get("timestamp");
String nonce = (String) request.get("nonce");
// 校验时间戳
if (StrUtil.isBlank(timestamp) || System.currentTimeMillis() - Long.parseLong(timestamp) > TIMESTAMP_EXPIRATION) {
return "时间戳无效或已过期";
}
// 校验Nonce
if (StrUtil.isBlank(nonce) || nonceMap.containsKey(nonce)) {
return "Nonce无效或已使用";
}
nonceMap.put(nonce, true);
// 假设已经有私钥
String privateKeyBase64 = "这里是私钥";
try {
// 使用Java原生或Hutool工具进行解密
String decryptedData = RSAUtil.decrypt(encryptedData, privateKeyBase64);
return "解密成功: " + decryptedData;
} catch (Exception e) {
return "解密失败: " + e.getMessage();
}
}
}
在上述代码中,首先定义了一个nonceMap用于存储已经使用过的 Nonce 值,防止重复使用。TIMESTAMP_EXPIRATION定义了时间戳的有效期为 60 秒。在decrypt方法中,从请求体中获取加密数据encryptedData、时间戳timestamp和 Noncenonce。首先校验时间戳,如果时间戳为空或当前时间与时间戳的差值超过有效期,则返回时间戳无效或已过期的提示。然后校验 Nonce,如果 Nonce 为空或已经在nonceMap中存在,则返回 Nonce 无效或已使用的提示。如果时间戳和 Nonce 校验通过,则将 Nonce 添加到nonceMap中,表示该 Nonce 已使用。最后,使用私钥对加密数据进行解密,并返回解密结果。如果解密过程中出现异常,则返回解密失败的提示。通过这种方式,可以有效防止重放攻击,确保解密接口的安全性。
四、AES+RSA 混合加密方案(大规模数据安全传输)
(一)混合加密优势分析
在实际应用中,数据安全至关重要,尤其是在大规模数据传输场景下,单一的加密算法往往难以满足高效性与安全性的双重需求。RSA 加密算法虽在安全性方面表现出色,但其加密速度较慢,单次加密数据量也较小。例如,在传输一个大小为 10MB 的文件时,若直接使用 RSA 加密,由于其加密速度慢,会导致传输时间大幅延长,严重影响用户体验。这是因为 RSA 加密基于复杂的数论运算,在进行加密时,需要对大整数进行幂运算和模运算,这些运算计算量庞大,耗时较长。同时,RSA 对单次加密的数据长度有限制,对于较长的数据,需要进行分段处理,进一步增加了加密的复杂性和时间成本。因此,RSA 更适合用于传输对称密钥,因为对称密钥通常长度较短,使用 RSA 加密对称密钥既能保证密钥传输的安全性,又不会因加密速度慢和数据量限制而产生较大影响。
AES 加密算法则具有加密速度快的显著优势,能够高效处理大数据量。例如,在处理上述 10MB 的文件时,AES 加密可以在较短时间内完成加密操作,大大提高了数据传输效率。这是因为 AES 采用分组加密方式,将数据分成固定长度的块进行加密,算法结构相对简单,计算量较小,所以加密速度快。然而,AES 作为对称加密算法,存在密钥配送问题。在不安全的网络环境中,直接传输 AES 密钥极易被窃取,一旦密钥泄露,数据的安全性将荡然无存。例如,在公共 Wi-Fi 环境下,攻击者可能通过网络嗅探等手段获取传输中的 AES 密钥,从而轻松解密使用该密钥加密的数据。
为了充分发挥两种算法的优势,兼顾效率与安全性,AES+RSA 混合加密方案应运而生。在该方案中,前端首先生成 AES 密钥,利用 AES 算法的高速特性对大规模的正文数据进行加密。然后,使用后端提供的 RSA 公钥对生成的 AES 密钥进行加密。这样,在传输过程中,传输的是加密后的 AES 密钥和 AES 加密后的正文数据。后端接收到数据后,使用 RSA 私钥解密得到 AES 密钥,再用 AES 密钥解密正文数据。通过这种方式,既利用了 AES 加密速度快的优势处理大量数据,又借助 RSA 解决了 AES 密钥配送的安全问题,实现了数据在传输过程中的高效与安全。
(二)前端实现步骤
- 生成 AES 密钥并加密:在前端,使用crypto - js库生成 AES 密钥。该库提供了丰富的加密功能,能够方便地生成符合安全标准的随机密钥。以下是生成 AES 密钥并使用 RSA 公钥加密的代码示例:
import CryptoJS from 'crypto - js';
import { setPublicKey, encryptData } from '@/utils/rsa';
// 生成AES密钥
const aesKey = CryptoJS.lib.WordArray.random(32);
// 将AES密钥转换为Base64编码的字符串,以便于传输和存储
const aesKeyBase64 = CryptoJS.enc.Base64.stringify(aesKey);
// 获取RSA公钥,假设已经从后端获取到公钥并设置到加密工具类中
// 这里模拟已经设置好公钥的情况
// 在实际应用中,需要通过接口请求从后端获取公钥并调用setPublicKey方法设置
setPublicKey('这里是从后端获取的公钥');
// 使用RSA公钥加密AES密钥
const encryptedAESKey = encryptData(aesKeyBase64);
在上述代码中,首先通过CryptoJS.lib.WordArray.random(32)生成一个 32 字节(256 位)的 AES 密钥aesKey,这是 AES - 256 加密中常用的密钥长度,能够提供较高的安全性。然后使用CryptoJS.enc.Base64.stringify(aesKey)将 AES 密钥转换为 Base64 编码的字符串aesKeyBase64,Base64 编码可以将二进制数据转换为文本格式,便于在网络中传输和在文本环境中存储。接下来,假设已经从后端获取到 RSA 公钥并通过setPublicKey方法设置到加密工具类中(在实际应用中,需要通过发送 HTTP 请求到后端提供的接口获取公钥,并调用setPublicKey方法进行设置),然后调用encryptData方法使用 RSA 公钥对 AES 密钥进行加密,得到加密后的 AES 密钥encryptedAESKey。
2. 数据封装与传输:使用生成的 AES 密钥对正文数据进行加密,这里假设正文数据是一个字符串plainText。加密完成后,将加密后的 AES 密钥和正文数据一同发送至后端。以下是数据封装与传输的代码示例:
// 假设plainText是要传输的正文数据
const plainText = '这里是要传输的正文数据';
// 使用AES密钥加密正文数据
const encryptedText = CryptoJS.AES.encrypt(plainText, aesKey).toString();
// 构造要发送到后端的数据对象
const dataToSend = {
encryptedAESKey: encryptedAESKey,
encryptedText: encryptedText
};
// 发送数据到后端,这里使用fetch模拟发送请求
fetch('/api/sendData', {
method: 'POST',
headers: {
'Content - Type': 'application/json'
},
body: JSON.stringify(dataToSend)
})
.then(response => response.json())
.then(data => {
console.log('后端响应:', data);
})
.catch(error => {
console.error('发送数据失败:', error);
});
在上述代码中,首先定义要传输的正文数据plainText。然后使用CryptoJS.AES.encrypt(plainText, aesKey).toString()方法,利用之前生成的 AES 密钥aesKey对正文数据进行加密,得到加密后的正文数据encryptedText。接着构造dataToSend对象,包含加密后的 AES 密钥encryptedAESKey和加密后的正文数据encryptedText。最后,使用fetch函数发送 POST 请求到/api/sendData接口,将dataToSend对象转换为 JSON 字符串后作为请求体发送到后端。在请求成功后,处理后端返回的响应数据;如果请求过程中出现错误,将在控制台打印错误信息。通过这样的处理,确保了数据在前端的加密和封装,并安全地发送到后端。
(三)后端解密流程
- 解析 AES 密钥:后端接收到前端发送的数据后,首先使用 RSA 私钥解密得到 AES 密钥。在 Java 中,使用java.security包下的相关类实现解密操作。以下是使用 RSA 私钥解密 AES 密钥并转换为字节数组的代码示例:
import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class DecryptionUtil {
public static byte[] decryptAESKey(String encryptedAESKey, String privateKeyBase64) throws Exception {
byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] encryptedAESKeyBytes = Base64.getDecoder().decode(encryptedAESKey);
return cipher.doFinal(encryptedAESKeyBytes);
}
}
在上述代码中,首先将 Base64 编码的私钥字符串privateKeyBase64解码为字节数组privateKeyBytes,然后创建PKCS8EncodedKeySpec对象,传入私钥字节数组,通过KeyFactory生成私钥对象privateKey。接着获取Cipher实例,指定加密模式为RSA/ECB/PKCS1Padding,并将其初始化为解密模式,使用私钥privateKey进行初始化。将 Base64 编码的加密后的 AES 密钥字符串encryptedAESKey解码为字节数组encryptedAESKeyBytes,最后通过cipher.doFinal方法使用私钥对加密后的 AES 密钥进行解密,返回解密后的 AES 密钥字节数组。
2. AES 解密正文数据:使用 Apache Commons Codec 库实现 AES - ECB - PKCS5Padding 解密。以下是使用解密后的 AES 密钥对正文数据进行解密的代码示例:
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class DecryptionUtil {
// 省略解密AES密钥的方法
public static String decryptText(String encryptedText, byte[] aesKey) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(aesKey, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] encryptedTextBytes = Base64.decodeBase64(encryptedText);
byte[] decryptedBytes = cipher.doFinal(encryptedTextBytes);
return new String(decryptedBytes, "UTF-8");
}
}
在上述代码中,首先根据解密后的 AES 密钥字节数组aesKey创建SecretKeySpec对象secretKey,指定加密算法为 AES。然后获取Cipher实例,指定加密模式为AES/ECB/PKCS5Padding,并将其初始化为解密模式,使用secretKey进行初始化。将 Base64 编码的加密后的正文数据字符串encryptedText解码为字节数组encryptedTextBytes,通过cipher.doFinal方法使用 AES 密钥对加密后的正文数据进行解密,得到解密后的字节数组decryptedBytes,最后将解密后的字节数组转换为字符串并返回,使用UTF - 8编码确保字符的正确解析。
五、生产环境最佳实践与安全规范
(一)密钥管理规范
- 存储安全:在生产环境中,私钥的安全性至关重要,一旦私钥泄露,数据的安全性将受到严重威胁。因此,私钥禁止明文存储。可以借助安全配置中心,如 HashiCorp Vault 来管理私钥。Vault 提供了安全的密钥存储和访问控制功能,它采用了多种安全机制,如加密存储、访问策略控制等,确保私钥的安全。通过 Vault,可将私钥加密存储在其内部,只有经过授权的应用或用户才能通过特定的 API 请求获取私钥,并且在获取过程中,私钥会以加密的形式传输,进一步保障了私钥的安全。例如,在一个大型分布式系统中,各个微服务可能需要使用私钥进行数据解密或签名操作,通过将私钥存储在 Vault 中,每个微服务可以根据自身的权限配置,安全地获取所需的私钥,而无需担心私钥在存储或传输过程中被泄露。
除了使用安全配置中心,还可以通过环境变量来管理私钥。在应用启动时,将私钥以环境变量的形式注入到应用中,应用在运行过程中从环境变量中读取私钥。这种方式可以避免私钥在代码中硬编码,降低了私钥泄露的风险。例如,在 Linux 系统中,可以在启动应用的脚本中使用export命令设置环境变量,如export RSA_PRIVATE_KEY=xxxxxx,然后在应用代码中通过System.getenv("RSA_PRIVATE_KEY")来获取私钥。
公钥虽然可以公开,但为了进一步提高安全性和系统性能,也可以进行缓存处理。缓存公钥可以减少重复获取公钥的网络开销和计算资源消耗。可以使用本地缓存,如 Guava Cache(在 Java 应用中)或 Memcached 等分布式缓存来存储公钥。同时,为了防止公钥被篡改或过期,建议每月更新一次公钥。在更新公钥时,需要确保前端和后端的公钥同步更新,避免出现加密和解密不匹配的情况。例如,在一个电商应用中,用户登录时需要使用公钥对密码进行加密,通过缓存公钥,可以快速获取公钥进行加密操作,提高用户登录的响应速度。当公钥更新时,电商应用可以通过消息队列等机制通知前端和后端,确保公钥的一致性。
2. 生成规范:在生成 RSA 密钥对时,使用加密安全的随机数生成器是至关重要的。以 Java 的SecureRandom为例,它是一个强加密随机数生成器,能够生成高质量的随机数,确保生成的密钥对具有足够的随机性和安全性。在使用SecureRandom生成密钥对时,它会利用系统的熵源(如硬件设备的噪声、系统时钟的微小变化等)来生成随机数,这些随机数用于选择大质数\(p\)和\(q\),从而生成密钥对。例如,在 Java 中使用KeyPairGenerator生成 RSA 密钥对时,可以这样配置:
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");SecureRandom secureRandom = new SecureRandom();keyPairGenerator.initialize(2048, secureRandom);KeyPair keyPair = keyPairGenerator.generateKeyPair();
在上述代码中,SecureRandom实例secureRandom被传递给KeyPairGenerator的initialize方法,用于生成具有高随机性的密钥对。
在生产环境中,为了抵御当前计算能力下的破解风险,强制使用 2048 位以上的密钥是必要的。随着计算技术的不断发展,尤其是量子计算的潜在威胁,较长的密钥长度能够提供更高的安全性。2048 位密钥在当前的计算能力下,能够有效抵抗传统计算机的暴力破解攻击。而对于一些对安全性要求极高的场景,如金融机构的核心数据加密、政府机密信息传输等,推荐使用 4096 位密钥,进一步增强安全性。4096 位密钥虽然会增加加密和解密的计算开销,但能够提供更高级别的安全保障,即使面对未来可能出现的更强大的计算能力,也能有效保护数据的安全。例如,在金融交易系统中,涉及大量的资金转移和客户敏感信息,使用 4096 位密钥可以确保交易数据在传输和存储过程中的安全性,防止黑客通过暴力破解获取关键信息,保障金融交易的安全和稳定。
(二)跨平台兼容性处理
- 密钥格式统一:在前端使用 PKCS#8 格式私钥时,由于不同平台对密钥格式的解析方式可能不同,需要通过jsrsasign.KEYUTIL.getKey()方法进行解析,以确保私钥能够被正确识别和使用。jsrsasign库提供了丰富的密钥处理功能,KEYUTIL.getKey()方法能够将 PKCS#8 格式的私钥字符串解析为可用于加密和解密操作的密钥对象。例如,在前端使用jsrsasign库进行 RSA 解密时,可能会遇到如下代码:
import { KEYUTIL } from 'jsrsasign';const privateKeyPem = '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDEQ1D9x00iL87+\n...\n-----END PRIVATE KEY-----';const privateKey = KEYUTIL.getKey(privateKeyPem);// 使用privateKey进行解密操作
在上述代码中,首先定义了一个 PKCS#8 格式的私钥字符串privateKeyPem,然后通过KEYUTIL.getKey()方法将其解析为privateKey对象,后续就可以使用这个privateKey对象进行解密操作,确保了在前端环境中能够正确处理 PKCS#8 格式私钥。
后端生成 PEM 格式密钥时,确保包含完整的头尾标识(-----BEGIN PRIVATE KEY-----和-----END PRIVATE KEY-----)是非常重要的。这些标识用于标识密钥的类型和格式,不同的平台和库在解析密钥时依赖这些标识来识别密钥。如果缺少这些标识,可能会导致密钥解析失败,影响加密和解密的正常进行。例如,在 Java 中使用KeyPairGenerator生成 RSA 密钥对并将私钥转换为 PEM 格式时,需要正确添加头尾标识:
import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.PrivateKey;import java.security.spec.PKCS8EncodedKeySpec;import java.util.Base64;public class RSAUtil {public static String generatePrivateKeyPem() throws Exception {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");keyPairGenerator.initialize(2048);KeyPair keyPair = keyPairGenerator.generateKeyPair();PrivateKey privateKey = keyPair.getPrivate();PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());byte[] privateKeyBytes = pkcs8EncodedKeySpec.getEncoded();String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKeyBytes);StringBuilder privateKeyPem = new StringBuilder();privateKeyPem.append("-----BEGIN PRIVATE KEY-----\n");privateKeyPem.append(privateKeyBase64);privateKeyPem.append("\n-----END PRIVATE KEY-----");return privateKeyPem.toString();}}
在上述代码中,通过StringBuilder构建 PEM 格式的私钥字符串,正确添加了头尾标识,确保生成的 PEM 格式私钥能够被其他平台或库正确解析。
2. 编码一致性:在数据传输过程中,统一使用 Base64 编码传输密文是一种良好的实践。Base64 编码将二进制数据转换为文本格式,便于在网络中传输和在文本环境中存储。避免二进制数据直接传输,因为二进制数据在传输过程中可能会受到字符集、编码方式等因素的影响,导致数据丢失或损坏。例如,在前端将加密后的密文发送到后端时,需要将密文进行 Base64 编码:
import { encryptData } from '@/utils/rsa';const plainText = '要加密的明文';const encryptedData = encryptData(plainText);const encodedEncryptedData = btoa(encryptedData);// 将encodedEncryptedData发送到后端
在上述代码中,首先对明文进行加密得到encryptedData,然后使用btoa函数将加密后的密文进行 Base64 编码,得到encodedEncryptedData,最后将encodedEncryptedData发送到后端,确保密文在传输过程中的正确性和完整性。
当处理 URL 参数时,对加密结果进行encodeURIComponent()编码是必要的。URL 中只能包含特定的字符集,对于一些特殊字符,如+、/、=等,在 URL 中可能会被错误解析。encodeURIComponent()函数会将这些特殊字符转换为 URL 安全的格式,确保加密结果能够正确地作为 URL 参数传输。例如,在前端将加密后的密码作为 URL 参数发送到后端时,需要进行如下处理:
const encryptedPassword = encryptData('用户密码');const encodedPassword = encodeURIComponent(encryptedPassword);const url = `/api/login?password=${encodedPassword}`;// 发送请求到url
在上述代码中,首先对用户密码进行加密得到encryptedPassword,然后使用encodeURIComponent()函数对加密后的密码进行编码,得到encodedPassword,最后将encodedPassword作为 URL 参数构建请求 URL,确保密码在 URL 传输过程中的正确性,防止因特殊字符导致的参数解析错误。
(三)性能优化策略
- 前端优化:在前端进行长文本加密时,由于 RSA 加密对数据长度有限制,采用分片处理是一种有效的解决方案。将长文本分割为多个符合加密长度限制的块,然后对每个块进行单独加密,最后将加密后的块拼接起来,形成完整的密文。这样可以避免因长文本导致的加密失败或性能问题。例如,在加密一个较长的文件内容时,假设 RSA 加密的最大长度限制为 117 字节,可以使用如下代码进行分片加密:
import { encryptData } from '@/utils/rsa';const longText = '这里是非常长的文本内容...';const chunkSize = 117;let encryptedChunks = [];for (let i = 0; i < longText.length; i += chunkSize) {const chunk = longText.slice(i, i + chunkSize);const encryptedChunk = encryptData(chunk);encryptedChunks.push(encryptedChunk);}const encryptedLongText = encryptedChunks.join('');
在上述代码中,定义了长文本longText和每个分片的大小chunkSize,通过循环将长文本分割为多个大小为chunkSize的块,对每个块进行加密,将加密后的块存储在encryptedChunks数组中,最后将所有加密后的块拼接起来,得到加密后的长文本encryptedLongText。
使用 Web Workers 将加密逻辑放到后台线程执行,可以避免加密过程阻塞 UI 线程,提高用户体验。Web Workers 允许在后台线程中执行脚本,不会影响主线程的运行。例如,在进行长文本加密时,可以创建一个 Web Worker 来执行加密任务:
// main.jsconst worker = new Worker('encryptWorker.js');worker.onmessage = function (e) {const encryptedLongText = e.data;// 处理加密后的长文本};worker.postMessage('这里是非常长的文本内容...');// encryptWorker.jsself.onmessage = function (e) {const longText = e.data;const chunkSize = 117;let encryptedChunks = [];for (let i = 0; i < longText.length; i += chunkSize) {const chunk = longText.slice(i, i + chunkSize);const encryptedChunk = encryptData(chunk);encryptedChunks.push(encryptedChunk);}const encryptedLongText = encryptedChunks.join('');self.postMessage(encryptedLongText);};
在上述代码中,在main.js中创建了一个 Web Worker,将长文本发送到encryptWorker.js中进行加密处理。在encryptWorker.js中,执行长文本的分片加密逻辑,将加密后的长文本通过self.postMessage发送回main.js,在main.js中通过worker.onmessage接收加密后的长文本并进行后续处理,这样可以确保加密过程在后台线程执行,不会阻塞 UI 线程,用户可以继续进行其他操作,提高了应用的响应性和用户体验。
2. 后端优化:在后端,解密接口采用异步处理(如 Spring WebFlux)可以提升并发性能。在高并发场景下,大量的解密请求可能会导致服务器资源紧张,采用异步处理可以让服务器在处理解密请求时,不会阻塞其他请求的处理,提高服务器的并发处理能力。Spring WebFlux 是基于响应式编程模型的 Web 框架,它使用非阻塞 I/O,能够有效地处理大量并发请求。例如,在 Spring Boot 应用中,使用 Spring WebFlux 实现异步解密接口:
import org.springframework.http.MediaType;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RestController;import reactor.core.publisher.Mono;@RestControllerpublic class DecryptionController {@PostMapping(value = "/api/decrypt", consumes = MediaType.APPLICATION_JSON_VALUE)public Mono<String> decrypt(@RequestBody Mono<String> encryptedDataMono) {return encryptedDataMono.map(encryptedData -> {// 这里进行解密操作try {// 假设已经有私钥String privateKeyBase64 = "这里是私钥";String decryptedData = RSAUtil.decrypt(encryptedData, privateKeyBase64);return "解密成功: " + decryptedData;} catch (Exception e) {return "解密失败: " + e.getMessage();}});}}
在上述代码中,@PostMapping注解的decrypt方法接收一个Mono<String>类型的encryptedDataMono,表示异步接收加密数据。通过map操作对加密数据进行解密处理,返回一个包含解密结果的Mono<String>,Spring WebFlux 会自动处理异步操作,提高接口的并发性能。
缓存常用公钥可以减少密钥生成开销。在实际应用中,可能会频繁使用某些公钥进行加密操作,每次都重新生成公钥会消耗大量的计算资源和时间。通过缓存常用公钥,可以在需要时直接从缓存中获取,避免重复生成公钥的开销。可以使用本地缓存,如 Guava Cache(在 Java 应用中)或分布式缓存,如 Redis 来存储公钥。例如,在 Java 应用中使用 Guava Cache 缓存公钥:
import com.google.common.cache.Cache;import com.google.common.cache.CacheBuilder;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.NoSuchAlgorithmException;import java.security.PublicKey;public class RSACacheUtil {private static final Cache<String, PublicKey> publicKeyCache = CacheBuilder.newBuilder().maximumSize(100).build();public static PublicKey getPublicKey(String keyId) {return publicKeyCache.getIfPresent(keyId);}public static void putPublicKey(String keyId, PublicKey publicKey) {publicKeyCache.put(keyId, publicKey);}public static PublicKey generateAndCachePublicKey(String keyId) throws NoSuchAlgorithmException {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");keyPairGenerator.initialize(2048);KeyPair keyPair = keyPairGenerator.generateKeyPair();PublicKey publicKey = keyPair.getPublic();putPublicKey(keyId, publicKey);return publicKey;}}
在上述代码中,定义了一个publicKeyCache,使用CacheBuilder进行配置,设置最大缓存数量为 100。getPublicKey方法用于从缓存中获取公钥,putPublicKey方法用于将公钥存入缓存,generateAndCachePublicKey方法用于生成公钥并将其存入缓存,在后续需要使用公钥时,可以通过getPublicKey方法从缓存中获取,减少了公钥生成的开销,提高了系统性能。
(四)安全漏洞防范
- 中间人攻击:为了防止中间人攻击,强制启用 HTTPS 是必要的。HTTPS 通过 SSL/TLS 协议对数据进行加密传输,确保数据在传输过程中的安全性。同时,配合 HSTS(HTTP Strict Transport Security)头可以进一步增强安全性,防止协议降级攻击。HSTS 头告诉浏览器只能通过 HTTPS 访问网站,即使在用户手动输入 HTTP 地址时,浏览器也会自动将其转换为 HTTPS。例如,在 Web 服务器配置中添加 HSTS 头:
server {
listen 443 ssl;
server_name example.com;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
# 其他配置
}
在上述 Nginx 配置中,通过add_header指令添加了 HSTS 头,设置max - age为 31536000 秒(一年),表示浏览器在一年内都应使用 HTTPS 访问该网站,includeSubDomains表示子域名也应遵循此规则,preload表示将该网站添加到 HSTS 预加载列表中,进一步增强安全性。
前端验证公钥指纹可以确保获取的公钥未被篡改。公钥指纹是公钥的唯一标识,通过比对公钥指纹,可以验证公钥的真实性。在获取公钥时,前端
六、典型问题排查与解决方案
(一)加密解密失败常见原因
在 RSA 加密解密过程中,可能会遇到各种问题导致加密解密失败。以下是一些常见问题及原因和解决方案:
问题现象 | 可能原因 | 解决方案 |
前端加密报错 "Message too long" | 数据长度超过 RSA 单次加密限制 | 使用分段加密,或改用 AES+RSA 混合方案 |
后端解密返回乱码 | 编码格式不一致 | 统一使用 UTF-8 编码,检查 Base64 编解码是否正确 |
私钥解析失败 | 密钥格式错误 | 确保私钥包含完整的 PKCS#8 头尾部,使用解析 PEM 格式 |
当出现前端加密报错 "Message too long" 时,这通常是因为数据长度超过了 RSA 单次加密的限制。在 RSA 加密中,由于其加密原理和数学运算的特性,对单次加密的数据长度有严格要求。例如,对于 1024 位密钥,一般最多只能加密 117 字节的数据。当需要加密的数据长度超过这个限制时,就会出现上述报错。为了解决这个问题,可以采用分段加密的方式,将长数据分割成多个符合加密长度限制的小块,分别对这些小块进行加密,最后再将加密后的小块拼接起来。也可以考虑改用 AES+RSA 混合方案,利用 AES 加密速度快且对数据长度限制较小的特点,先使用 AES 对长数据进行加密,然后再使用 RSA 加密 AES 密钥,这样既能保证数据的安全性,又能有效处理长数据。
后端解密返回乱码,往往是由于编码格式不一致导致的。在数据传输和处理过程中,前端和后端可能使用不同的编码格式,或者在进行 Base64 编解码时出现错误,都可能导致解密后的结果出现乱码。例如,前端使用 UTF-8 编码对数据进行加密,而后端在解密时却使用了 GBK 编码,就会导致乱码。为了解决这个问题,需要统一使用 UTF-8 编码,这是一种广泛支持且能够处理多种语言字符的编码格式。同时,要仔细检查 Base64 编解码是否正确,确保数据在编码和解码过程中的准确性。
私钥解析失败,通常是因为密钥格式错误。在 RSA 加密中,私钥需要符合特定的格式,如 PKCS#8 格式,并且要包含完整的头尾部标识(-----BEGIN PRIVATE KEY-----和-----END PRIVATE KEY-----)。如果私钥格式不正确,或者缺少这些关键标识,就会导致私钥解析失败。例如,在从文件中读取私钥时,如果文件格式不正确,或者私钥内容被篡改,就可能出现解析失败的情况。为了解决这个问题,要确保私钥包含完整的 PKCS#8 头尾部,并且在使用私钥时,正确解析 PEM 格式,以保证私钥能够被正确识别和使用。
(二)跨语言兼容性问题
1.Java 与 JS 的 PKCS#1/PKCS#8 差异:Java 默认生成 PKCS#8 格式私钥,而 JS 的 jsencrypt 库在使用 PKCS#8 格式私钥时,需要手动添加头尾标识。这是因为不同语言和库对密钥格式的处理方式存在差异。在 Java 中,PKCS#8 格式是一种标准的私钥格式,Java 的加密库能够直接识别和处理这种格式的私钥。而在 JS 中,jsencrypt 库在处理 PKCS#8 格式私钥时,需要明确的头尾标识来识别私钥的开始和结束。如果缺少这些标识,jsencrypt 库可能无法正确解析私钥,导致加密或解密失败。
2.解决方案:后端生成 PEM 格式私钥时,统一添加-----BEGIN PRIVATE KEY-----和-----END PRIVATE KEY-----。这样,无论是 Java 还是 JS,都能正确识别和使用私钥,避免因格式差异导致的兼容性问题。在 Java 中生成私钥时,可以按照以下方式添加头尾标识:
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class RSAUtil {
public static String generatePrivateKeyPem() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());
byte[] privateKeyBytes = pkcs8EncodedKeySpec.getEncoded();
String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKeyBytes);
StringBuilder privateKeyPem = new StringBuilder();
privateKeyPem.append("-----BEGIN PRIVATE KEY-----\n");
privateKeyPem.append(privateKeyBase64);
privateKeyPem.append("\n-----END PRIVATE KEY-----");
return privateKeyPem.toString();
}
}
在上述代码中,通过StringBuilder构建 PEM 格式的私钥字符串,正确添加了头尾标识,确保生成的私钥能够在不同语言环境中被正确解析和使用。
3. 填充模式不匹配:Java 默认使用 RSA/ECB/PKCS1Padding,而 JS 的 jsrsasign 库需显式设置算法参数。填充模式在 RSA 加密中起着重要作用,它用于在明文长度不足时进行填充,确保每个块都能被正确加密。不同的填充模式会影响加密和解密的结果,如果加密和解密两端使用的填充模式不一致,就会导致解密失败。在 Java 中,RSA/ECB/PKCS1Padding 是一种常用的填充模式,它在加密时会在明文后面填充一些特定的字节,以满足加密块的长度要求。而在 JS 的 jsrsasign 库中,需要显式设置算法参数来指定填充模式,如果未正确设置,就可能导致填充模式不匹配。
4. 前端代码示例:
import { KJUR } from 'jsrsasign';
// 假设已经获取到公钥和待加密数据
const publicKeyPem = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----';
const dataToEncrypt = '要加密的数据';
// 创建RSAKey对象并设置公钥
const rsaKey = new KJUR.crypto.RSAKey();
rsaKey.readPublicKeyFromPEMString(publicKeyPem);
// 创建Cipher对象并设置加密模式和填充方式
const cipher = new KJUR.crypto.Cipher({
alg: 'RSA/ECB/PKCS1Padding',
mode: 'ECB',
padding: 'PKCS1Padding'
});
// 初始化Cipher对象为公钥加密模式
cipher.init(rsaKey, 'e');
// 加密数据
const encryptedData = cipher.doFinal(dataToEncrypt);
在上述代码中,首先创建了RSAKey对象并设置公钥,然后创建Cipher对象并显式设置加密模式为RSA/ECB/PKCS1Padding,包括模式ECB和填充方式PKCS1Padding。最后,初始化Cipher对象为公钥加密模式,并对数据进行加密,确保了与 Java 端填充模式的一致性,避免了因填充模式不匹配导致的加密解密问题。
(三)性能瓶颈优化
- 前端长文本加密卡顿问题:一次性加密大文件时,由于 RSA 加密计算量较大,会导致浏览器假死。在前端进行 RSA 加密时,当处理大文件或长文本时,加密过程需要进行大量的数学运算,如幂运算和模运算,这些运算会占用大量的 CPU 资源和时间。而浏览器的主线程负责处理用户界面的渲染和交互,如果在主线程中进行长时间的加密操作,就会导致主线程被阻塞,无法及时响应用户的操作,从而出现浏览器假死的情况。
- 方案:实现进度条和分片加密,使用 Blob.slice () 分块处理,结合 requestIdleCallback 优化主线程占用。Blob.slice()方法可以将大文件分割成多个小块,每个小块的大小可以根据 RSA 加密的最大长度限制来确定。然后,对每个小块进行加密,这样可以减少每次加密的计算量,避免一次性加密大文件导致的卡顿。requestIdleCallback函数可以在浏览器空闲时执行回调函数,将加密任务放在浏览器空闲时间执行,避免占用主线程的时间,从而提高用户体验。在加密过程中,可以通过实现进度条来实时显示加密进度,让用户了解加密的进展情况。
- 后端高并发解密延迟问题:大量解密请求会导致 CPU 负载过高,从而产生高并发解密延迟问题。在后端处理解密请求时,每个解密操作都需要进行复杂的数学运算,如使用私钥对密文进行解密时,需要进行幂运算和模运算。当有大量解密请求同时到达时,CPU 需要同时处理这些运算,会导致 CPU 负载过高,处理速度变慢,从而出现解密延迟的情况。
- 方案:使用连接池管理 Cipher 实例(需注意线程安全),或引入硬件加速(如 OpenSSL 引擎)。使用连接池管理 Cipher 实例可以减少 Cipher 实例的创建和销毁开销,提高资源利用率。在高并发场景下,频繁创建和销毁 Cipher 实例会消耗大量的系统资源,使用连接池可以复用 Cipher 实例,减少资源浪费。但需要注意线程安全问题,因为多个线程可能同时访问和使用 Cipher 实例,需要采取适当的同步机制来确保数据的一致性和安全性。引入硬件加速(如 OpenSSL 引擎)可以利用硬件的特性,如专门的加密芯片或多核 CPU 的并行计算能力,来加速解密过程,提高系统的并发处理能力。OpenSSL 引擎是一个开源的加密库,它提供了对多种加密算法的支持,并且可以利用硬件加速来提高加密和解密的速度。在 Java 中,可以通过 JNI(Java Native Interface)技术调用 OpenSSL 引擎,实现硬件加速的解密操作。
七、总结与扩展方向
(一)技术价值与应用前景
RSA 作为非对称加密的基石,在数据安全传输领域有着不可替代的地位。它基于数论原理,通过公钥加密、私钥解密的方式,巧妙地解决了对称加密中密钥配送的难题,为数据在不安全的网络环境中传输提供了可靠的保障。在实际应用中,结合前端 Vue 和后端 Java 的实现方案,我们能够构建出完整的数据加密传输体系,从用户输入数据的前端加密,到后端接收并解密数据,这一全流程覆盖了基础原理到工程实践的各个环节,具有极高的实用价值。
在用户认证场景中,RSA 加密可以对用户的登录密码进行加密传输,确保密码在网络传输过程中不被窃取,只有拥有私钥的服务器端才能解密获取真实密码,从而验证用户身份。在支付系统中,涉及大量的资金交易和用户敏感信息,RSA 加密可用于保护支付信息的安全,防止支付数据被篡改或窃取,保障用户的财产安全。在数据接口加密方面,RSA 可以对接口请求和响应数据进行加密,确保数据在传输过程中的完整性和保密性,防止数据被非法获取或篡改。
随着科技的不断发展,量子计算逐渐成为研究热点,其强大的计算能力对传统加密算法构成了潜在威胁。在量子计算环境下,RSA 加密所依赖的大整数分解难题可能不再安全,因为量子计算机有可能在短时间内完成对大整数的分解,从而破解 RSA 加密。因此,未来我们需要密切关注后量子密码算法的发展,如格密码等。格密码基于格理论,具有抗量子攻击的特性,它通过在格空间中进行复杂的数学运算来实现加密和解密,目前被认为是一种有潜力的后量子密码算法。虽然格密码在实际应用中还面临一些挑战,如算法复杂度较高、实现难度较大等,但随着研究的深入和技术的进步,有望在未来成为保障数据安全的重要手段。尽管如此,在当前的计算环境下,RSA 仍然具有显著的优势,其安全性在传统计算机上经过了长时间的验证,并且在现有技术条件下,破解 RSA 加密仍然是极其困难的。因此,RSA 在未来一段时间内仍将在数据安全领域发挥重要作用。
(二)扩展开发建议
- 集成数字签名:在接口请求中添加 RSA 签名是增强数据完整性和防篡改能力的有效手段。数字签名利用私钥对数据进行签名,生成一个唯一的签名值,这个签名值与数据紧密绑定。接收方在接收到数据和签名后,使用发送方的公钥对签名进行验证。如果数据在传输过程中被篡改,签名验证将失败,因为篡改后的数据对应的签名值会发生变化。在使用 jsrsasign 库进行签名生成与校验时,首先需要获取私钥和待签名的数据。假设我们已经有一个包含接口请求参数的对象requestParams,并且获取到了私钥privateKeyPem,可以按照以下步骤进行签名生成:
import { KEYUTIL, KJUR } from 'jsrsasign';
// 假设已经获取到私钥和待签名的数据
const privateKeyPem = '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDEQ1D9x00iL87+\n...\n-----END PRIVATE KEY-----';
const requestParams = {
param1: 'value1',
param2: 'value2'
};
// 将请求参数转换为字符串,确保数据的一致性
const dataToSign = JSON.stringify(requestParams);
// 使用私钥进行签名
const privateKey = KEYUTIL.getKey(privateKeyPem);
const sig = new KJUR.crypto.Signature({ alg: 'SHA256withRSA' });
sig.init(privateKey);
sig.updateString(dataToSign);
const signature = sig.sign();
// 将签名添加到请求参数中,发送到后端
requestParams.signature = signature;
在后端接收到请求后,使用发送方的公钥对签名进行校验。假设后端使用 Java 进行开发,并且已经获取到公钥publicKeyPem和接收到的签名signature以及请求参数requestParams,可以按照以下步骤进行签名校验:
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import cn.hutool.json.JSONUtil;
public class SignatureVerification {
public static boolean verifySignature(String publicKeyPem, String signature, Object requestParams) {
RSA rsa = new RSA(null, publicKeyPem);
// 将请求参数转换为字符串,确保与前端签名时的数据一致
String dataToVerify = JSONUtil.toJsonStr(requestParams);
return rsa.verify(dataToVerify, signature, KeyType.PublicKey);
}
}
通过上述步骤,在接口请求中集成 RSA 签名,能够有效地确保数据在传输过程中的完整性和防篡改能力,提高系统的安全性。
2. 结合 OAuth2.0:在 OAuth2.0 的授权码模式中,使用 RSA 对令牌进行签名可以显著增强 OAuth 流程的安全性。OAuth2.0 是一种广泛应用的授权框架,用于授权第三方应用访问用户资源。在授权码模式下,客户端首先向授权服务器请求授权码,用户在授权服务器上进行登录并授权,授权服务器返回授权码给客户端。客户端使用授权码向授权服务器请求访问令牌和刷新令牌。在这个过程中,使用 RSA 对令牌进行签名可以防止令牌被伪造或篡改。假设授权服务器使用 Java 开发,并且已经生成了 RSA 密钥对,私钥用于对令牌进行签名,公钥用于验证签名。当生成访问令牌时,可以使用以下代码对令牌进行签名:
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class TokenGenerator {
private static final String PRIVATE_KEY_PEM = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDEQ1D9x00iL87+\n...\n-----END PRIVATE KEY-----";
private static final long EXPIRATION_TIME = 3600000; // 令牌有效期1小时
public static String generateToken(Claims claims) {
RSA rsa = new RSA(PRIVATE_KEY_PEM, null);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.RS256, rsa.getPrivateKey())
.compact();
}
}
在资源服务器接收到令牌时,使用公钥对令牌进行验证:
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class TokenValidator {
private static final String PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----";
public static boolean validateToken(String token) {
try {
RSA rsa = new RSA(null, PUBLIC_KEY_PEM);
Claims claims = Jwts.parserBuilder()
.setSigningKey(rsa.getPublicKey())
.build()
.parseClaimsJws(token)
.getBody();
Date expiration = claims.getExpiration();
return expiration.after(new Date());
} catch (Exception e) {
return false;
}
}
}
通过在 OAuth2.0 的授权码模式中使用 RSA 对令牌进行签名和验证,能够增强 OAuth 流程的安全性,确保令牌的真实性和完整性,防止令牌被伪造或篡改,保护用户资源的安全访问。
3. 移动端适配:将前端 Vue 的加密逻辑迁移至 Android(Kotlin)和 iOS(Swift),实现多平台统一加密方案,能够扩大加密技术的应用范围,为用户提供更全面的安全保障。在 Android 平台上,使用 Kotlin 实现 RSA 加密可以借助 Bouncy Castle 库,该库提供了丰富的加密算法实现。以下是一个简单的 Kotlin 示例,展示如何使用 Bouncy Castle 库进行 RSA 加密:
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.crypto.AsymmetricCipherKeyPair
import org.bouncycastle.crypto.generators.RSAKeyPairGenerator
import org.bouncycastle.crypto.params.RSAKeyGenerationParameters
import org.bouncycastle.crypto.util.PrivateKeyFactory
import org.bouncycastle.crypto.util.PublicKeyFactory
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.math.BigInteger
import java.security.KeyPair
import java.security.Security
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.*
class RSAUtil {
companion object {
init {
Security.addProvider(BouncyCastleProvider())
}
fun generateKeyPair(): KeyPair {
val keyPairGenerator = RSAKeyPairGenerator()
val random = SecureRandom()
val keyGenerationParameters = RSAKeyGenerationParameters(
BigInteger("10001", 16),
random,
2048,
80
)
keyPairGenerator.init(keyGenerationParameters)
val keyPair: AsymmetricCipherKeyPair = keyPairGenerator.generateKeyPair()
val publicKey = PublicKeyFactory.createKey(
SubjectPublicKeyInfo.getInstance(keyPair.public.encoded)
)
val privateKey = PrivateKeyFactory.createKey(
PrivateKeyInfo.getInstance(keyPair.private.encoded)
)
return KeyPair(publicKey, privateKey)
}
fun encrypt(data: String, publicKey: String): String {
val keySpec = X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))
val keyFactory = java.security.KeyFactory.getInstance("RSA", "BC")
val pubKey = keyFactory.generatePublic(keySpec)
val cipher = javax.crypto.Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC")
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, pubKey)
val encryptedBytes = cipher.doFinal(data.toByteArray())
return Base64.getEncoder().encodeToString(encryptedBytes)
}
fun decrypt(encryptedData: String, privateKey: String): String {
val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))
val keyFactory = java.security.KeyFactory.getInstance("RSA", "BC")
val privKey = keyFactory.generatePrivate(keySpec)
val cipher = javax.crypto.Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC")
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, privKey)
val encryptedBytes = Base64.getDecoder().decode(encryptedData)
val decryptedBytes = cipher.doFinal(encryptedBytes)
return String(decryptedBytes)
}
}
}
在 iOS 平台上,使用 Swift 实现 RSA 加密可以借助 CommonCrypto 库。以下是一个简单的 Swift 示例,展示如何使用 CommonCrypto 库进行 RSA 加密:
import Foundation
import CommonCrypto
func generateKeyPair() -> (publicKey: String, privateKey: String)? {
var publicKeyRef: SecKey?
var privateKeyRef: SecKey?
let parameters: [String : Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits as String: 2048,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: false,
kSecAttrApplicationTag as String: "com.example.rsa"
],
kSecPublicKeyAttrs as String: [
kSecAttrIsPermanent as String: false,
kSecAttrApplicationTag as String: "com.example.rsa"
]
]
let status = SecKeyGeneratePair(parameters as CFDictionary, &publicKeyRef, &privateKeyRef)
guard status == errSecSuccess, let publicKey = publicKeyRef, let privateKey = privateKeyRef else {
return nil
}
let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) as! Data
let privateKeyData = SecKeyCopyExternalRepresentation(privateKey, nil) as! Data
let publicKeyBase64 = publicKeyData.base64EncodedString()
let privateKeyBase64 = privateKeyData.base64EncodedString()
return (publicKeyBase64, privateKeyBase64)
}
func encrypt(data: String, publicKey: String) -> String? {
guard let publicKeyData = Data(base64Encoded: publicKey),
let publicKey = SecKeyCreateWithData(publicKeyData as CFData, nil) else {
return nil
}
let dataToEncrypt = data.data(using:.utf8)!
var encryptedData = Data(count: Int(CCCryptor(kCCEncrypt, kCCAlgorithmRSAES_OAEP, kCCOptionPKCS1Padding, nil, 0, nil, dataToEncrypt, dataToEncrypt.count, nil, 0, nil)))
let status = SecKeyEncrypt(publicKey,
SecPadding.PKCS1,
dataToEncrypt,
&encryptedData)
guard status == errSecSuccess else {
return nil
}
return encryptedData.base64EncodedString()
}
func decrypt(encryptedData: String, privateKey: String) -> String? {
guard let privateKeyData = Data(base64Encoded: privateKey),
let privateKey = SecKeyCreateWithData(privateKeyData as CFData, nil) else {
return nil
}
guard let dataToDecrypt = Data(base64Encoded: encryptedData) else {
return nil
}
var decryptedData = Data(count: Int(CCCryptor(kCCDecrypt, kCCAlgorithmRSAES_OAEP, kCCOptionPKCS1Padding, nil, 0, nil, dataToDecrypt, dataToDecrypt.count, nil, 0, nil)))
let status = SecKeyDecrypt(privateKey,
SecPadding.PKCS1,
dataToDecrypt,
&decryptedData)
guard status == errSecSuccess else {
return nil
}
return String(data: decryptedData, encoding:.utf8)
}
通过将前端 Vue 的加密逻辑迁移至 Android 和 iOS 平台,实现多平台统一加密方案,可以确保在不同的移动设备上都能为用户提供一致的安全加密服务,保护用户数据的安全,提升用户体验。
通过本文的详细解析,开发者可快速掌握 RSA 在前后端的实战应用,同时理解其背后的数学原理和安全设计,为构建高安全性的企业级系统奠定基础。在实际开发过程中,开发者可以根据具体的业务需求和场景,灵活运用 RSA 加密技术,并结合其他安全措施,如 HTTPS、数字证书等,进一步提升系统的安全性和可靠性。同时,不断关注加密技术的发展动态,及时引入新的技术和方法,以应对不断变化的安全挑战。