我們一起聊聊加解密的藝術(shù)
加密這個(gè)事情其實(shí)之前和小伙伴們聊過很多次,不過最近松哥又想到一些細(xì)節(jié)問題,再和小伙伴們補(bǔ)充聊一聊。
一 對稱加密
對稱加密和非對稱加密是兩種不同的加密方法,它們在數(shù)據(jù)安全和信息傳輸中扮演著重要的角色。下面我將分別介紹這兩種加密技術(shù):
1.1 對稱加密(Symmetric Encryption)
對稱加密是指加密和解密都使用相同的密鑰。這意味著發(fā)送方和接收方都必須擁有這個(gè)密鑰才能進(jìn)行加密和解密操作。
常見的對稱加密算法:
- AES(高級(jí)加密標(biāo)準(zhǔn))
- DES(數(shù)據(jù)加密標(biāo)準(zhǔn))
- 3DES(三重?cái)?shù)據(jù)加密算法)
1.2 對稱加密特點(diǎn)
一般來說,對稱加密具有如下特點(diǎn):
- 速度較快:由于加密和解密使用相同的密鑰,對稱加密通常比非對稱加密要快。
- 密鑰管理:對稱加密的主要挑戰(zhàn)在于密鑰的分發(fā)和管理。如果密鑰泄露,加密的安全性就會(huì)受到威脅。
- 適用于大量數(shù)據(jù):由于速度快,對稱加密適合加密大量數(shù)據(jù)。
從這里可以看到,對稱加密主要有兩大優(yōu)勢:第一就是運(yùn)算速度快;第二就是適用于大量數(shù)據(jù)。
但是,對稱加密有一個(gè)致命的問題,就是密鑰管理。如何從服務(wù)端將密鑰安全的傳輸?shù)娇蛻舳耸莻€(gè)問題!另外就是當(dāng)一對多通信的時(shí)候,如何管理好密鑰不被泄露也是一個(gè)考驗(yàn)。這是對稱加密的不足之處。
1.3 代碼案例
接下來松哥給大家演示下 Java 代碼如何做對稱加解密。
在 Java 中實(shí)現(xiàn)對稱加密,通常使用 Java 加密架構(gòu)(Java Cryptography Architecture, JCA)提供的類和接口。
下面是一個(gè)使用 AES(高級(jí)加密標(biāo)準(zhǔn))算法進(jìn)行對稱加密和解密的簡單示例:
public class SymmetricEncryptionExample {
// 生成密鑰
public static SecretKey generateKey() throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(128); // 可以是128, 192或256位
return keyGenerator.generateKey();
}
// 加密方法
public static String encrypt(String data, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptedBytes = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encryptedBytes);
}
// 解密方法
public static String decrypt(String encryptedData, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
return new String(decryptedBytes);
}
public static void main(String[] args) {
try {
// 生成密鑰
SecretKey key = generateKey();
// 原始數(shù)據(jù)
String originalData = "Hello, JavaBoy!";
// 加密
String encryptedData = encrypt(originalData, key);
System.out.println("加密數(shù)據(jù): " + encryptedData);
// 解密
String decryptedData = decrypt(encryptedData, key);
System.out.println("解密數(shù)據(jù): " + decryptedData);
} catch (Exception e) {
e.printStackTrace();
}
}
}- 生成密鑰:使用 KeyGenerator 生成一個(gè)AES密鑰。
- 加密:使用 Cipher 類進(jìn)行加密,將數(shù)據(jù)轉(zhuǎn)換成字節(jié)后加密,并使用 Base64 編碼轉(zhuǎn)換為字符串,以便于存儲(chǔ)或傳輸。
- 解密:將加密的字符串解碼回字節(jié),然后使用相同的密鑰進(jìn)行解密。
以上代碼大家需要注意的是:
- 密鑰長度(如 128 位)應(yīng)根據(jù)安全需求選擇。
- 確保密鑰安全存儲(chǔ),不要在代碼中硬編碼密鑰。
- 對于生產(chǎn)環(huán)境,應(yīng)考慮使用更安全的密鑰管理策略。
出于安全考慮,我們一般使用上面的方案生成密鑰。這種方案生成的密鑰有一個(gè)特點(diǎn)就是系統(tǒng)每次重啟就會(huì)變。如果你希望能夠自己控制密鑰的生成,那么可以通過如下方式生成密鑰:
public static SecretKey generateKeyFromPassword(String password, int keySize) throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), "salt".getBytes(), 65536, keySize);
return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
}這樣就可以通過自己傳入的參數(shù)去控制密鑰。
1.4 前后端搭配
在跨語言(如 JavaScript 和 Java)使用 AES 算法進(jìn)行加密和解密時(shí),關(guān)鍵是確保兩端使用相同的密鑰、算法模式(如 CBC, ECB 等)、填充模式(如 PKCS5Padding, PKCS7Padding 等)和初始化向量(IV,如果使用了需要 IV的 模式如 CBC)。
之前有小伙伴說自己前端加密之后后端總是無法解密,松哥這里也給一個(gè)前后端搭配的例子。
前端加密后端解密
前端代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AES加密示例</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
</head>
<body>
<script>
function encryptAES(text, secretKey) {
const key = CryptoJS.enc.Utf8.parse(secretKey);
const iv = CryptoJS.lib.WordArray.random(128 / 8); // 對于CBC模式,需要IV
const encrypted = CryptoJS.AES.encrypt(text, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
// 返回加密后的文本和IV(Base64格式),實(shí)際使用中可能需要安全地傳輸這些值
return {
ciphertext: encrypted.toString(),
iv: iv.toString(CryptoJS.enc.Base64)
};
}
const secretKey = 'helloworldhelloworldhelloworld11'; // 確保密鑰是32個(gè)字符長(256位)
const text = 'Hello, javaboy!';
const result = encryptAES(text, secretKey);
console.log(result);
</script>
</body>
</html>后端代碼:
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class AESDecrypt {
public static String decryptAES(String encryptedData, String secretKey, String iv) throws Exception {
IvParameterSpec ivParameterSpec = new IvParameterSpec(Base64.getDecoder().decode(iv));
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] decodedValue = Base64.getDecoder().decode(encryptedData);
byte[] decryptedValue = cipher.doFinal(decodedValue);
return new String(decryptedValue, "UTF-8");
}
public static void main(String[] args) {
try {
//前端加密后的文本
String encryptedText = "PYANpAjMsRnBIEhovtEXQw==";
String secretKey = "helloworldhelloworldhelloworld11";
//前端 IV,要和加密后的文本一起傳到后端
String iv = "y/jUHcgSOpOiyNlsfjNUBg==";
String decryptedText = decryptAES(encryptedText, secretKey, iv);
System.out.println("解密文本: " + decryptedText);
} catch (Exception e) {
e.printStackTrace();
}
}
}注意這里前端加密時(shí)會(huì)產(chǎn)生一個(gè) iv 參數(shù),要隨著前端加密結(jié)果一起傳遞給后端。
后端加密前端解密
在 Java 進(jìn)行 AES 加密并在 JavaScript 中解密時(shí),同樣需要確保兩端使用相同的密鑰、算法模式(如 CBC、ECB 等)、填充模式(如 PKCS5Padding、PKCS7Padding 等)以及(如果適用)相同的初始化向量(IV)。
后端代碼:
public class AESEncrypt {
public static String encryptAES(String plainText, String secretKey, String iv) throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(256); // 對于AES-256
SecretKey secret = new SecretKeySpec(secretKey.getBytes("UTF-8"), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(Base64.getDecoder().decode(iv));
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secret, ivParameterSpec);
byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(encrypted);
}
public static void main(String[] args) {
try {
String plainText = "Hello, 江南一點(diǎn)雨!";
String secretKey = "helloworldhelloworldhelloworld11"; // 確保密鑰是32個(gè)字符長(256位)
String iv = Base64.getEncoder().encodeToString(new byte[16]); // 示例IV,實(shí)際應(yīng)用中應(yīng)更安全地生成
String encryptedText = encryptAES(plainText, secretKey, iv);
System.out.println("Encrypted text: " + encryptedText);
System.out.println("IV (Base64): " + iv); // 確保將IV發(fā)送給解密方
} catch (Exception e) {
e.printStackTrace();
}
}
}前端代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
<script>
function decryptAES(ciphertext, secretKey, iv) {
const key = CryptoJS.enc.Utf8.parse(secretKey);
const ivParsed = CryptoJS.enc.Base64.parse(iv);
const decrypted = CryptoJS.AES.decrypt(
{
ciphertext: CryptoJS.enc.Base64.parse(ciphertext)
},
key,
{
iv: ivParsed,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
return decrypted.toString(CryptoJS.enc.Utf8);
}
const secretKey = 'helloworldhelloworldhelloworld11';
const iv = 'AAAAAAAAAAAAAAAAAAAAAA=='; // 從Java代碼獲取
const ciphertext = '1lPNVF1injas78KUWeKp5FusEr6f0pGgcrLAg9ELFr8='; // 從Java代碼獲取
const decryptedText = decryptAES(ciphertext, secretKey, iv);
console.log(decryptedText);
</script>
</head>
<body>
</body>
</html>以上前后端交互加解密代碼松哥親測都是沒問題的,大家有這方面的需求記得及時(shí)收藏本文,可以作為參考。
二 非對稱加密
2.1 什么是非對稱加密
非對稱加密,也稱為公鑰加密,是一種使用兩個(gè)不同密鑰(公鑰和私鑰)的加密方式。
一般來說,非對稱加密有如下幾種不同的特點(diǎn):
- 公鑰與私鑰
公鑰與私鑰的生成:非對稱加密使用一對密鑰,公鑰是公開的,任何人都可以訪問,而私鑰是私有的,只有密鑰的持有者可以訪問。這兩個(gè)密鑰是由數(shù)學(xué)算法生成的,且相互關(guān)聯(lián)但不可從一方推導(dǎo)出另一方。
- 安全性高
難以破解:由于公鑰和私鑰的復(fù)雜數(shù)學(xué)關(guān)系,非對稱加密的安全性較高。攻擊者很難從公鑰中推斷出私鑰,從而保證了加密數(shù)據(jù)的安全性。
抗量子計(jì)算攻擊:一些區(qū)塊鏈項(xiàng)目開始采用抗量子計(jì)算攻擊的加密算法,如橢圓曲線數(shù)字簽名算法(ECDSA)的量子安全變體,以應(yīng)對未來量子計(jì)算的威脅。
- 公開密鑰分發(fā)方便
公鑰的公開性:公鑰可以公開給任何人,因此分發(fā)起來非常方便。任何人都可以使用公鑰來加密數(shù)據(jù),但只有私鑰持有者才能解密。
- 身份驗(yàn)證與數(shù)字簽名
身份驗(yàn)證:公鑰可以用作用戶的身份標(biāo)識(shí),其他人可以驗(yàn)證用戶的身份而無需了解其私鑰。這有助于在區(qū)塊鏈等去中心化網(wǎng)絡(luò)中建立可信身份。
數(shù)字簽名:私鑰持有者可以使用私鑰對消息進(jìn)行簽名,其他人則可以使用公鑰來驗(yàn)證簽名的真實(shí)性。這確保了消息的完整性和來源的可靠性。
- 安全通信
加密通信:公鑰可以用于加密消息,只有持有相應(yīng)私鑰的人才能解密。這確保了通信過程中的數(shù)據(jù)安全,防止了信息被未經(jīng)授權(quán)的人員訪問。
- 數(shù)字資產(chǎn)控制
區(qū)塊鏈地址與私鑰:區(qū)塊鏈地址通常由公鑰派生而來,用戶通過私鑰來控制與該地址相關(guān)聯(lián)的數(shù)字資產(chǎn)。私鑰的安全性對數(shù)字資產(chǎn)的安全至關(guān)重要。
- 加密解密速度
相對較慢:非對稱加密的加密和解密速度相對于對稱加密要慢得多,因?yàn)樗枰M(jìn)行更加復(fù)雜的數(shù)學(xué)計(jì)算。然而,這并不影響其在安全通信、身份驗(yàn)證等領(lǐng)域的應(yīng)用。
2.2 非對稱加密的用途
非對稱加密有兩個(gè)經(jīng)典使用場景。
- 加密:這是我們最為熟知的用法,就是公鑰加密,私鑰解密。
- 簽名:考慮到網(wǎng)絡(luò)不可信,數(shù)據(jù)在傳輸過程中可能被篡改,這個(gè)時(shí)候公私鑰可以反過來用,用私鑰對數(shù)據(jù)進(jìn)行簽名,公鑰進(jìn)行驗(yàn)簽,確保數(shù)據(jù)安全完整。
針對第二點(diǎn)用途,有的小伙伴會(huì)將之表述為用私鑰進(jìn)行加密,公鑰進(jìn)行解密,反正大伙知道說的是同一回事。
非對稱加密算法,盡管在理論上能夠用于數(shù)據(jù)加密和數(shù)字簽名,但在實(shí)踐中,其高計(jì)算復(fù)雜度和低效率成為了主要障礙。
相比對稱加密算法,非對稱加密的運(yùn)算速度要慢上幾個(gè)數(shù)量級(jí),這極大地影響了其處理大數(shù)據(jù)量的能力。此外,由于非對稱加密算法的加密和解密過程與密鑰長度緊密相關(guān),且不支持分組加密模式,導(dǎo)致它只能處理不超過密鑰長度的少量數(shù)據(jù),無法進(jìn)行大量數(shù)據(jù)的加密。
為了克服非對稱加密在性能上的不足,現(xiàn)代加密系統(tǒng)通常采用混合加密策略,即結(jié)合對稱加密和非對稱加密的優(yōu)點(diǎn)。在這種策略中,非對稱加密主要用于安全地傳輸一個(gè)對稱加密的密鑰(即“密鑰協(xié)商”)給另一方。一旦雙方安全地共享了這個(gè)對稱密鑰,就可以使用高效的對稱加密算法來加密和解密大量數(shù)據(jù)。這種結(jié)合使用的方法不僅提高了加密效率,還增強(qiáng)了通信的安全性,被廣泛應(yīng)用于各種安全通信協(xié)議中,如SSL/TLS。
在數(shù)字簽名領(lǐng)域,為了提升非對稱加密的效率也做了一些適配。具體做法是,首先對原始數(shù)據(jù)進(jìn)行摘要處理(可以利用 MD5、SHA 等),得到一個(gè)固定長度的摘要值。然后,使用非對稱加密算法對這個(gè)摘要值進(jìn)行加密,生成數(shù)字簽名。由于摘要算法能夠高效地將任意長度的數(shù)據(jù)壓縮為固定長度的摘要,因此不管原始數(shù)據(jù)多大,摘要數(shù)據(jù)長度都一樣,簽名過程也能保持高效。當(dāng)驗(yàn)證簽名時(shí),只需重新計(jì)算原始數(shù)據(jù)的摘要,并與解密后的簽名進(jìn)行比較,即可快速判斷數(shù)據(jù)是否被篡改。
2.3 加密案例
Java 代碼使用 RSA 加解密案例:
public class RsaDemo {
public static void main(String[] args) {
try {
KeyPair keyPair = generateRSAKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
String originalData = "Hello, 江南一點(diǎn)雨!";
String encryptedData = encrypt(publicKey, originalData);
String decryptedData = decrypt(privateKey, encryptedData);
System.out.println("加密后的數(shù)據(jù): " + encryptedData);
System.out.println("解密后的數(shù)據(jù): " + decryptedData);
} catch (Exception e) {
e.printStackTrace();
}
}
public static String decrypt(PrivateKey privateKey, String encryptedData) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
return new String(decryptedBytes);
}
public static String encrypt(PublicKey publicKey, String data) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] bytes = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(bytes);
}
public static KeyPair generateRSAKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048); // 可以指定密鑰長度,如2048位
return keyGen.generateKeyPair();
}
}2.4 前后端搭配
前端加密后端解密
后端代碼和上面案例中一致,不同的是,我們在拿到公鑰之后,可以將公鑰打印出來,這個(gè)公鑰將來要傳遞給前端:
public static void main(String[] args) {
try {
KeyPair keyPair = generateRSAKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
System.out.println("公鑰: " + Base64.getEncoder().encodeToString(publicKey.getEncoded()));
String originalData = "Hello, 江南一點(diǎn)雨!";
String encryptedData = encrypt(publicKey, originalData);
String decryptedData = decrypt(privateKey, encryptedData);
System.out.println("加密后的數(shù)據(jù): " + encryptedData);
System.out.println("解密后的數(shù)據(jù): " + decryptedData);
} catch (Exception e) {
e.printStackTrace();
}
}前端代碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RSA Encryption Example</title>
<!-- 引入jsencrypt庫 -->
<script src="https://cdn.jsdelivr.net/npm/jsencrypt@3.0.0-beta.1/bin/jsencrypt.min.js"></script>
</head>
<body>
<script>
// 重寫前端加密方法
function encryptData(publicKey, data) {
// 創(chuàng)建一個(gè)新的JSEncrypt對象
var encryptor = new JSEncrypt();
// 設(shè)置公鑰
encryptor.setPublicKey(publicKey);
// 加密數(shù)據(jù)
var encrypted = encryptor.encrypt(data);
return encrypted;
}
// 示例公鑰(實(shí)際使用時(shí)應(yīng)該替換為服務(wù)器提供的公鑰)
var publicKey = `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnP5FOXIghidLDE2FgFwzi4Y+qTdYjnc0eMoiL5i4gdOeTE+7Uu9YvDB51GcBWKD9lvmZr5rX8z0OBpwe526qsTekNpXoIVk9DB34US0HOrAXEEwpUTVuSR656cJGAwmWBkVaalQynEz4Dlzrm53zExVYueruYUuzFyuZDaQcFnl3rWUH/XDkCIe23z0R1TfT3Q2OYNKft0u56r0S/ko99utXuYJK9yowe7QGT6q4cSJwsITQTomCARAq9q+bSNuGEYa4FlYCKIKWIhKbMhz0FYIMB2fJN10GyZbbvKASqeMkuCoD2Efgd8/6uMwOaMcx9LgEkcFaQ3qgDutsPXNUswIDAQAB`;
// 要加密的數(shù)據(jù)
var data = 'Hello, javaboy!';
// 調(diào)用加密方法
var encryptedData = encryptData(publicKey, data);
// 輸出加密后的數(shù)據(jù)
console.log('Encrypted Data:', encryptedData);
</script>
</body>
</html>后端加密前端解密
在 Java 中使用 RSA 算法加密數(shù)據(jù),并在 JavaScript 中解密這些數(shù)據(jù),意味著服務(wù)端用前端的公鑰加密,前端用自己的私鑰解密,這種場景前端私鑰很容易被盜取,因此不推薦這種用法。我也就不舉例了。
好啦,又和小伙伴們聊了一遍對稱加密和非對稱加密,上面的案例代碼松哥都是測試通過的,小伙伴們可以作為參考。



































