对于前端来说,加密是一个老生常谈的话题。现在的加密方法和js库也是层出不穷。最近公司项目到了收尾阶段,所以想做一些其他的工作,决定把接口返回的数据简单的加密一下,以至于接口被拦截时,数据不那么赤裸裸的显示别人面前,就想到了用base64的方式。
虽然base64跟其他比较有名的rsa,hash等加密方法比起来显得不太专业,但相比其他复杂的库来说,代码量少,运算量小,并且还能加密解密都自己实现。并且,从开发的角度来说,如果前后端再约定一个一定的打乱顺序,拿到之后自己再重组解密,那么,破解还是不那么容易的。另一方面来说,我们的目的并不是为了别人无法破解(从理论上说,只要能解密的加密,对攻击者来说都只是时间问题),并且太过敏感的数据,我们一般是不会暴露在前端的。虽然 window 自带了 atob 和 btoa,但是不支持低版本浏览器,也不支持中文。如果你只是加密密码这些不含中文字符的内容,可以简单的使用下面的兼容代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//字符串转base64 function encode(str){ // 对字符串进行编码 var encode = encodeURI(str); // 对编码的字符串转化base64 var base64 = btoa(encode); return base64; } // base64转字符串 function decode(base64){ // 对base64转编码 var decode = atob(base64); // 编码转字符串 var str = decodeURI(decode); return str; } |
既然window自带的无法满足我们的需求,那我们就只能自己来完成这一转换功能了。在进行代码编写之前,我们先来了解一下base64是啥,以及如何转换,可帮助我们理解最后完成的代码。
什么是base64
base64是用规定的64种字符来表示任意二进制数据的一种编码格式,而且这64种字符均是可见字符,而之所以要是可见的是因为在不同设备上处理不可见字符时可能发生错误。通常,电子邮件数据、公钥证书会经常使用。
base64编码原理
我们知道单个字符一般用一个字节就可以表示(中文等其他特殊文字除外),而一个字节由8位二进制数构成。那么base64编码中,是将每6位二进制作为一个单位解析后参照字符集的索引就可以得到编码后的字符。举个例子:
1 2 |
二进制数据:000001 000011 编码后:BD |
但是我们发现一个问题,就是我们单个字符是8位二进制,而base64编码时需要以6位二进制数位单位进行编码。那么多余的2位怎么去处理呢?看下面的例子
1 2 3 4 |
未编码的字符串:B 对应的二进制数据: 010000 10 对残缺的二进制进行补足:010000 100000 000000 000000 编码后:Qg== |
通过上面的例子我们可以发现,base64编码时会将二进制通过在末尾补0的方式使其位数满足24的倍数。这样刚好能够编码出至少4个字符。从上面的栗子中我们可以看到=号的数量刚好是缺少2个字节数的数量,而g则是因为多余的2位二进制数补了4个0后编码成了g。所以我们就可以看到这样的编码了。如果还不理解再举一个例子:
1 2 3 4 |
未编码的字符串:BD 对应的二进制数据: 010000 100100 0100 对残缺的二进制进行补足:010000 100100 010000 000000 编码后:QkQ= |
有了理论知识后,我们接下来要做的,就是解析字符串为二进制数据了。我们知道,对于 js 中的字符串(不带中文字符)来说,刚好有个 charCodeAt() 方法返回指定字符的 unicode 编码对应的数值,这个数值就是二进制数表示的数值,我们只需要将其转换为二进制即可。
1 2 3 4 5 6 7 8 9 10 |
var str = 'ABCD', i = 0, len = str.length; for( ; i < len ; i++){ console.log(str.charCodeAt(i).toString(2)) } // 1000001 // 1000010 // 1000011 // 1000100 |
英文字符已经搞定了,那中文又该如何处理呢?直接上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
// 将单个字符的unicode转换为以UTF-8格式的二进制数据 function toUTF8Binary(unicode){ var len, binary = '', star = 0, bitStream = unicode.toString(2), // 转换为二进制比特流 bitLen = bitStream.length, i; // 0 =< unicode <= 127 , 2的8次方-1 if( unicode >= 0x000000 && unicode <= 0x00007F ){ binary = bitStream; for( i = 0 , len = 8 ; i < len-bitLen ; i ++ ){ binary = 0 + binary; // 不足8位补0 } } // 128 =< unicode <= 2047 , 2的11次方-1 else if( unicode >=0x000080 && unicode <=0x0007FF ){ binary = bitStream; for( i = 0 , len = 11 ; i < len-bitLen ; i ++ ){ binary = 0 +binary; // 不足11位补0 } binary = '110' + binary.substr(0,5) + '10' + binary.substr(5,6); } // 2048 =< unicode <= 65535 , 2的16次方-1 else if( unicode >=0x000800 && unicode <=0x00FFFF ){ binary = bitStream; for( i = 0 , len = 16 ; i < len-bitLen ; i ++ ){ binary = 0 +binary; // 不足16位补0 }; binary = '1110' + binary.substr(0,4) + '10' + binary.substr(4,6) + '10' + binary.substr(10,6); } // 65536 =< unicode <= 1114111 , 2的21次方-1 else if( unicode >=0x010000 && unicode <=0x10FFFF ){ binary = bitStream; for( i = 0 , len = 21 ; i < len-bitLen ; i ++ ){ binary = 0 +binary; // 不足21位补0 } binary = '11110' + binary.substr(0,3) + '10' + binary.substr(3,6) + '10' + binary.substr(9,6) + '10' + binary.substr(15,6); } return binary; } |
到这里,解析成base64的方法已经实现了,那base64转字符串就是一个逆解析的过程,这里就不再赘述了。
最后,附上完整代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
// secretB64.js export default { // private property _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", // public method for encoding encode: function(input) { var output = ""; var chr1, chr2, chr3, enc1, enc2, enc3, enc4; var i = 0; input = this._utf8_encode(input); while (i < input.length) { chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); chr3 = input.charCodeAt(i++); enc1 = chr1 >> 2; enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); enc4 = chr3 & 63; if (isNaN(chr2)) { enc3 = enc4 = 64; } else if (isNaN(chr3)) { enc4 = 64; } output = output + this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4); } return output; }, // public method for decoding decode: function(input) { var output = ""; var chr1, chr2, chr3; var enc1, enc2, enc3, enc4; var i = 0; input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); while (i < input.length) { enc1 = this._keyStr.indexOf(input.charAt(i++)); enc2 = this._keyStr.indexOf(input.charAt(i++)); enc3 = this._keyStr.indexOf(input.charAt(i++)); enc4 = this._keyStr.indexOf(input.charAt(i++)); chr1 = (enc1 << 2) | (enc2 >> 4); chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); chr3 = ((enc3 & 3) << 6) | enc4; output = output + String.fromCharCode(chr1); if (enc3 != 64) { output = output + String.fromCharCode(chr2); } if (enc4 != 64) { output = output + String.fromCharCode(chr3); } } output = this._utf8_decode(output); return output; }, // private method for UTF-8 encoding _utf8_encode: function(string) { string = string.replace(/\r\n/g, "\n"); var utftext = ""; for (var n = 0; n < string.length; n++) { var c = string.charCodeAt(n); if (c < 128) { utftext += String.fromCharCode(c); } else if ((c > 127) && (c < 2048)) { utftext += String.fromCharCode((c >> 6) | 192); utftext += String.fromCharCode((c & 63) | 128); } else { utftext += String.fromCharCode((c >> 12) | 224); utftext += String.fromCharCode(((c >> 6) & 63) | 128); utftext += String.fromCharCode((c & 63) | 128); } } return utftext; }, // private method for UTF-8 decoding _utf8_decode: function(utftext) { var string = ""; var i = 0; var c = 0, c3 =0, c2 =0; while (i < utftext.length) { c = utftext.charCodeAt(i); if (c < 128) { string += String.fromCharCode(c); i++; } else if ((c > 191) && (c < 224)) { c2 = utftext.charCodeAt(i + 1); string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); i += 2; } else { c2 = utftext.charCodeAt(i + 1); c3 = utftext.charCodeAt(i + 2); string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); i += 3; } } return string; } } |
使用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import secretB64 from '@/libs/secretB64' var obj = JSON.stringify({ title: '登录页面', loginErr: false, loginErrMsg: '的深V回复插卡式发布忄匚傘丨佱', loginTime: 0, loginDisabled: false, passwordErrorTime:1 }) var objmm = secretB64.encode(obj) console.log(objmm) // `eyJ0aXRsZSI6IueZu+W9lemhtemdoiIsImxvZ2luRXJyIjpmY WxzZSwibG9naW5FcnJNc2ciOiLnmoTmt7FW5Zue5aSN5o+S5Y2h5b yP5Y+R5biD5b+E5Yya5YKY5Lio5L2xIiwibG9naW5UaW1lIjowLCJ sb2dpbkRpc2FibGVkIjpmYWxzZSwicGFzc3dvcmRFcnJvclRpbWUiOjF9` console.log(JSON.parse(secretB64.decode(objmm))) // { title: '登录页面', loginErr: false, loginErrMsg: '的深V回复插卡式发布忄匚傘丨佱', loginTime: 0, loginDisabled: false, passwordErrorTime:1 } |
好了,到这里我们的banse64与字符串的互转算是完成了,从某种意义上来说,这已经是一种加密了。那我们还可以再做复杂一点,就以上面代码为例,后端返回数据的时候,可以把转成的字符串做一下重新组合,比如,把后六位截取了放到最前面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 原本应该是这样 var str = `eyJ0aXRsZSI6IueZu+W9lemhtemdoiIsImxvZ2luRXJyIjpmY WxzZSwibG9naW5FcnJNc2ciOiLnmoTmt7FW5Zue5aSN5o+S5Y2h5b yP5Y+R5biD5b+E5Yya5YKY5Lio5L2xIiwibG9naW5UaW1lIjowLCJ sb2dpbkRpc2FibGVkIjpmYWxzZSwicGFzc3dvcmRFcnJvclRpbWUiOjF9` // 重组后拿到的却是这样 str2 = `UiOjF9eyJ0aXRsZSI6IueZu+W9lemhtemdoiIsImxvZ2luRXJyIjpmY WxzZSwibG9naW5FcnJNc2ciOiLnmoTmt7FW5Zue5aSN5o+S5Y2h5b yP5Y+R5biD5b+E5Yya5YKY5Lio5L2xIiwibG9naW5UaW1lIjowLCJ sb2dpbkRpc2FibGVkIjpmYWxzZSwicGFzc3dvcmRFcnJvclRpbW` console.log(JSON.parse(secretB64.decode(str1))) // 能成功转为js对象 console.log(JSON.parse(secretB64.decode(str2))) // 报错 |
这样,一个简单的加密就成功了。
思考一下,如果我们把字符串的重组规则弄得足够复杂,是不是也基本满足了一定的加密需求。以我个人在项目中自定义的一个加密规则为例:
加密前的明文为:admin321$,加密后的密文为NE5vUkhGellXUnRhVzR6TWpFaw==;
加密前的明文为:321$admin,加密后的密文为MWtLYVRBMk16SXhKR0ZrYldsdQ==;
并且每次加密结果都不一样,你能一下子看出怎么加密的,简单的就破解它吗。
发表评论