敏感数据加密方案及实现
本文首发于政采云前端团队博客:敏感数据加密方案及实现
https://www.zoo.team/article/data-encryption
前言
现在是大数据时代,需要收集大量的个人信息用于统计。一方面它给我们带来了便利,另一方面一些个人信息数据在无意间被泄露,被非法分子用于推销和黑色产业。
2018 年 5 月 25 日,欧盟已经强制执行《通用数据保护条例》(General Data Protection Regulation,缩写作 GDPR)。该条例是欧盟法律中对所有欧盟个人关于数据保护和隐私的规范。这意味着个人数据必须使用假名化或匿名化进行存储,并且默认使用尽可能最高的隐私设置,以避免数据泄露。
相信大家也都不想让自己在外面“裸奔”。所以,作为前端开发人员也应该尽量避免用户个人数据的明文传输,尽可能的降低信息泄露的风险。
看到这里可能有人会说现在都用 HTTPS 了,数据在传输过程中是加密的,前端就不需要加密了。其实不然,我可以在你发送 HTTPS 请求之前,通过谷歌插件来捕获 HTTPS 请求中的个人信息,下面我会为此演示。所以前端数据加密还是很有必要的。
数据泄露方式
中间人攻击
中间人攻击是常见的攻击方式。详细过程可以参见这里:https://zh.wikipedia.org/wiki/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB。大概的过程是中间人通过 DNS 欺骗等手段劫持了客户端与服务端的会话。
客户端、服务端之间的信息都会经过中间人,中间人可以获取和转发两者的信息。在 HTTP 下,前端数据加密还是避免不了数据泄露,因为中间人可以伪造密钥。为了避免中间人攻击,我们一般采用 HTTPS 的形式传输。
谷歌插件
HTTPS 虽然可以防止数据在网络传输过程中被劫持,但是在发送 HTTPS 之前,数据还是可以从谷歌插件中泄露出去。
因为谷歌插件可以捕获 Network 中的所有请求,所以如果某些插件中有恶意的代码还是可以获取到用户信息的,下面为大家演示。
加密算法介绍
-
对称加密
对称加密算法,又称为共享密钥加密算法。在对称加密算法中,使用的密钥只有一个,发送和接收双方都使用这个密钥对数据进行加密和解密。
这就要求加密和解密方事先都必须知道加密的密钥。其优点是算法公开、计算量小、加密速度快、加密效率高;缺点是密钥泄露之后,数据就会被破解。一般不推荐单独使用。根据实现机制的不同,常见的算法主要有 AES (https://zh.wikipedia.org/wiki/%E9%AB%98%E7%BA%A7%E5%8A%A0%E5%AF%86%E6%A0%87%E5%87%86)、ChaCha20 (https://zh.wikipedia.org/wiki/Salsa20#ChaCha20)、3DES (https://zh.wikipedia.org/wiki/3DES)等。
-
非对称加密
非对称加密算法,又称为公开密钥加密算法。它需要两个密钥,一个称为公开密钥 (public key),即公钥;另一个称为私有密钥 (private key),即私钥。
他俩是配对生成的,就像钥匙和锁的关系。因为加密和解密使用的是两个不同的密钥,所以这种算法称为非对称加密算法。其优点是算法强度复杂、安全性高;缺点是加解密速度没有对称加密算法快。常见的算法主要有 RSA (https://zh.wikipedia.org/wiki/RSA%E5%8A%A0%E5%AF%86%E6%BC%94%E7%AE%97%E6%B3%95)、Elgamal (https://zh.wikipedia.org/wiki/ElGamal%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95)等。
-
散列算法
散列算法又称散列函数、哈希函数,是把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定成特定长度的值。一般用于校验数据的完整性,平时我们下载文件就可以校验 MD5 来判断下载的数据是否完整。常见的算法主要有 MD4 (https://zh.wikipedia.org/wiki/MD4)、MD5 (https://zh.wikipedia.org/wiki/MD5)、SHA (https://zh.wikipedia.org/wiki/SHA%E5%AE%B6%E6%97%8F) 等。
实现方案
-
方案一:如果用对称加密,那么服务端和客户端都必须知道密钥才行。那服务端势必要把密钥发送给客户端,这个过程中是不安全的,所以单单用对称加密行不通。
-
方案二:如果用非对称加密,客户端的数据通过公钥加密,服务端通过私钥解密,客户端发送数据实现加密没问题。客户端接受数据,需要服务端用公钥加密,然后客户端用私钥解密。所以这个方案需要两套公钥和私钥,需要在客户端和服务端各自生成自己的密钥。
-
方案三:如果把对称加密和非对称加密相结合。客户端需要生成一个对称加密的密钥 1,传输内容与该密钥 1进行对称加密传给服务端,并且把密钥 1 和公钥进行非对称加密,然后也传给服务端。服务端通过私钥把对称加密的密钥 1 解密出来,然后通过该密钥 1 解密出内容。以上是客户端到服务端的过程。如果是服务端要发数据到客户端,就需要把响应数据跟对称加密的密钥 1 进行加密,然后客户端接收到密文,通过客户端的密钥 1 进行解密,从而完成加密传输。
-
总结:以上只是列举了常见的加密方案。总的来看,方案二比较简单,但是需要维护两套公钥和私钥,当公钥变化的时候,必须通知对方,灵活性比较差。方案三相对方案二来说,密钥 1 随时可以变化,并且不需要通知服务端,相对来说灵活性、安全性好点并且方案三对内容是对称加密,当数据量大时,对称加密的速度会比非对称加密快。所以本文采用方案三给予代码实现。
代码实现
-
下面是具体的代码实现(以登录接口为例),主要的目的就是要把明文的个人信息转成密文传输。其中对称加密库使用的是 AES,非对称加密库使用的是RSA。
-
客户端:
-
AES 库(aes-js):https://github.com/ricmoo/aes-js
-
RSA库(jsencrypt):https://github.com/travist/jsencrypt
-
具体代码实现登录接口
1、客户端需要随机生成一个 aesKey,在页面加载完的时候需要从服务端请求 publicKey
let aesKey = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; // 随机产生
let publicKey = ""; // 公钥会从服务端获取
// 页面加载完之后,就去获取公钥
window.onload = () => {
axios({
method: "GET",
headers: { "content-type": "application/x-www-form-urlencoded" },
url: "http://localhost:3000/getPub",
})
.then(function (result) {
publicKey = result.data.data; // 获取公钥
})
.catch(function (error) {
console.log(error);
});
};
2、aes 加密和解密方法
/**
* aes加密方法
* @param {string} text 待加密的字符串
* @param {array} key 加密key
*/
function aesEncrypt(text, key) {
const textBytes = aesjs.utils.utf8.toBytes(text); // 把字符串转换成二进制数据
// 这边使用CTR-Counter加密模式,还有其他模式可以选择,具体可以参考aes加密库
const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
const encryptedBytes = aesCtr.encrypt(textBytes); // 进行加密
const encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); // 把二进制数据转成十六进制
return encryptedHex;
}
/**
* aes解密方法
* @param {string} encryptedHex 加密的字符串
* @param {array} key 加密key
*/
function aesDecrypt(encryptedHex, key) {
const encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex); // 把十六进制数据转成二进制
const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
const decryptedBytes = aesCtr.decrypt(encryptedBytes); // 进行解密
const decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); // 把二进制数据转成utf-8字符串
return decryptedText;
}
3、请求登录
/**
* 登陆接口
*/
function submitFn() {
const userName = document.querySelector("#userName").value;
const password = document.querySelector("#password").value;
const data = {
userName,
password,
};
const text = JSON.stringify(data);
const sendData = aesEncrypt(text, aesKey); // 把要发送的数据转成字符串进行加密
console.log("发送数据", text);
const encrypt = new JSEncrypt();
encrypt.setPublicKey(publicKey);
const encrypted = encrypt.encrypt(aesKey.toString()); // 把aesKey进行非对称加密
const url = "http://localhost:3000/login";
const params = { id: 0, data: { param1: sendData, param2: encrypted } };
axios({
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
url: url,
data: JSON.stringify(params),
})
.then(function (result) {
const reciveData = aesDecrypt(result.data.data, aesKey); // 用aesKey进行解密
console.log("接收数据", reciveData);
})
.catch(function (error) {
console.log("error", error);
});
}
-
服务端(Node):
-
AES库(aes-js):https://github.com/ricmoo/aes-js
-
RSA 库(node-rsa):https://github.com/rzcoder/node-rsa
-
具体代码实现登录接口
1、引用加密库
const http = require("http");
const aesjs = require("aes-js");
const NodeRSA = require("node-rsa");
const rsaKey = new NodeRSA({ b: 1024 }); // key的size为1024位
let aesKey = null; // 用于保存客户端的aesKey
let privateKey = ""; // 用于保存服务端的公钥
rsaKey.setOptions({ encryptionScheme: "pkcs1" }); // 设置加密模式
2、实现 login 接口
http
.createServer((request, response) => {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Headers", "Content-Type");
response.setHeader("Content-Type", "application/json");
switch (request.method) {
case "GET":
if (request.url === "/getPub") {
const publicKey = rsaKey.exportKey("public");
privateKey = rsaKey.exportKey("private");
response.writeHead(200);
response.end(JSON.stringify({ result: true, data: publicKey })); // 把公钥发送给客户端
return;
}
break;
case "POST":
if (request.url === "/login") {
let str = "";
request.on("data", function (chunk) {
str += chunk;
});
request.on("end", function () {
const params = JSON.parse(str);
const reciveData = decrypt(params.data);
console.log("reciveData", reciveData);
// 一系列处理之后
response.writeHead(200);
response.end(
JSON.stringify({
result: true,
data: aesEncrypt(
JSON.stringify({ userId: 123, address: "杭州" }), // 这个数据会被加密
aesKey
),
})
);
});
return;
}
break;
default:
break;
}
response.writeHead(404);
response.end();
})
.listen(3000);
3、加密和解密方法
function decrypt({ param1, param2 }) {
const decrypted = rsaKey.decrypt(param2, "utf8"); // 解密得到aesKey
aesKey = decrypted.split(",").map((item) => {
return +item;
});
return aesDecrypt(param1, aesKey);
}
/**
* aes解密方法
* @param {string} encryptedHex 加密的字符串
* @param {array} key 加密key
*/
function aesDecrypt(encryptedHex, key) {
const encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex); // 把十六进制转成二进制数据
const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5)); // 这边使用CTR-Counter加密模式,还有其他模式可以选择,具体可以参考aes加密库
const decryptedBytes = aesCtr.decrypt(encryptedBytes); // 进行解密
const decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); // 把二进制数据转成字符串
return decryptedText;
}
/**
* aes加密方法
* @param {string} text 待加密的字符串
* @param {array} key 加密key
*/
function aesEncrypt(text, key) {
const textBytes = aesjs.utils.utf8.toBytes(text); // 把字符串转成二进制数据
const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
const encryptedBytes = aesCtr.encrypt(textBytes); // 加密
const encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); // 把二进制数据转成十六进制
return encryptedHex;
}
-
完整的示例代码:https://github.com/Pulset/FrontDataEncrypt
演示效果
总结
本文主要介绍了一些前端安全方面的知识和具体加密方案的实现。为了保护客户的隐私数据,不管是 HTTP 还是 HTTPS,都建议密文传输信息,让破解者增加一点攻击难度吧。当然数据加解密也会带来一定性能上的消耗,这个需要各位开发者各自衡量了。
参考文献
看完这篇文章,我奶奶都懂了 https 的原理 (https://www.cnblogs.com/sujing/p/10927569.html)
中间人攻击 (https://zh.wikipedia.org/wiki/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB)
看完两件事
如果你觉得这篇内容对你挺有启发,我想邀请你帮我两件小事
1.点个「在看」,让更多人也能看到这篇内容(点了「在看」,bug -1 😊)
招贤纳士
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com
本文分享自微信公众号 - 政采云前端团队(Zoo-Team)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
这个缓存更新的套路你都知道吗?
前言 Hello,everybody,我是asong,上一篇文章我们一起聊一聊了面试中几个常见的缓存问题,今天我依然聊一聊缓存,不过今天我们聊的不是面试了,我们一起来看一看我们在系统中缓存更新的设计,因自己经验有限,所以这些缓存设计来源于网上,我只是在这里总结一下,有什么不对的欢迎指出~~~😊。 缓存预热 To solve 缓存冷启动 在上一篇文章中[常见面试题之缓存雪崩、缓存穿透、缓存击穿],忘记讲了一个概念——缓存预热,所以在这篇文章补充一下,开一个好头,预热嘛~~~。 什么是缓存预热呢?我们都知道平常在跑步前都要热身,可以预防肌肉拉伤等一系列的好处。所以缓存预热具有同样的道理,我们的新系统上线后,我们可以将相关的缓存数据直接加载到缓存系统。这样可以避免在用户请求的时候,先去查询数据库,然后再将数据缓存的问题。用户可以直接查询事先已被预热的缓存数据。其实缓存预热是为了解决缓存冷启动问题,我们新系统上线后,redis集群启动后,没有任何的缓存数据,这就是redis的冷启动。 如上图所示,如果不进行预热,那么Redis初识状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库...
- 下一篇
一篇文章告诉你搜索引擎是如何工作的
搜索的普遍流程 搜索,推荐,广告三兄弟,整体的技术栈,流程框架是比较相似的。主要区别在于业务逻辑上的细微不同,但是可以肯定的是,搜索是三兄弟中最重要的。 搜索的整体流程同样是召回,排序两大块。但是除此之外,从一个完备的搜索引擎来看,要处理的事情远不止这么简单。总体来看,整个搜索可以看作这么几个阶段: 数据预处理 query understanding 召回模块 排序模块 后处理 数据预处理 对于输入 query 第一步需要预处理为方便操作的形式,以供后续的步骤可以有效进行。常见的操作有: 无效内容的过滤:比如标点符号, emoji 表情,奇怪的字符等。 简繁体转化。 长度截断。 数字转中文,中文数字转阿拉伯数字等。 译名,别名等转化。 禁搜词,禁搜内容等过滤。 在预处理后可以获得一个比较规整的 query 字段,接下来对相应的 query 进行逐步处理。 QU/query understanding QU 部分的内容并非一个搜索引擎必须的,但却是一个想要做好做优秀的搜索引擎必须的。整个 QU 部分的效果会对召回和排序阶段都产生巨大影响,而后续的无论召回还是排序都很依赖这一步的结果。 Q...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8编译安装MySQL8.0.19
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Red5直播服务器,属于Java语言的直播服务器
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- SpringBoot2整合Thymeleaf,官方推荐html解决方案