实战封装加密工具类

时游大约 3 分钟

实战:封装加密工具类(URL 传参与登录密码场景)

场景 1:URL 传参防篡改

路由跳转携带敏感参数(如 ?userId=1&orderId=100),直接暴露容易被猜测和篡改。思路:AES 加密参数 → Base64 → URL 安全处理,接收方解密还原。

utils/crypto.js(完整工具类)

import CryptoJS from "crypto-js";

// 密钥与 IV:前后端约定,16 字节(128 位 AES);生产环境不要硬编码在前端包里,
// 更稳妥的做法是接口下发或走 HTTPS + 服务端签名
const SECRET_KEY = CryptoJS.enc.Utf8.parse("1234567890abcdef");
const SECRET_IV = CryptoJS.enc.Utf8.parse("abcdef1234567890");

/**
 * AES 加密(CBC 模式,PKCS7 填充),输出 Base64
 * 固定 key+iv,同一明文密文相同,方便作为 URL 参数还原
 */
export function encrypt(plainText) {
	if (!plainText) return "";
	const encrypted = CryptoJS.AES.encrypt(
		CryptoJS.enc.Utf8.parse(String(plainText)),
		SECRET_KEY,
		{
			iv: SECRET_IV,
			mode: CryptoJS.mode.CBC,
			padding: CryptoJS.pad.Pkcs7
		}
	);
	return encrypted.toString(); // Base64 密文
}

/**
 * AES 解密,失败返回空字符串
 */
export function decrypt(cipherText) {
	if (!cipherText) return "";
	try {
		const decrypted = CryptoJS.AES.decrypt(cipherText, SECRET_KEY, {
			iv: SECRET_IV,
			mode: CryptoJS.mode.CBC,
			padding: CryptoJS.pad.Pkcs7
		});
		return decrypted.toString(CryptoJS.enc.Utf8);
	} catch (e) {
		return "";
	}
}

/**
 * 对象参数加密:{ userId: 1 } -> "5U2Jk9..."(拼在 ?params= 后)
 */
export function encryptParams(params) {
	return encodeURIComponent(encrypt(JSON.stringify(params)));
}

/**
 * 对象参数解密:密文 -> { userId: 1 }
 */
export function decryptParams(cipher) {
	try {
		return JSON.parse(decodeURIComponent(decrypt(cipher)));
	} catch (e) {
		return null;
	}
}

/**
 * MD5 签名(不可逆):给参数生成摘要,配合时间戳防重放
 */
export function signParams(params, salt = "blog-salt") {
	const sortedKeys = Object.keys(params).sort();
	const raw = sortedKeys.map(k => `${k}=${params[k]}`).join("&") + `&salt=${salt}`;
	return CryptoJS.MD5(raw).toString();
}

使用:路由跳转

import { encryptParams } from "@/utils/crypto";

// 加密跳转
const query = encryptParams({ userId: 1, orderId: 100 });
router.push(`/order-detail?params=${query}`);
// 地址栏看到的是 /order-detail?params=U2FsdGVkX1%2F8x3...(无明文信息)

使用:目标页解密

import { decryptParams } from "@/utils/crypto";

const route = useRoute();
const params = decryptParams(route.query.params);
if (!params) {
	// 解密失败:参数被篡改或过期
	router.replace("/403");
} else {
	console.log(params.userId, params.orderId); // 1 100
}

场景 2:登录密码传输

原则:前端不负责密码的最终安全(HTTPS 才是根基),但要避免明文直传。

常见方案:前端 MD5/SHA 加盐摘要后传输,服务端比对摘要:

import { signParams } from "@/utils/crypto";

export async function login(username, password) {
	return axios.post("/login", {
		username,
		// 加盐摘要,避免明文密码出现在请求体/日志中
		password: signParams({ username, timestamp: Date.now() }, password)
	});
}

更严格的方案是 RSA 非对称加密(crypto-js 不支持,用 jsencrypt):

npm install jsencrypt
import JSEncrypt from "jsencrypt";

// 公钥由服务端下发(前端只加密,私钥只有服务端有,抓包也解不开)
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...(服务端下发)
-----END PUBLIC KEY-----`;

const encryptor = new JSEncrypt();
encryptor.setPublicKey(PUBLIC_KEY);

export function rsaEncrypt(text) {
	return encryptor.encrypt(text); // 返回密文字符串,加密失败返回 false
}

// 登录:rsaEncrypt(JSON.stringify({ username, password, timestamp }))

前端加密的安全边界(重要)

  1. 前端加密 ≠ 安全:密钥放在前端代码里,打开 devtools 就能拿到。它能防"肉眼直接看懂",防不了恶意用户
  2. 传输安全的根基是 HTTPS,前端加密只是纵深防御的一层
  3. 密码不要用纯 MD5(彩虹表可查),至少加盐 + 多次迭代,更好的是服务端 bcrypt/scrypt/argon2
  4. crypto-js 适合:参数混淆、防爬虫、签名校验、本地缓存的轻度加密;真正的鉴权交给 token/HTTPS
Loading...