目录
1. 前言:为何要在前端加密?
在传统的客户端-服务器交互中,用户在前端输入的敏感信息(如用户名、密码、信用卡号等)通常会以明文通过 HTTPS 提交到后台。即便在 HTTPS 保护下,仍有以下安全隐患:
- 前端漏洞:如果用户的浏览器或网络受到中间人攻击,可能篡改或窃取表单数据。虽然 HTTPS 可以避免网络监听,但存在一些复杂场景(如企业网络代理、根证书伪造等),会让 HTTPS 保护失效。
- 浏览器泄露:当用户在公用计算机或不安全环境下输入敏感数据,可能被浏览器插件劫持。
- 后端日志:如果后端在日志中意外记录了明文敏感信息,可能存在泄露风险。
- 合规需求:某些行业(如金融、医疗)要求即便在传输层使用 TLS,也要在应用层对敏感数据额外加密以符合法规。
因此,在前端对敏感数据进行一次对称加密(如 AES),并在后端对其解密,能够为安全防护增加一道“保险层”,即便数据在传输层被截获,也难以被攻击者直接获取明文。
**本指南将演示如何在 Vue 前端使用 CryptoJS 对数据(以登录密码为例)进行 AES 加密,并在 Java 后端使用 JCE(Java Cryptography Extension)对之解密验证。**整个流程清晰可见,适合初学者和中高级开发者参考。
2. CryptoJS 简介与安装配置
2.1 CryptoJS 主要功能概览
CryptoJS 是一套纯 JavaScript 实现的常用加密算法库,包含以下常见模块:
- 哈希函数:MD5、SHA1、SHA224、SHA256、SHA3 等
- 对称加密:AES、DES、TripleDES、RC4、Rabbit
- 编码方式:Base64、UTF-8、Hex、Latin1 等
- HMAC(Hash-based Message Authentication Code):HmacSHA1、HmacSHA256 等
由于 CryptoJS 纯前端可用,不依赖于 Node 内置模块,体积较小、使用方便,常用于浏览器环境的数据加密、签名和哈希操作。
2.2 在 Vue 中安装并引入 CryptoJS
安装 CryptoJS
在你的 Vue 项目根目录下执行:npm install crypto-js --save
或者使用 Yarn:
yarn add crypto-js
在组件中引入 CryptoJS
在需要进行加密操作的 Vue 组件中,引入相关模块。例如我们要使用 AES 对称加密,可写:
import CryptoJS from 'crypto-js';
如果只想单独引入 AES 相关模块以减小包体积,也可以:
import AES from 'crypto-js/aes'; import Utf8 from 'crypto-js/enc-utf8'; import Base64 from 'crypto-js/enc-base64';
这样打包后只会包含 AES、Utf8、Base64 模块,而不会附带其他算法。
配置示例(main.js 或组件中)
若希望在全局都可以使用CryptoJS
,可在main.js
中:import Vue from 'vue'; import CryptoJS from 'crypto-js'; Vue.prototype.$crypto = CryptoJS;
这样在任意组件中,可以通过
this.$crypto.AES.encrypt(...)
访问 CryptoJS 功能。不过出于清晰性,我们更建议在单个组件顶层直接import CryptoJS from 'crypto-js'
。
3. 前端加密实战:使用 AES 对称加密
为了最大程度地兼容性与安全性,我们采用 AES-256-CBC 模式对称加密。对称加密的特点是加密/解密使用同一个密钥(Key)与初始向量(IV),加密速度快,适合浏览器端。
3.1 AES 加密原理简述
- AES(Advanced Encryption Standard,高级加密标准)是一种分组密码算法,支持 128、192、256 位密钥长度。
- CBC 模式(Cipher Block Chaining):对每个分组与前一分组的密文进行异或运算,增强安全性。
对称加密的基本流程:
- 生成密钥(Key)与初始向量(IV):Key 一般为 32 字节(256 位),IV 长度为 16 字节(128 位)。
- 对明文进行 Padding:AES 分组长度为 16 字节,不足则填充(CryptoJS 默认使用 PKCS#7 填充)。
- 加密:For each block:
CipherText[i] = AES_Encrypt(PlainText[i] ⊕ CipherText[i-1])
,其中CipherText[0] = AES_Encrypt(PlainText[0] ⊕ IV)
。 - 输出密文:以 Base64 或 Hex 編码传输。
要在前端与后端一致地加解密,需约定相同的 Key、IV、Padding 及 编码方式。本例中,我们统一使用:
- Key:以 32 字节随机字符串(由后端与前端约定),使用 UTF-8 编码
- IV:以 16 字节随机字符串(也可以使用固定或随机 IV),使用 UTF-8 编码
- Padding:默认 PKCS#7
- 输出:Base64 编码
示例:
Key = '12345678901234567890123456789012' // 32 字节 IV = 'abcdefghijklmnop' // 16 字节
3.2 在 Vue 组件中编写 AES 加密函数
在 Vue 组件中,可将加密逻辑封装为一个方法,方便调用。以下示例演示如何使用 CryptoJS 对字符串进行 AES-256-CBC 加密并输出 Base64。
<script>
import CryptoJS from 'crypto-js';
export default {
name: 'EncryptExample',
data() {
return {
// 测试用明文
plaintext: 'Hello, Vue + Java 加密解密!',
// 32 字节(256 位)Key,前后端需保持一致
aesKey: '12345678901234567890123456789012',
// 16 字节(128 位)IV
aesIv: 'abcdefghijklmnop',
// 存放加密后 Base64 密文
encryptedText: ''
};
},
methods: {
/**
* 使用 AES-256-CBC 对 plaintext 进行加密,输出 Base64
*/
encryptAES(plain) {
// 将 Key 与 IV 转成 WordArray
const key = CryptoJS.enc.Utf8.parse(this.aesKey);
const iv = CryptoJS.enc.Utf8.parse(this.aesIv);
// 执行加密
const encrypted = CryptoJS.AES.encrypt(
CryptoJS.enc.Utf8.parse(plain),
key,
{
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
// encrypted.toString() 默认返回 Base64 编码
return encrypted.toString();
},
/**
* 测试加密流程
*/
doEncrypt() {
this.encryptedText = this.encryptAES(this.plaintext);
console.log('加密后的 Base64:', this.encryptedText);
}
},
mounted() {
// 示例:组件加载后自动加密一次
this.doEncrypt();
}
};
</script>
核心步骤:
CryptoJS.enc.Utf8.parse(...)
:将 UTF-8 字符串转为 CryptoJS 能识别的WordArray
(内部格式)。CryptoJS.AES.encrypt(messageWordArray, keyWordArray, { iv, mode, padding })
:执行加密。encrypted.toString()
:将加密结果以 Base64 字符串形式返回。
如果想输出 Hex 编码,可写 encrypted.ciphertext.toString(CryptoJS.enc.Hex)
;但后端也要对应以 Hex 解码。
3.3 示例代码:登录表单提交前加密
通常我们在登录时,只需对“密码”字段进行加密,其他表单字段(如用户名、验证码)可不加密。以下是一个完整的 Vue 登录示例:
<!-- src/components/Login.vue -->
<template>
<div class="login-container">
<h2>登录示例(前端 AES 加密)</h2>
<el-form :model="loginForm" ref="loginFormRef" label-width="80px">
<el-form-item label="用户名" prop="username" :rules="[{ required: true, message: '请输入用户名', trigger: 'blur' }]">
<el-input v-model="loginForm.username" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password" :rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]">
<el-input v-model="loginForm.password" type="password" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">登录</el-button>
</el-form-item>
</el-form>
<div v-if="encryptedPassword">
<h4>加密后密码(Base64):</h4>
<p class="cipher">{{ encryptedPassword }}</p>
</div>
</div>
</template>
<script>
import CryptoJS from 'crypto-js';
import axios from 'axios';
export default {
name: 'Login',
data() {
return {
loginForm: {
username: '',
password: ''
},
// 与后端约定的 Key 与 IV(示例)
aesKey: '12345678901234567890123456789012',
aesIv: 'abcdefghijklmnop',
encryptedPassword: ''
};
},
methods: {
/**
* 对密码进行 AES 加密,返回 Base64
*/
encryptPassword(password) {
const key = CryptoJS.enc.Utf8.parse(this.aesKey);
const iv = CryptoJS.enc.Utf8.parse(this.aesIv);
const encrypted = CryptoJS.AES.encrypt(
CryptoJS.enc.Utf8.parse(password),
key,
{
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
return encrypted.toString();
},
/**
* 表单提交事件
*/
handleSubmit() {
this.$refs.loginFormRef.validate(valid => {
if (!valid) return;
// 1. 对密码加密
const cipherPwd = this.encryptPassword(this.loginForm.password);
this.encryptedPassword = cipherPwd;
// 2. 组装参数提交给后端
const payload = {
username: this.loginForm.username,
password: cipherPwd // 将密文发送给后端
};
// 3. 发送 POST 请求
axios.post('/api/auth/login', payload)
.then(res => {
console.log('后端返回:', res.data);
this.$message.success('登录成功!');
})
.catch(err => {
console.error(err);
this.$message.error('登录失败!');
});
});
}
}
};
</script>
<style scoped>
.login-container {
width: 400px;
margin: 50px auto;
}
.cipher {
word-break: break-all;
background: #f5f5f5;
padding: 10px;
border: 1px dashed #ccc;
}
</style>
- 该示例使用了 Element-UI 的
el-form
、el-input
、el-button
组件,仅作演示。 encryptPassword
方法对loginForm.password
进行 AES 加密,并把 Base64 密文赋给encryptedPassword
(用于在页面上实时展示)。- 提交请求时,将
username
与加密后的password
一并 POST 到后端/api/auth/login
接口。后端收到密文后需要对其解密,才能比对数据库中的明文(或哈希)密码。
3.4 前端加密流程 ASCII 图解
┌────────────────────────────────────────┐
│ 用户输入表单 │
│ username: alice │
│ password: mySecret123 │
└──────────────┬─────────────────────────┘
│ 点击“登录”触发 handleSubmit()
▼
┌─────────────────────────────────────┐
│ 调用 encryptPassword('mySecret123') │
│ 1. keyWordArray = Utf8.parse(aesKey) │
│ 2. ivWordArray = Utf8.parse(aesIv) │
│ 3. encrypted = AES.encrypt( │
│ Utf8.parse(password), │
│ keyWordArray, │
│ { iv: ivWordArray, mode: CBC } │
│ ) │
│ 4. cipherText = encrypted.toString() │
└──────────────┬───────────────────────┘
│ 返回 Base64 密文
▼
┌─────────────────────────────────────┐
│ 组装 payload = { │
│ username: 'alice', │
│ password: 'U2FsdGVkX1...==' │
│ } │
└──────────────┬───────────────────────┘
│ axios.post('/api/auth/login', payload)
▼
┌─────────────────────────────────────┐
│ 发送 HTTPS POST 请求 (json) │
└─────────────────────────────────────┘
4. 后端解密实战:Java 中使用 JCE 解密
前端对数据进行了 AES-256-CBC 加密并以 Base64 格式发送到后端,Java 后端需要做以下几件事:
- 接收 Base64 密文字符串
- Base64 解码得到密文字节数组
- 使用与前端相同的 Key、IV 以及填充模式(PKCS5Padding,对应 PKCS7)进行 AES 解密
- 将解密后的字节数组转换为 UTF-8 明文
下面逐步演示在 Java(以 Spring Boot 为例)中如何解密。
4.1 Java 加密/解密基础(JCE)
Java 中的加密/解密 API 集中在 javax.crypto
包内,核心类包括:
Cipher
:加解密的核心类,指定算法/模式/填充方式后,可调用init()
、doFinal()
进行加密解密。SecretKeySpec
:用来将字节数组转换成对称密钥SecretKey
。IvParameterSpec
:用来封装初始化向量(IV)。Base64
:Java 8 内置的 Base64 编解码类(java.util.Base64
)。
对应 AES/CBC/PKCS5Padding 解密流程示例(伪代码):
// 1. 准备 Key 与 IV
byte[] keyBytes = aesKey.getBytes(StandardCharsets.UTF_8); // 32 字节
byte[] ivBytes = aesIv.getBytes(StandardCharsets.UTF_8); // 16 字节
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
// 2. Base64 解码密文
byte[] cipherBytes = Base64.getDecoder().decode(base64CipherText);
// 3. 初始化 Cipher
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
// 4. 执行解密
byte[] plainBytes = cipher.doFinal(cipherBytes);
// 5. 转为 UTF-8 字符串
String plaintext = new String(plainBytes, StandardCharsets.UTF_8);
注意:Java 默认使用 PKCS5Padding,而 CryptoJS 使用的是 PKCS7Padding。二者在实现上是兼容的,所以无需额外配置即可互通。
4.2 Java 后端引入依赖(Maven 配置)
如果你使用 Spring Boot,可在 pom.xml
中引入 Web 依赖即可,无需额外加密库,因为 JCE 已内置于 JDK。示例如下:
<!-- pom.xml -->
<project>
<!-- ... 省略其他配置 ... -->
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 如果需要 JSON 处理,Spring Boot 通常自带 Jackson -->
<!-- 直接使用 spring-boot-starter-web 即可 -->
</dependencies>
</project>
对于更早期的 JDK(如 JDK 7),若使用 AES-256 可能需要安装 JCE Unlimited Strength Jurisdiction Policy Files。不过从 JDK 8u161 开始,Unlimited Strength 已默认启用,无需额外安装。
4.3 Java 解密工具类示例
在 src/main/java/com/example/util/EncryptUtils.java
创建一个工具类 EncryptUtils
,封装 AES 解密方法:
package com.example.util;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class EncryptUtils {
/**
* 使用 AES/CBC/PKCS5Padding 对 Base64 编码的密文进行解密
*
* @param base64CipherText 前端加密后的 Base64 密文
* @param aesKey 与前端约定的 32 字节(256 位)Key
* @param aesIv 与前端约定的 16 字节 (128 位) IV
* @return 解密后的明文字符串
*/
public static String decryptAES(String base64CipherText, String aesKey, String aesIv) {
try {
// 1. 将 Base64 密文解码成字节数组
byte[] cipherBytes = Base64.getDecoder().decode(base64CipherText);
// 2. 准备 Key 和 IV
byte[] keyBytes = aesKey.getBytes(StandardCharsets.UTF_8);
byte[] ivBytes = aesIv.getBytes(StandardCharsets.UTF_8);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
// 3. 初始化 Cipher
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
// 4. 执行解密
byte[] plainBytes = cipher.doFinal(cipherBytes);
// 5. 转为字符串并返回
return new String(plainBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
return null; // 解密失败返回 null,可根据实际情况抛出异常
}
}
}
关键点说明:
aesKey.getBytes(StandardCharsets.UTF_8)
:将约定的 32 字节 Key 转为字节数组。Cipher.getInstance("AES/CBC/PKCS5Padding")
:指定 AES/CBC 模式,填充方式为 PKCS5Padding。SecretKeySpec
与IvParameterSpec
分别封装 Key 与 IV。cipher.doFinal(cipherBytes)
:执行真正的解密操作,返回明文字节数组。
4.4 Spring Boot Controller 示例接收并解密
以下示例展示如何在 Spring Boot Controller 中接收前端发送的 JSON 请求体,提取密文字段并调用 EncryptUtils.decryptAES(...)
解密,再与数据库中的明文/哈希密码进行比对。
// src/main/java/com/example/controller/AuthController.java
package com.example.controller;
import com.example.util.EncryptUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
// 与前端保持一致的 Key 与 IV
private static final String AES_KEY = "12345678901234567890123456789012"; // 32 字节
private static final String AES_IV = "abcdefghijklmnop"; // 16 字节
/**
* 登录接口:接收前端加密后的用户名 & 密码,解密后验证
*/
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Map<String, String> payload) {
String username = payload.get("username");
String encryptedPwd = payload.get("password");
// 1. 对密码进行解密
String plainPassword = EncryptUtils.decryptAES(encryptedPwd, AES_KEY, AES_IV);
if (plainPassword == null) {
return ResponseEntity.badRequest().body("解密失败");
}
// 2. TODO:在这里根据 username 从数据库查询用户信息,并比对明文密码或哈希密码
// 假设从数据库查出 storedPassword
String storedPassword = "mySecret123"; // 示例:实际项目中请使用哈希比对
if (plainPassword.equals(storedPassword)) {
// 验证通过
return ResponseEntity.ok("登录成功!");
} else {
return ResponseEntity.status(401).body("用户名或密码错误");
}
}
}
- 方法参数
@RequestBody Map<String, String> payload
:Spring 会自动将 JSON 转为Map
,其中username
对应用户输入的用户名,password
对应前端加密后的 Base64 密文。 - 成功解密后,得到明文密码
plainPassword
。在实际项目中,应将plainPassword
与数据库中存储的哈希密码(如 BCrypt 存储)比对,而不是直接明文比对。此处为了演示,假设数据库中存的是明文mySecret123
。
4.5 后端解密流程 ASCII 图解
Vue 前端发送请求:
POST /api/auth/login
Content-Type: application/json
{
"username": "alice",
"password": "U2FsdGVkX18Yr8...==" // Base64 AES-256-CBC 密文
}
│
▼
┌───────────────────────────────────────────────────────────┐
│ AuthController.login(@RequestBody payload) │
│ 1. username = payload.get("username") │
│ 2. encryptedPwd = payload.get("password") │
│ 3. 调用 EncryptUtils.decryptAES(encryptedPwd, AES_KEY, AES_IV) │
│ → Base64.decode → Cipher.init → doFinal() → 明文 bytes │
│ → 转字符串 plainPassword │
│ 4. 从数据库查出 storedPassword │
│ 5. plainPassword.equals(storedPassword) ? │
│ - 是:登录成功 │
│ - 否:用户名或密码错误 │
└───────────────────────────────────────────────────────────┘
5. 完整示例:从前端到后台的端到端流程
下面将前面零散的代码整合为一个“简单的登录Demo”,包括 Vue 端组件与 Java Spring Boot 后端示例,方便你实践一遍完整流程。
5.1 Vue 端示例组件:登录并加密提交
项目目录结构(前端)
vue-cryptojs-demo/
├── public/
│ └── index.html
├── src/
│ ├── App.vue
│ ├── main.js
│ └── components/
│ └── Login.vue
├── package.json
└── vue.config.js
src/components/Login.vue
<template>
<div class="login-container">
<h2>Vue + CryptoJS 登录示例</h2>
<el-form :model="loginForm" ref="loginFormRef" label-width="80px">
<el-form-item label="用户名" prop="username" :rules="[{ required: true, message: '请输入用户名', trigger: 'blur' }]">
<el-input v-model="loginForm.username" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password" :rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]">
<el-input v-model="loginForm.password" type="password" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">登录</el-button>
</el-form-item>
</el-form>
<div v-if="encryptedPassword" style="margin-top: 20px;">
<h4>加密后密码(Base64):</h4>
<p class="cipher">{{ encryptedPassword }}</p>
</div>
</div>
</template>
<script>
import CryptoJS from 'crypto-js';
import axios from 'axios';
export default {
name: 'Login',
data() {
return {
loginForm: {
username: '',
password: ''
},
// 与后端保持一致的 Key 与 IV
aesKey: '12345678901234567890123456789012', // 32 字节
aesIv: 'abcdefghijklmnop', // 16 字节
encryptedPassword: ''
};
},
methods: {
/**
* 对密码进行 AES/CBC/PKCS7 加密
*/
encryptPassword(password) {
const key = CryptoJS.enc.Utf8.parse(this.aesKey);
const iv = CryptoJS.enc.Utf8.parse(this.aesIv);
const encrypted = CryptoJS.AES.encrypt(
CryptoJS.enc.Utf8.parse(password),
key,
{
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
return encrypted.toString(); // Base64
},
/**
* 表单提交
*/
handleSubmit() {
this.$refs.loginFormRef.validate(valid => {
if (!valid) return;
// 1. 对密码加密
const cipherPwd = this.encryptPassword(this.loginForm.password);
this.encryptedPassword = cipherPwd;
// 2. 组装参数
const payload = {
username: this.loginForm.username,
password: cipherPwd
};
// 3. 发送请求到后端(假设后端地址为 http://localhost:8080)
axios.post('http://localhost:8080/api/auth/login', payload)
.then(res => {
this.$message.success(res.data);
})
.catch(err => {
console.error(err);
if (err.response && err.response.status === 401) {
this.$message.error('用户名或密码错误');
} else {
this.$message.error('登录失败,请稍后重试');
}
});
});
}
}
};
</script>
<style scoped>
.login-container {
width: 400px;
margin: 50px auto;
}
.cipher {
word-break: break-all;
background: #f5f5f5;
padding: 10px;
border: 1px dashed #ccc;
}
</style>
src/App.vue
<template>
<div id="app">
<Login />
</div>
</template>
<script>
import Login from './components/Login.vue';
export default {
name: 'App',
components: { Login }
};
</script>
<style>
body {
font-family: 'Arial', sans-serif;
}
</style>
src/main.js
import Vue from 'vue';
import App from './App.vue';
// 引入 Element-UI(可选)
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
Vue.config.productionTip = false;
new Vue({
render: h => h(App)
}).$mount('#app');
至此,前端示例部分完成。用户输入用户名和密码,点击“登录”后触发 handleSubmit()
,先加密密码并显示加密结果,再将加密后的密码与用户名一起以 JSON POST 到 Spring Boot 后端。
5.2 Java 后端示例:解密并校验用户名密码
项目目录结构(后端)
java-cryptojs-demo/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ ├── com/example/DemoApplication.java
│ │ │ ├── controller/AuthController.java
│ │ │ └── util/EncryptUtils.java
│ │ └── resources/
│ │ └── application.properties
└── pom.xml
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>java-cryptojs-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Java CryptoJS Demo</name>
<description>Spring Boot Demo for CryptoJS Decryption</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
</parent>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok(可选,用于简化日志) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Spring Boot Maven Plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
src/main/java/com/example/DemoApplication.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
src/main/java/com/example/util/EncryptUtils.java
package com.example.util;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class EncryptUtils {
/**
* 解密 Base64 AES 密文(AES/CBC/PKCS5Padding)
*
* @param base64CipherText 前端加密后的 Base64 编码密文
* @param aesKey 32 字节 Key
* @param aesIv 16 字节 IV
* @return 明文字符串 或 null(解密失败)
*/
public static String decryptAES(String base64CipherText, String aesKey, String aesIv) {
try {
// Base64 解码
byte[] cipherBytes = Base64.getDecoder().decode(base64CipherText);
// Key 与 IV
byte[] keyBytes = aesKey.getBytes(StandardCharsets.UTF_8);
byte[] ivBytes = aesIv.getBytes(StandardCharsets.UTF_8);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
// 初始化 Cipher
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
// 执行解密
byte[] plainBytes = cipher.doFinal(cipherBytes);
return new String(plainBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
src/main/java/com/example/controller/AuthController.java
package com.example.controller;
import com.example.util.EncryptUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
// 与前端保持一致的 Key 与 IV
private static final String AES_KEY = "12345678901234567890123456789012";
private static final String AES_IV = "abcdefghijklmnop";
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Map<String, String> payload) {
String username = payload.get("username");
String encryptedPwd = payload.get("password");
// 解密
String plainPassword = EncryptUtils.decryptAES(encryptedPwd, AES_KEY, AES_IV);
if (plainPassword == null) {
return ResponseEntity.badRequest().body("解密失败");
}
// TODO:在此处根据 username 查询数据库并校验密码
// 演示:假设用户名 alice,密码 mySecret123
if ("alice".equals(username) && "mySecret123".equals(plainPassword)) {
return ResponseEntity.ok("登录成功!");
} else {
return ResponseEntity.status(401).body("用户名或密码错误");
}
}
}
src/main/resources/application.properties
server.port=8080
启动后端:
mvn clean package java -jar target/java-cryptojs-demo-1.0.0.jar
后端将监听在
http://localhost:8080
,与前端的 Axios 请求保持一致。
6. 注意事项与最佳实践
6.1 密钥与 IV 的管理
切勿将 Key 明文硬编码在生产代码中
- 生产环境应通过更安全的方式管理密钥,例如从环境变量、Vault 服务或后端配置中心动态下发。
- 前端存储 Key 本身并不能完全保证安全,只是增加一次防护。如果前端 Key 泄露,攻击者依然可以伪造密文。
IV 的选择
- CBC 模式下 IV 应尽量随机生成,保证同一明文多次加密输出不同密文,从而增强安全性。
- 在示例中,我们使用了固定 IV 便于演示与调试。在生产中,建议每次随机生成 IV,并将 IV 与密文一起发送给后端(例如将 IV 放在密文前面,Base64 编码后分割)。
示例:
// 前端随机生成 16 字节 IV const ivRandom = CryptoJS.lib.WordArray.random(16); const encrypted = CryptoJS.AES.encrypt( CryptoJS.enc.Utf8.parse(plainPassword), key, { iv: ivRandom, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ); // 将 IV 与密文一起拼接:iv + encrypted.toString() const result = ivRandom.toString(CryptoJS.enc.Base64) + ':' + encrypted.toString();
后端解密时,需先从
result
中解析出 Base64 IV 和 Base64 Ciphertext,分别解码后调用 AES 解密。Key 的长度与格式
- AES-256 要求 Key 长度为 32 字节,AES-128 则要求 Key 长度为 16 字节。可根据需求选择。
- 请使用 UTF-8 编码来生成字节数组。若 Key 包含非 ASCII 字符,务必保持前后端编码一致。
6.2 数据完整性与签名
对称加密只能保证机密性(confidentiality),即对手无法从密文恢复明文,但并不能保证数据在传输过程中未被篡改。为此,可在密文外层再加一层签名(HMAC)或摘要校验(SHA256):
计算 HMAC-SHA256:
- 在发送密文
cipherText
之外,前端对cipherText
使用 HMAC-SHA256 计算签名signature = HMAC_SHA256(secretSignKey, cipherText)
。 - 将
{ cipherText, signature }
一并发送给后台。 - 后端收到后,先用相同的
secretSignKey
对cipherText
计算 HMAC 并比对signature
,确保密文未被中间篡改,再做 AES 解密。
- 在发送密文
代码示例(前端):
import CryptoJS from 'crypto-js'; // 1. 计算签名 const signature = CryptoJS.HmacSHA256(cipherText, signKey).toString(); // 2. 最终 payload const payload = { username: 'alice', password: cipherText, sign: signature };
代码示例(后端):
// 1. 接收 cipherText 与 sign String cipherText = payload.get("password"); String sign = payload.get("sign"); // 2. 使用相同的 signKey 计算 HMAC-SHA256 Mac hmac = Mac.getInstance("HmacSHA256"); hmac.init(new SecretKeySpec(signKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] computed = hmac.doFinal(cipherText.getBytes(StandardCharsets.UTF_8)); String computedSign = Base64.getEncoder().encodeToString(computed); if (!computedSign.equals(sign)) { return ResponseEntity.status(400).body("签名校验失败"); } // 3. 通过签名校验后再解密 String plainPassword = EncryptUtils.decryptAES(cipherText, AES_KEY, AES_IV);
这样,前端加密完的数据在传输过程中不仅是机密的,还保证了完整性与防篡改。
6.3 前端加密的局限性
Key 暴露风险
- 前端的 Key 无法完全保密,只要用户手里有源码或在浏览器控制台调试,就能看到 Key。真正的机密管理应在后端完成。
- 前端加密更多是一种“次级防护”,用于防止简单的明文泄露,而非替代后端安全机制。
仅防止明文泄露,并不防止重放攻击
如果攻击者截获了合法密文,仍可直接“重放”该密文来进行登录尝试。解决方法:
- 在加密前插入时间戳、随机数(nonce)等参数,并在后端验证这些参数是否过期或是否已使用。
- 结合 HMAC 签名,确保每次请求的签名必须与时间戳/随机数一致。
兼容性与浏览器支持
- CryptoJS 纯 JavaScript 实现,对大多数现代浏览器兼容良好,但在极老旧浏览器可能性能较差。
- 如果对性能要求更高,可考虑使用 Web Crypto API(仅限现代浏览器),但兼容性不如 CryptoJS 广泛。
7. 总结
本文全面介绍了如何在 Vue 前端使用 CryptoJS 进行 AES 对称加密,并在 Java 后端使用 JCE 进行解密的端到端流程。涵盖内容包括:
- 前端加密动机:为何要在传输层之外再额外加密敏感数据。
- CryptoJS 介绍与安装:如何在 Vue 项目中引入并使用 CryptoJS 进行 AES 加密。
- 前端加密示例:详细讲解 AES/CBC/PKCS7 加密流程及代码示例,演示登录时对密码加密提交。
- 后端解密详解:基于 JCE 的 AES/CBC/PKCS5Padding 解密实现,并在 Spring Boot Controller 中演示如何接收并验证。
- 完整示例:提供 Vue 端组件与 Java 后端示例,展示实际运行效果。
- 注意事项与最佳实践:包括密钥和 IV 管理、数据完整性签名、防重放攻击,以及前端加密局限性等。
通过本文,你可以快速上手在 Vue 与 Java 环境下实现安全的对称加密与解密,提升敏感数据传输的安全性。当然,在实际生产环境中,还应结合更完善的认证授权、HTTPS/TLS、Token 签名等方案,共同构筑更高强度的安全防线。