URL安全的自定义Base64编码

# 需求描述

业务需要,需要在后台管理动态生成一个url链接,直接复制或通过二维码分享该链接。

    {
        "id":"abcd1234",
        "name":"我是名称1234asdv@#$%^/=&*()"
    }
  1. 参数是一个对象,对象不会太复杂,但可能有不可控的用户输入特殊字符
  2. 可能会复制链接分享至IM软件-用户直接点击浏览器会自动转义
  3. 最好简单加密,一来需要保护隐私,二来不确定复制过程会被修改,因为并不能确定用户在哪里使用(可能在浏览器直接使用).

# 主要思路

首先,网页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

直接使用会有两个问题

  1. 分享链接,IM软件中链接可直接打开,url会被浏览器自动解析; 代码再解码就会失败。
  2. 地址栏自动解码后会出现明文,私密性安全性不高,且参数可被修改

# 方案二: Base64编码

JS 标准API window.atobwindow.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-

npm (opens new window)

github (opens new window)

# 方案五: 自己实现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语义的+/=替换掉即可。

最近更新
01
echarts扇形模拟镜头焦距与可视化角度示意图
03-10
02
vite插件钩子
03-02
03
vite的依赖预构建
02-13
更多文章>