URL安全的自定义Base64编码
# 需求描述
业务需要,需要在后台管理动态生成一个url链接,直接复制或通过二维码分享该链接。
{
"id":"abcd1234",
"name":"我是名称1234asdv@#$%^/=&*()"
}
- 参数是一个对象,对象不会太复杂,但可能有不可控的用户输入特殊字符
- 可能会复制链接分享至IM软件-用户直接点击浏览器会自动转义
- 最好简单加密,一来需要保护隐私,二来不确定复制过程会被修改,因为并不能确定用户在哪里使用(可能在浏览器直接使用).
# 主要思路
首先,网页URL的合法字符,这是因为网络标准RFC 1738 (opens new window) 做了硬性规定:
只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号。
"...only alphanumerics, the special characters "$-_.+!*'(),", and reserved characters used for their reserved purposes may be used unencoded within a URL."
"只有字母和数字[0-9a-zA-Z]、一些特殊符号"$-_.+!*'(),"[不包括双引号]、以及某些保留字,才可以不经过编码直接用于URL。"
因为对象字符不可控,可能使用特殊字符。所以我们必须将对象序列化为url安全的字符串,再作为url参数传递,使用时再反序列化为对象。
# 方案一: 直接使用js函数
encodeURIComponent / decodeURIComponent将字符串编码再解码。
var obj = {
"id":"abcd1234",
"name":"我是名称1234asdv@#$%^/=&*()"
};
encodeURIComponent(JSON.stringify(obj))
// %7B%22id%22%3A%22abcd1234%22%2C%22name%22%3A%22%E6%88%91%E6%98%AF%E5%90%8D%E7%A7%B01234asdv%40%23%24%25%5E%2F%3D%26*()%22%7D
直接使用会有两个问题
- 分享链接,IM软件中链接可直接打开,url会被浏览器自动解析; 代码再解码就会失败。
- 地址栏自动解码后会出现明文,私密性安全性不高,且参数可被修改
# 方案二: Base64编码
JS 标准API window.atob
与 window.btoa
; 仅支持 ASCII 字符串,不能处理中文.
window.btoa(JSON.stringify({name:'122343我'}))
// VM15760:1 Uncaught DOMException: Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.
# 方案三: Base64配合encodeURIComponent
简单粗暴,先将对象序列化后再使用encodeURIComponent编码,再使用btoa编码.接收到后依次使用atob - decodeURIComponent解码,JSON.parse反序列化回对象。
var obj = {
"id":"abcd1234",
"name":"我是名称1234asdv@#$%^/=&*()"
};
window.btoa(window.encodeURIComponent(JSON.stringify(obj)))
//"JTdCJTIyaWQlMjIlM0ElMjJhYmNkMTIzNCUyMiUyQyUyMm5hbWUlMjIlM0ElMjIlRTYlODglOTElRTYlOTglQUYlRTUlOTAlOEQlRTclQTclQjAxMjM0YXNkdiU0MCUyMyUyNCUyNSU1RSUyRiUzRCUyNiooKSUyMiU3RA=="
JSON.parse(window.decodeURIComponent(window.atob('JTdCJTIyaWQlMjIlM0ElMjJhYmNkMTIzNCUyMiUyQyUyMm5hbWUlMjIlM0ElMjIlRTYlODglOTElRTYlOTglQUYlRTUlOTAlOEQlRTclQTclQjAxMjM0YXNkdiU0MCUyMyUyNCUyNSU1RSUyRiUzRCUyNiooKSUyMiU3RA==')))
// {id: "abcd1234", name: "我是名称1234asdv@#$%^/=&*()"}
此方案最简单,安全性隐私性也不错,唯一缺点是容易猜出编码方式,从而被篡改参数。
# 方案四: 直接使用js-base64库
js-base64支持的api非常全面。这个方案最省事,可以直接查看文档安装使用。
import { Base64 } from 'js-base64';
let latin = 'dankogai';
let utf8 = '小飼弾'
let u8s = new Uint8Array([100,97,110,107,111,103,97,105]);
Base64.encode(latin); // ZGFua29nYWk=
Base64.btoa(latin); // ZGFua29nYWk=
Base64.btoa(utf8); // raises exception
Base64.fromUint8Array(u8s); // ZGFua29nYWk=
Base64.fromUint8Array(u8s, true); // ZGFua29nYW which is URI safe
Base64.encode(utf8); // 5bCP6aO85by+
Base64.encode(utf8, true) // 5bCP6aO85by-
Base64.encodeURI(utf8); // 5bCP6aO85by-
# 方案五: 自己实现Base64编码
base64编码原理可以看这篇文章 (opens new window),讲的很清晰。 以下是base64的js标准编码的实现。
function Base64() {
// private property
_keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
// public method for encoding
this.encode = function (input) {
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
input = _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 +
_keyStr.charAt(enc1) + _keyStr.charAt(enc2) +
_keyStr.charAt(enc3) + _keyStr.charAt(enc4);
}
return output;
}
// public method for decoding
this.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 = _keyStr.indexOf(input.charAt(i++));
enc2 = _keyStr.indexOf(input.charAt(i++));
enc3 = _keyStr.indexOf(input.charAt(i++));
enc4 = _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 = _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 = c1 = 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;
}
}
站在巨人肩膀上,我们只需要把标准实现中url相关的特殊字符+/=替换掉即可
// 修改两行 +/= 替换为 -*@
_keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-*@";
input = input.replace(/[^A-Za-z0-9\-\*\@]/g, "");
js-base64库源码 (opens new window) 中也是这样替换的
使用方法
const bs = new Base64()
bs.encode(JSON.stringify(obj))
// eyJpZCI6ImFiY2QxMjM0IiwibmFtZSI6IuaIkeaYr-WQjeensDEyMzRhc2R2QCMkJV4vPSYqKCkifQ@@
JSON.parse(bs.decode('eyJpZCI6ImFiY2QxMjM0IiwibmFtZSI6IuaIkeaYr-WQjeensDEyMzRhc2R2QCMkJV4vPSYqKCkifQ@@'))
// {id: "abcd1234", name: "我是名称1234asdv@#$%^/=&*()"}
# 总结
如果要追求一定的安全性,建议自己造轮子,修改Base64编码字符集,也可以自定义顺序,只要将有url语义的+/=
替换掉即可。