需求
要实现房管功能,我需要实现一个方法,来生成和激活高级命令,激活后则可以获得高级权限。显然,我不想被别人看到这段代码里我是如何生成命令和校验命令的。
我也不想放到服务器上做成一个后端接口,因为每次都去请求接口会对服务器产生压力。而websocket每次收到消息时都需要去验证这个高级命令
所以,我确定了两个核心诉求
- 高级命令的生成和激活直接在前端完成,避免请求服务器。
- 不能让人看到生成和激活的核心逻辑,否则就失去了意义。
一番调研之后,发现WebAssembly
可以满足我的这两点需求,接下来就是实战了。
WebAssembly介绍
WebAssembly 有一套完整的语义,并且可以生成一种 .wasm 的二进制格式,体积小且加载快, 其目标就是充分发挥硬件能力以达到原生执行效率。
运行仍然是在浏览器里,但使用一种二进制格式的.wasm
文件 ,这完全能满足我的需求,并且主流浏览器对WebAssembly的兼容性很好。
比如下面这段代码,大家可以复制粘贴到浏览器的调试窗口中感受一下
WebAssembly.compile(new Uint8Array(` 00 61 73 6d 01 00 00 00 01 0c 02 60 02 7f 7f 01 7f 60 01 7f 01 7f 03 03 02 00 01 07 10 02 03 61 64 64 00 00 06 73 71 75 61 72 65 00 01 0a 13 02 08 00 20 00 20 01 6a 0f 0b 08 00 20 00 20 00 6c 0f 0b`.trim().split(/[\s\r\n]+/g).map(str => parseInt(str, 16)) )).then(module => { const instance = new WebAssembly.Instance(module) const { add, square } = instance.exports console.log('2 + 4 =', add(2, 4)) console.log('3^2 =', square(3)) console.log('(2 + 5)^2 =', square(add(2 + 5))) })
显然,你无法轻易的得知那段Uint8Array
里的算法是具体如何实现的...
但是要编写代码,并编译成.wasm
文件可不算太简单,比如在本案中,我选择C++
来编写代码,并通过emscripten
工具链编译成.wasm
文件。
emscripten
安装 emscripten 环境
# Get the emsdk repo git clone https://github.com/emscripten-core/emsdk.git # Enter that directory cd emsdk # Fetch the latest version of the emsdk (not needed the first time you clone) git pull # Download and install the latest SDK tools. ./emsdk install latest # Make the "latest" SDK "active" for the current user. (writes .emscripten file) ./emsdk activate latest # Activate PATH and other environment variables in the current terminal source ./emsdk_env.sh
环境安装好之后,我们立刻来写一段C语言
,并编译成.wasm
让它运行在浏览器里吧。
#include <stdio.h> int main(int argc, char ** argv) { printf("掘友们好"); printf("\n"); printf("这是一段来自C语言的问候"); printf("\n"); }
编译命令
emcc helloworld.c -s WASM=1 -o helloworld.html
编译完成后,记得起一个服务,比如live-server
,因为我们需要读取本地的.wasm
文件。这样我们就可以在浏览器里看到这段来自C语言
的问候了
但是坦白说,我并不认为WebAssembly可以减小代码量,比如这段简单的代码编译成.wasm
后竟然需要12KB
。
实战代码保护
比如原本我需要的JS逻辑如下,每天根据零点的时间戳转36进制得到一个6位长度的字符串(这个长度的输入体验还算友好),用户输入这个命令后即可激活高级功能。
蝌蚪池塘里的算法已变更,不要天真的以为我会爆料自己的算法哦:P
JS的写法
function verifyCommand(cmd){ var t_cmd = (new Date(new Date().setHours(0, 0, 0, 0)).getTime()/1000).toString(36); if(cmd == t_cmd)return 1; else return 0; }
JS在浏览器中是完全暴露的,在浏览器调试模式下,非常容易就可以知晓高级命令是如何生成的。
当然如文章开头所说的,我们也可以放到后端接口中去校验
PHP的写法
function MyEncode($var,$targetBit){ $dic = array( 0 => '0', 1 => '1', 2 => '2', 3 => '3', 4 => '4', 5 => '5', 6 => '6', 7 => '7', 8 => '8', 9 => '9', 10 => 'A', 11 => 'B', 12 => 'C', 13 => 'D', 14 => 'E', 15 => 'F', 16 => 'G', 17 => 'H', 18 => 'I', 19 => 'J', 20 => 'K', 21 => 'L', 22 => 'M', 23 => 'N', 24 => 'O', 25 => 'P', 26 => 'Q', 27 => 'R', 28 => 'S', 29 => 'T', 30 => 'U', 31 => 'V', 32 => 'W', 33 => 'X', 34 => 'Y', 35 => 'Z' ); $yushu=bcmod($var,$targetBit); $shang=floor(bcdiv($var, $targetBit)); if($shang==0){ return $dic[$yushu]; } else { return MyEncode($shang,$targetBit).$dic[$yushu]; } } date_default_timezone_set('PRC'); $time =strtotime(date("Y-m-d 00:00:00")); $v_cmd = MyEncode(intval($time),36); $cmd = strtoupper($_GET["cmd"]); if($cmd==$v_cmd){ echo 1; }else{ echo 0; }
但是利用服务端的话,那每次校验都必须请求接口,无疑会对服务器造成压力。
现在我们用C语言
来写一下,并最终编译为WebAssembly的.wasm
文件
//获得今天零点的时间戳,秒 unsigned int getTodayZeroTime() { //太长了,函数的实现请看源码 } //将数字转为指定进制,decimal为进制值 void f( long int x, char *p ,int decimal) { //太长了,函数的实现请看源码 } int verifyCommand(std::string cmd) { int t_time = getTodayZeroTime(); char t_cmd[MAXN]=""; f(t_time,t_cmd,36); if(strcmp(t_cmd, cmd.data()) == 0){ return 1; }else{ return 0; } }
JS调用.wasm
里的C方法并传参
要想在JS中调用WebAssembly里的函数,我们在函数外面用一个extern "C"来包裹
extern "C" { int verifyCommand(std::string cmd) { int t_time = getTodayZeroTime(); char t_cmd[MAXN]=""; f(t_time,t_cmd,36); if(strcmp(t_cmd, cmd.data()) == 0){ return 1; }else{ return 0; } } }
在编译.wasm
时添加命令
emcc verify.cc -o verify.html -s EXPORTED_FUNCTIONS="['_verifyCommand']" -s EXPORTED_RUNTIME_METHODS='["ccall"]'
重点是这两个参数,注意下划线和引号
- -s EXPORTED_FUNCTIONS="['_verifyCommand']"
- -s EXPORTED_RUNTIME_METHODS='["ccall"]'
编译完成后我们就可以使用JS来调用verifyCommand
这个C语言
里的函数了
//第一个参数,函数名称 //第二个参数,函数返回值类型 //第三个参数,函数受参类型数组 //第四个参数,传递的参数数组 Module.ccall("verifyCommand",'number',['string'],['qmocg0']); 复制代码
以下是调试窗口截图,我特意输对和输错来看了看返回值
现在我们就完成了这个高级命令的校验函数,直接在前端同步完成校验,无须请求后台接口异步获得校验结果。因为核心代码都在二进制的.wasm
文件中,有心人也无法【轻松】的看到函数内的逻辑是如何实现的。为什么重点标记了一下【轻松】二字呢?
因为能编译,就能反编译...
本文就到这里把,各位小蝌蚪们池塘见,记得点赞收藏,早晚你会用到的!