Java 与 JavaScript 加密解密实现指南

前言

在前后端分离的应用中,为了保护敏感信息,常常需要对数据进行加密传输。本文将详细介绍如何使用 Java 的 Cipher 类和 JavaScript 的 crypto-js 库实现前后端的加密和解密功能,确保数据传输的安全性。

一、前端实现(使用 crypto-js)

1. 安装 crypto-js

1
2
3
4
5
# 使用 npm
npm install crypto-js

# 使用 yarn
yarn add crypto-js

2. 加密实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import CryptoJS from 'crypto-js';

/**
* AES 加密
* @param {string} plaintext - 明文
* @param {string} key - 密钥(16位)
* @param {string} iv - 初始化向量(16位)
* @returns {string} 加密后的密文(Base64 编码)
*/
function aesEncrypt(plaintext, key, iv) {
const keyHex = CryptoJS.enc.Utf8.parse(key);
const ivHex = CryptoJS.enc.Utf8.parse(iv);
const encrypted = CryptoJS.AES.encrypt(plaintext, keyHex, {
iv: ivHex,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}

/**
* AES 解密
* @param {string} ciphertext - 密文(Base64 编码)
* @param {string} key - 密钥(16位)
* @param {string} iv - 初始化向量(16位)
* @returns {string} 解密后的明文
*/
function aesDecrypt(ciphertext, key, iv) {
const keyHex = CryptoJS.enc.Utf8.parse(key);
const ivHex = CryptoJS.enc.Utf8.parse(iv);
const decrypted = CryptoJS.AES.decrypt(ciphertext, keyHex, {
iv: ivHex,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return decrypted.toString(CryptoJS.enc.Utf8);
}

// 使用示例
const key = '1234567890123456'; // 16位密钥
const iv = '1234567890123456'; // 16位初始化向量
const plaintext = 'Hello, World!';

const ciphertext = aesEncrypt(plaintext, key, iv);
console.log('加密后:', ciphertext);

const decryptedText = aesDecrypt(ciphertext, key, iv);
console.log('解密后:', decryptedText);

二、后端实现(使用 Java Cipher)

1. 依赖

如果使用 Maven,需要添加以下依赖:

1
2
3
4
5
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>

2. 工具类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

public class AESUtils {

/**
* AES 加密
* @param plaintext 明文
* @param key 密钥,必须为16、24或32个字符组成
* @param iv 初始化向量,必须为16个字符组成
* @return 加密后的密文(Base64编码)
* @throws Exception 加密异常
*/
public static String aesEncrypt(String plaintext, String key, String iv) throws Exception {
if (StringUtils.isEmpty(plaintext) || StringUtils.isEmpty(key) || StringUtils.isEmpty(iv)) {
return null;
}

// 创建密钥和初始化向量
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));

// 初始化Cipher对象
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);

// 执行加密
byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));

// 返回Base64编码的密文
return Base64.encodeBase64String(encryptedBytes);
}

/**
* AES 解密
* @param ciphertext 密文(Base64编码)
* @param key 密钥,必须为16、24或32个字符组成
* @param iv 初始化向量,必须为16个字符组成
* @return 解密后的明文
* @throws Exception 解密异常
*/
public static String aesDecrypt(String ciphertext, String key, String iv) throws Exception {
if (StringUtils.isEmpty(ciphertext) || StringUtils.isEmpty(key) || StringUtils.isEmpty(iv)) {
return null;
}

// 解码Base64密文
byte[] encryptedBytes = Base64.decodeBase64(ciphertext);

// 创建密钥和初始化向量
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));

// 初始化Cipher对象
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);

// 执行解密
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);

// 返回明文
return new String(decryptedBytes, StandardCharsets.UTF_8);
}

/**
* 生成随机密钥
* @param length 密钥长度(16、24或32)
* @return 随机密钥
*/
public static String generateRandomKey(int length) {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder key = new StringBuilder();
for (int i = 0; i < length; i++) {
int index = (int) (Math.random() * chars.length());
key.append(chars.charAt(index));
}
return key.toString();
}
}

3. 使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main(String[] args) throws Exception {
// 密钥和初始化向量(实际应用中应该从配置文件或环境变量中获取)
String key = "1234567890123456";
String iv = "1234567890123456";
String plaintext = "Hello, World!";

// 加密
String ciphertext = AESUtils.aesEncrypt(plaintext, key, iv);
System.out.println("加密后: " + ciphertext);

// 解密
String decryptedText = AESUtils.aesDecrypt(ciphertext, key, iv);
System.out.println("解密后: " + decryptedText);
}
}

三、Cipher 类详解

1. 获取 Cipher 实例

1
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

参数格式为:算法/模式/填充模式,常用的组合有:

  • AES 算法

    • AES/CBC/NoPadding (128)
    • AES/CBC/PKCS5Padding (128)
    • AES/ECB/NoPadding (128)
    • AES/ECB/PKCS5Padding (128)
  • DES 算法

    • DES/CBC/NoPadding (56)
    • DES/CBC/PKCS5Padding (56)
    • DES/ECB/NoPadding (56)
    • DES/ECB/PKCS5Padding (56)
  • DESede (3DES) 算法

    • DESede/CBC/NoPadding (168)
    • DESede/CBC/PKCS5Padding (168)
    • DESede/ECB/NoPadding (168)
    • DESede/ECB/PKCS5Padding (168)
  • RSA 算法

    • RSA/ECB/PKCS1Padding (1024, 2048)
    • RSA/ECB/OAEPWithSHA-1AndMGF1Padding (1024, 2048)
    • RSA/ECB/OAEPWithSHA-256AndMGF1Padding (1024, 2048)

2. 初始化 Cipher

