前端加密解密实战:从原理到项目应用的完整指南
我的写法,亲测靠谱
加密解密这东西,前端能躲就躲。但有些项目你就是绕不开——比如要存用户敏感信息到 localStorage,又不想裸奔;或者和后端传数据时要求前端先做一层对称加密。这时候别慌,我一般用 CryptoJS + 自定义封装,虽然不是最牛的方案,但稳定、简单、不容易出事。
核心逻辑是:只做对称加密(AES),密钥由环境变量注入或动态生成,绝不硬编码在代码里。为什么?后面会讲一个我踩过的巨坑。
下面是我常用的封装方式:
import CryptoJS from 'crypto-js';
// 密钥来源必须灵活,不能写死
const getEncryptionKey = () => {
// 比如从登录后返回的 token 中提取 key,或者从环境变量读取
return process.env.REACT_APP_CRYPTO_KEY || 'fallback-temp-key-for-dev';
};
export const encrypt = (data) => {
try {
const key = getEncryptionKey();
const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(data), key).toString();
return ciphertext;
} catch (error) {
console.warn('加密失败', error);
return null;
}
};
export const decrypt = (ciphertext) => {
try {
const key = getEncryptionKey();
const bytes = CryptoJS.AES.decrypt(ciphertext, key);
const originalText = bytes.toString(CryptoJS.enc.Utf8);
if (!originalText) return null;
return JSON.parse(originalText);
} catch (error) {
console.warn('解密失败,可能是密钥不对或数据损坏', error);
return null;
}
};
这段代码我在三个项目里都用过,最大的好处是:出问题有兜底(try/catch),不会因为一条数据解密失败直接崩页面。而且用了 JSON.stringify 和 parse,支持对象加密,比单纯加解字符串实用得多。
这里注意,我踩过好几次坑的是密钥管理。最早一次我把密钥直接写成字符串常量放在 utils 文件里,结果 build 完代码被人反编译一眼看出来,测试账号密码全被导出来了……后来改成了运行时注入,至少不会明文躺在 js 包里。
这几种错误写法,别再踩坑了
见过太多人乱搞加密,我列几个典型反面案例:
- 密钥写死在代码里:像这样
const KEY = '123456',然后传给 AES.encrypt。拜托,打包完 source map 一开,谁都能看到。就算你混淆了变量名,静态分析工具分分钟扒出来。 - 用 Base64 当加密:有人把敏感数据
btoa(JSON.stringify(data))就当“加密”了,实际上这只是编码,连加密都不是。稍微懂点前端的复制粘贴就能 decode 回去。 - 本地存储加密数据却不处理异常:比如 localStorage.setItem(‘user’, encrypt(user)),但 decrypt 的时候没考虑 key 变了怎么办。结果用户一升级版本,key 更新了,老数据全废,直接报错卡登录页。
- 在 URL 参数里传加密串还带等号:AES 加密出来的字符串包含
/,+,=,直接拼在 URL 上,不 encodeURI 就跳转,后端收不到完整参数。折腾了半天发现是 + 被当成空格处理了……
上面这些问题我都亲自踩过,尤其是最后一个,排查了两个小时才定位到是 URL 编码问题。建议处理办法是加密后用 base64url 编码再传输:
function base64UrlEncode(str) {
return str.replace(/+/g, '-').replace(///g, '_').replace(/=+$/, '');
}
function base64UrlDecode(str) {
str = (str + '===').slice(0, str.length + (str.length % 4));
return str.replace(/-/g, '+').replace(/_/g, '/');
}
这样加密后的字符串才能安全放进 query string 或 cookie。
实际项目中的坑
上个月我接手一个旧项目,里面有个需求是保存用户的支付信息到本地,方便快速填写。原开发用了 CryptoJS 加密,但有个致命问题:每次启动应用生成不同的随机 key,加密完存进 localStorage。结果重启之后根本没法解密之前的数据!
我当时一看代码人都傻了,这不是等于没加密吗?数据每次都是新的,老的永远读不出来。修复办法很简单:key 必须持久化或者可推导,不能随机。
最后我们改成用用户 ID 做 salt,拼接固定 secret 生成 key:
const deriveKeyFromUserId = (userId) => {
const baseKey = process.env.REACT_APP_ENCRYPTION_SALT;
return CryptoJS.SHA256(baseKey + userId).toString().substr(0, 32); // AES-256 需要 32 字节
};
这样同一个用户每次进来看到的 key 是一致的,又能保证不同用户之间隔离。当然,前提是用户 ID 不变。
还有一个细节:不要忽略浏览器兼容性。CryptoJS 在 IE11 上基本没问题,但如果你用 Web Crypto API(原生 crypto.subtle),那得小心,IE 全系列不支持,Safari 有些方法行为也不一样。所以中小型项目我还是推荐 CryptoJS,兼容性稳得多。
还有个小技巧:你可以加个版本前缀来标识加密格式,方便以后升级算法。
export const encrypt = (data) => {
try {
const key = getEncryptionKey();
const rawStr = JSON.stringify(data);
const ciphertext = CryptoJS.AES.encrypt(rawStr, key).toString();
return v1:${ciphertext}; // 加个版本号
} catch (e) {
return null;
}
};
export const decrypt = (cipherWithVersion) => {
if (!cipherWithVersion) return null;
let ciphertext;
if (cipherWithVersion.startsWith('v1:')) {
ciphertext = cipherWithVersion.slice(3);
} else {
// 兼容老数据
ciphertext = cipherWithVersion;
}
try {
const key = getEncryptionKey();
const bytes = CryptoJS.AES.decrypt(ciphertext, key);
const result = bytes.toString(CryptoJS.enc.Utf8);
return result ? JSON.parse(result) : null;
} catch {
return null;
}
};
这样未来如果要换算法(比如改 ChaCha20-Poly1305),可以直接出 v2 版本,不影响老用户。
API 请求里的加密也要小心
有些项目要求前端把整个请求体加密发给后端。比如这样:
fetch('https://jztheme.com/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: encrypt({ name: '张三', phone: '13888888888' })
})
看着没啥问题,但要注意两点:
- 加密后的 body 已经不是 JSON 了,Content-Type 应该改成 text/plain 或自定义类型,否则后端 parser 可能误解析。
- 如果用了 HTTPS,其实没必要再加这一层加密。除非你们公司合规要求“双重保护”,不然纯属增加复杂度。
我自己更倾向于:HTTPS + 后端字段级加密,而不是让前端背这个锅。毕竟前端环境不可控,真想偷数据的人早就 hook 掉 decrypt 方法了。
总结一下我怎么防翻车
最后说说我现在的标准操作流程:
- 只在必要场景用前端加密:比如临时缓存敏感信息,或对接特定第三方协议。
- 绝对不用前端做身份认证类的加密逻辑(比如算签名登录),交给后端。
- 密钥不硬编码,通过安全渠道下发或派生。
- 所有加密操作包 try/catch,失败降级为空或提示重新输入。
- 本地存储加密数据时,加上时间戳或版本标识,便于清理或迁移。
- 优先使用经过考验的库(CryptoJS),不自己造轮子。
改完后仍有一两个小问题,比如不同设备同步数据时密钥不一致,但这属于产品设计层面的问题,技术上已经做到可控了。
以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流,比如有人用 WebAssembly 跑加密算法的也可以聊聊,我一直没敢在生产环境试。这个技巧的拓展用法还有很多,后续会继续分享这类实战博客。

暂无评论