下面是一个完整的Vue3+TypeScript前端AES加密与Spring Boot后端AES解密的示例:
一、前端 Vue3 + TypeScript 代码
-
安装依赖:
bash
复制
下载
npm install crypto-js
-
创建加密工具文件
src/utils/crypto.ts
:
typescript
复制
下载
import CryptoJS from 'crypto-js' // AES加密配置 const AES_KEY = CryptoJS.enc.Utf8.parse('1234567890abcdef1234567890abcdef') // 32位密钥 const AES_IV = CryptoJS.enc.Utf8.parse('abcdef1234567890') // 16位偏移量 /** * AES加密 * @param data 待加密数据 * @returns 加密后的Base64字符串 */ export function encryptAES(data: any): string { const dataStr = JSON.stringify(data) const encrypted = CryptoJS.AES.encrypt(dataStr, AES_KEY, { iv: AES_IV, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }) return encrypted.toString() } /** * 发送加密数据 * @param url 请求地址 * @param data 请求数据 */ export async function sendEncryptedData(url: string, data: any) { const encrypted = encryptAES(data) try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Encrypted': 'true' // 添加加密标识头 }, body: JSON.stringify({ data: encrypted }) }) if (!response.ok) throw new Error('Network response was not ok') return await response.json() } catch (error) { console.error('Encrypted request failed:', error) throw error } }
-
在组件中使用:
vue
复制
下载
<script setup lang="ts"> import { ref } from 'vue' import { sendEncryptedData } from '@/utils/crypto' const formData = ref({ username: 'testUser', password: 'secret123', sensitiveInfo: 'This is confidential' }) async function submitData() { try { const response = await sendEncryptedData( 'http://your-backend-api.com/encrypted-endpoint', formData.value ) console.log('Server response:', response) } catch (e) { console.error('Submission error:', e) } } </script> <template> <div> <button @click="submitData">发送加密数据</button> </div> </template>
二、后端 Spring Boot 代码
-
添加依赖到
pom.xml
:
xml
复制
下载
运行
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
-
创建AES工具类
src/main/java/com/example/demo/util/AesUtil.java
:
java
复制
下载
import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public class AesUtil { private static final String KEY = "1234567890abcdef1234567890abcdef"; // 32字符 private static final String IV = "abcdef1234567890"; // 16字符 private static final String ALGORITHM = "AES/CBC/PKCS5Padding"; public static String decrypt(String encryptedData) throws Exception { byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData); SecretKeySpec secretKey = new SecretKeySpec(KEY.getBytes(), "AES"); IvParameterSpec ivParameterSpec = new IvParameterSpec(IV.getBytes()); Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec); byte[] decryptedBytes = cipher.doFinal(encryptedBytes); return new String(decryptedBytes); } }
-
创建控制器
src/main/java/com/example/demo/controller/EncryptedController.java
:
java
复制
下载
import com.example.demo.util.AesUtil; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @RestController public class EncryptedController { @PostMapping("/encrypted-endpoint") public Map<String, Object> handleEncryptedRequest( @RequestBody Map<String, String> payload, @RequestHeader(value = "X-Encrypted", required = false) String encryptedFlag) { try { if (!"true".equalsIgnoreCase(encryptedFlag)) { throw new SecurityException("Unencrypted request not allowed"); } String encryptedData = payload.get("data"); String decryptedJson = AesUtil.decrypt(encryptedData); // 将解密后的JSON转为Map // 实际项目中应使用ObjectMapper解析为具体对象 System.out.println("Decrypted data: " + decryptedJson); return Map.of( "status", "success", "decryptedData", decryptedJson ); } catch (Exception e) { throw new SecurityException("Decryption failed: " + e.getMessage()); } } }
-
配置跨域支持(可选)
src/main/java/com/example/demo/config/WebConfig.java
:
java
复制
下载
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:5173") // 前端地址 .allowedMethods("*"); } }
三、关键配置说明
-
AES参数一致性:
-
密钥:32字节(256位)
1234567890abcdef1234567890abcdef
-
IV:16字节(128位)
abcdef1234567890
-
模式:CBC
-
填充:PKCS7(前端)/ PKCS5(后端实际等效)
-
-
数据传输流程:
图表
代码
下载
后端前端后端前端1. JSON序列化数据2. AES-CBC加密3. POST { data: "加密Base64" }4. 检查X-Encrypted头5. Base64解码6. AES解密7. 处理原始数据8. 返回响应
-
安全注意事项:
-
生产环境应将密钥存储在环境变量/配置中心,而非代码中
-
考虑使用HTTPS防止中间人攻击
-
可添加请求签名防止重放攻击
-
Java需确保JCE无限强度权限(JDK8需手动安装)
-
四、测试流程
-
启动Spring Boot应用
-
运行Vue应用(默认端口5173)
-
点击前端"发送加密数据"按钮
-
后端控制台应打印解密后的原始JSON数据
注意:如果遇到Java解密失败,检查:
密钥/IV长度是否正确
是否添加了JCE无限强度策略文件
前端加密后的Base64字符串是否完整传输
确保前后端都使用相同的AES模式(CBC)
这个方案实现了前后端之间的安全数据传输,保护敏感信息在传输过程中的机密性。
在后端解密后出现额外的双引号,是因为前端加密时对数据进行了 JSON 序列化处理。以下是解决方案和修改后的代码:
问题分析
当前流程:
图表
代码
下载
前端对象
JSON.stringify
AES加密
后端接收
AES解密
JSON字符串带双引号
我们需要将解密后的 JSON 字符串解析为 Java 对象
后端修改方案
1. 添加 Jackson 依赖(如果尚未添加)
pom.xml
:
xml
复制
下载
运行
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> <!-- 使用最新版本 --> </dependency>
2. 修改控制器代码
EncryptedController.java
:
java
复制
下载
import com.example.demo.util.AesUtil; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @RestController public class EncryptedController { private final ObjectMapper objectMapper = new ObjectMapper(); // 创建接收数据的DTO类 public static class EncryptedDataDTO { private String username; private String password; private String sensitiveInfo; // 必须有无参构造函数 public EncryptedDataDTO() {} // Getters and Setters public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getSensitiveInfo() { return sensitiveInfo; } public void setSensitiveInfo(String sensitiveInfo) { this.sensitiveInfo = sensitiveInfo; } } @PostMapping("/encrypted-endpoint") public Map<String, Object> handleEncryptedRequest( @RequestBody Map<String, String> payload, @RequestHeader(value = "X-Encrypted", required = false) String encryptedFlag) { try { if (!"true".equalsIgnoreCase(encryptedFlag)) { throw new SecurityException("Unencrypted request not allowed"); } String encryptedData = payload.get("data"); String decryptedJson = AesUtil.decrypt(encryptedData); // 将JSON字符串解析为Java对象 EncryptedDataDTO data = objectMapper.readValue(decryptedJson, EncryptedDataDTO.class); System.out.println("用户名: " + data.getUsername()); System.out.println("密码: " + data.getPassword()); System.out.println("敏感信息: " + data.getSensitiveInfo()); return Map.of( "status", "success", "username", data.getUsername(), "received", true ); } catch (Exception e) { throw new SecurityException("处理失败: " + e.getMessage()); } } }
替代方案:直接操作字符串(不推荐)
如果你坚持要获取原始字符串(不推荐,因为会丢失数据结构):
java
复制
下载
// 在解密后添加字符串处理 String decryptedJson = AesUtil.decrypt(encryptedData); // 移除两端的双引号(脆弱的方法,仅当确定字符串格式时使用) if (decryptedJson.startsWith("\"") && decryptedJson.endsWith("\"")) { decryptedJson = decryptedJson.substring(1, decryptedJson.length() - 1); } System.out.println("处理后的字符串: " + decryptedJson);
注意:这种方法非常脆弱,因为:
-
如果字符串本身包含双引号会被破坏
-
无法正确处理嵌套数据结构
-
失去类型安全和结构化优势
推荐方案总结
方案 | 优点 | 缺点 |
---|---|---|
JSON 反序列化 | 类型安全 结构化数据处理 自动类型转换 | 需要创建DTO类 需要配置反序列化 |
字符串操作 | 简单直接 | 脆弱易出错 丢失数据结构 难以维护 |
完整流程说明
图表
代码
下载
后端前端后端前端1. 准备数据对象2. JSON.stringify(对象)3. AES加密字符串4. 发送 { data: "加密数据" }5. AES解密获得JSON字符串6. ObjectMapper解析JSON7. 获得Java对象(无多余引号)8. 返回处理结果
前端配合建议
确保前端发送的是有效的JSON对象:
typescript
复制
下载
// 正确 - 对象会被正确序列化 const data = { username: 'test', password: 'secret' } // 错误 - 字符串会被双重序列化 const data = JSON.stringify({ ... }) // 不要这样做!
使用推荐方案后,后端将能直接获取到:
java
复制
下载
data.getUsername() // = "test" (字符串,无额外引号) data.getPassword() // = "secret" (字符串,无额外引号)
这样处理后,你将获得干净的结构化数据,无需手动处理双引号问题。
在生产环境中安全存储加密密钥至关重要。以下是不将密钥硬编码在代码中的推荐方案,按安全性从低到高排序:
1. 环境变量(基础方案)
适用场景:中小型项目,快速实现
前端实现(Vue3)
typescript:src/utils/crypto.ts
复制
下载
// 从环境变量读取密钥(注意:前端环境变量是公开的!) const AES_KEY = CryptoJS.enc.Utf8.parse(import.meta.env.VITE_AES_KEY || 'default_key_32bytes_xxxxxxxxxxxx'); const AES_IV = CryptoJS.enc.Utf8.parse(import.meta.env.VITE_AES_IV || 'default_iv_16bytes_xx');
.env
文件(添加到 .gitignore
):
env:.env.local
复制
下载
VITE_AES_KEY=1234567890abcdef1234567890abcdef VITE_AES_IV=abcdef1234567890
后端实现(Spring Boot)
application.yml
:
yaml
复制
下载
aes: key: ${AES_KEY:1234567890abcdef1234567890abcdef} # 默认值仅用于开发 iv: ${AES_IV:abcdef1234567890}
修改 AesUtil.java
:
java
复制
下载
@Component public class AesUtil { private final String key; private final String iv; @Autowired public AesUtil( @Value("${aes.key}") String key, @Value("${aes.iv}") String iv ) { this.key = key; this.iv = iv; } public String decrypt(String encryptedData) throws Exception { // 使用 this.key 和 this.iv } }
启动方式:
bash
复制
下载
# Linux/macOS export AES_KEY=prod_key_32bytes_xxxxxxxxxxxx export AES_IV=prod_iv_16bytes_xxxx java -jar your-app.jar # Windows set AES_KEY=prod_key_32bytes_xxxxxxxxxxxx set AES_IV=prod_iv_16bytes_xxxx java -jar your-app.jar
2. 配置文件加密(中级方案)
适用场景:需要更安全的配置存储
使用 jasypt
加密配置文件:
-
添加依赖:
xml:pom.xml
复制
下载
<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.5</version> </dependency>
-
加密密钥:
bash
复制
下载
# 安装 jasypt (需要 Java) java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI \ input="real_secret_key" password=master_password algorithm=PBEWithMD5AndDES # 输出:ENC(加密后的字符串)
-
配置
application.yml
:
yaml
复制
下载
aes: key: ENC(密文1) # 替换为加密后的值 iv: ENC(密文2) jasypt: encryptor: password: ${JASYPT_PASSWORD:} # 从环境变量获取主密码
-
启动应用:
bash
复制
下载
export JASYPT_PASSWORD=master_password java -jar your-app.jar
3. 密钥管理服务(高级方案)
适用场景:大型企业级应用,最高安全要求
使用 HashiCorp Vault
图表
代码
下载
应用
Vault Agent
HashiCorp Vault
安全存储
-
添加依赖:
xml:pom.xml
复制
下载
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-vault-config</artifactId> <version>3.1.0</version> </dependency>
-
配置
bootstrap.yml
:
yaml
复制
下载
spring: cloud: vault: uri: https://vault.example.com:8200 authentication: TOKEN token: ${VAULT_TOKEN} kv: enabled: true backend: secret application-name: my-app
-
修改
AesUtil
:
java
复制
下载
@Configuration public class AesUtil { private final String key; private final String iv; public AesUtil( @Value("${aes.key}") String key, @Value("${aes.iv}") String iv ) { this.key = key; this.iv = iv; } }
-
在Vault中存储密钥:
bash
复制
下载
vault kv put secret/my-app aes.key="prod_key" aes.iv="prod_iv"
云服务商方案
云平台 | 服务 | 特点 |
---|---|---|
AWS | Secrets Manager | 自动轮换密钥,与IAM集成 |
Azure | Key Vault | 硬件安全模块(HSM)支持 |
GCP | Secret Manager | 版本控制,审计日志 |
4. 硬件安全模块(HSM)(最高安全)
适用场景:金融、政府等高安全要求场景
图表
代码
下载
硬件安全模块应用硬件安全模块应用加密请求(明文)密文解密请求(密文)明文
实现方式:
-
使用JCA(Java Cryptography Architecture)接口
-
集成HSM厂商提供的SDK
-
密钥永不离开HSM设备
前端安全特别注意事项
重要:前端无法完全隐藏密钥!所有客户端代码都是公开的。解决方案:
-
使用非对称加密:
图表
代码
下载
公钥加密
私钥解密
前端
后端
-
前端:RSA/ECC加密对称密钥
-
后端:用私钥解密获取对称密钥
-
-
代理层解决方案:
图表
代码
下载
注入密钥
客户端
安全代理
前端
后端
-
通过安全代理动态注入密钥
-
定期轮换密钥
-
-
短期令牌方案:
-
后端生成短期有效的加密令牌
-
前端使用时提交令牌获取临时密钥
-
密钥管理最佳实践
-
密钥轮换:
图表
代码
下载
2023-01-012023-02-012023-03-012023-04-012023-05-012023-06-01生产密钥每日自动生成新密钥主密钥数据密钥密钥轮换周期
-
最小权限原则:
-
应用只能访问必要的密钥
-
不同环境使用不同密钥
-
-
审计与监控:
-
记录所有密钥访问
-
异常访问实时告警
-
-
密钥存储:
-
禁用开发密钥在生产环境
-
使用密钥版本控制
-
定期备份密钥(加密存储)
-
推荐实施路径
图表
代码
下载
是
否
是
否
项目启动
环境变量
安全要求提高?
配置文件加密
保持
企业级需求?
密钥管理服务
HSM
根据项目规模和安全要求:
-
初创项目:环境变量 + 严格访问控制
-
中型项目:Jasypt加密配置 + 密钥轮换
-
大型企业:Vault/AWS KMS + 审计日志
-
金融级:HSM + 硬件隔离
始终记住:安全是一个过程,不是一次性配置。定期审计密钥使用情况,及时更新安全措施至关重要。