1
cipher.init(int opmode, Key key, AlgorithmParameterSpec params);
  • opmode:操作模式,如 Cipher.ENCRYPT_MODE(加密)或 Cipher.DECRYPT_MODE(解密)
  • key:密钥,可以通过以下方式创建:
    • SecretKeySpec:直接使用字节数组创建密钥
    • KeyGenerator:生成随机密钥
    • KeyPairGenerator:用于RSA算法生成密钥对
  • params:算法参数,CBC模式下需要传入 IvParameterSpec 对象

3. 填充模式说明

  • NoPadding:不填充,要求明文长度必须是块大小的整数倍
  • PKCS5Padding:最常用的填充方式,填充长度为 1-8 字节
  • PKCS7Padding:与 PKCS5Padding 类似,填充长度为 1-块大小字节

四、完整的前后端加密示例

1. 前端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// frontend.js
import CryptoJS from 'crypto-js';

// 加密函数
function encryptData(data, key, iv) {
const keyHex = CryptoJS.enc.Utf8.parse(key);
const ivHex = CryptoJS.enc.Utf8.parse(iv);
const encrypted = CryptoJS.AES.encrypt(JSON.stringify(data), keyHex, {
iv: ivHex,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}

// 解密函数
function decryptData(ciphertext, key, iv) {
const keyHex = CryptoJS.enc.Utf8.parse(key);
const ivHex = CryptoJS.enc.Utf8.parse(iv);
const decrypted = CryptoJS.AES.decrypt(ciphertext, keyHex, {
iv: ivHex,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8));
}

// 示例:发送加密数据到后端
async function sendEncryptedData() {
const key = '1234567890123456'; // 实际应用中应该从安全的地方获取
const iv = '1234567890123456'; // 实际应用中应该随机生成

const data = {
username: 'user123',
password: 'password123',
sensitiveInfo: 'secret data'
};

const encryptedData = encryptData(data, key, iv);

try {
const response = await fetch('/api/encrypted', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
encryptedData: encryptedData,
iv: iv // 实际应用中应该通过安全通道传输
})
});

const result = await response.json();
console.log('Response:', result);
} catch (error) {
console.error('Error:', error);
}
}

2. 后端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// EncryptedController.java
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class EncryptedController {

// 实际应用中应该从配置文件或环境变量中获取
private static final String SECRET_KEY = "1234567890123456";

@PostMapping("/api/encrypted")
public ResponseEntity<?> handleEncryptedData(@RequestBody EncryptedRequest request) {
try {
// 解密数据
String decryptedData = AESUtils.aesDecrypt(
request.getEncryptedData(),
SECRET_KEY,
request.getIv()
);

// 解析解密后的数据
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> data = objectMapper.readValue(decryptedData, Map.class);

// 处理业务逻辑
System.out.println("Received data: " + data);

// 准备响应数据
Map<String, Object> responseData = new HashMap<>();
responseData.put("status", "success");
responseData.put("message", "Data received and decrypted successfully");

// 加密响应数据
String encryptedResponse = AESUtils.aesEncrypt(
objectMapper.writeValueAsString(responseData),
SECRET_KEY,
request.getIv()
);

return ResponseEntity.ok(Map.of("encryptedResponse", encryptedResponse));
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(500).body(Map.of("error", "Decryption failed"));
}
}

static class EncryptedRequest {
private String encryptedData;
private String iv;

// getters and setters
}
}

五、安全性最佳实践

  1. 密钥管理

    • 不要硬编码密钥,应该从配置文件或环境变量中获取
    • 定期轮换密钥
    • 使用密钥管理服务(如 AWS KMS、HashiCorp Vault 等)
  2. 初始化向量 (IV)

    • 每次加密都应该使用随机生成的 IV
    • IV 不需要保密,但需要与密文一起传输
    • IV 长度必须与加密算法的块大小相同
  3. 算法选择

    • 使用 AES-256 而不是 DES 或 3DES
    • 优先使用 CBC 模式而不是 ECB 模式
    • 使用 PKCS7Padding 填充模式
  4. 传输安全

    • 始终使用 HTTPS 传输加密数据
    • 考虑使用 TLS 1.3
    • 实现请求签名验证
  5. 防止重放攻击

    • 在加密数据中包含时间戳
    • 实现请求非ce机制
  6. 错误处理

    • 不要在错误消息中泄露敏感信息
    • 实现统一的错误处理机制

六、常见问题及解决方案

1. 加密和解密结果不一致

原因

  • 密钥长度不匹配
  • 初始化向量不一致
  • 编码方式不同
  • 填充模式不匹配

解决方案

  • 确保前后端使用相同的密钥长度
  • 确保前后端使用相同的初始化向量
  • 统一使用 UTF-8 编码
  • 确保前后端使用相同的填充模式

2. 密钥管理困难

解决方案

  • 使用环境变量存储密钥
  • 实现密钥轮换机制
  • 考虑使用密钥管理服务

3. 性能问题

解决方案

  • 只对敏感数据进行加密
  • 考虑使用对称加密算法(如 AES)
  • 实现加密结果缓存

七、参考资料

  1. Java Cipher 官方文档
  2. CryptoJS 官方文档
  3. AES 加密算法详解
  4. https://www.cnblogs.com/caizhaokai/p/10944667.html

八、总结

通过本文的介绍,你应该已经掌握了如何使用 Java 的 Cipher 类和 JavaScript 的 crypto-js 库实现前后端的加密和解密功能。在实际应用中,你需要根据具体的业务需求和安全要求,选择合适的加密算法和参数,确保数据传输的安全性。

记住,加密只是安全的一部分,你还需要考虑其他安全措施,如访问控制、认证授权、输入验证等,以构建一个全面的安全体系。