基于工作量证明(PoW)的验证码系统的简单实现
前言
CAPTCHA全称Completely Automated Public Turing Test,译为全自动区分计算机和人类的图灵测试,是指各种认证方法,这些方法利用一个对于人类来说很简单但对机器来说很难的挑战来测试用户,以验证用户是否为人类。
随着人工智能技术不断发展,许多传统的验证码测试系统被计算机轻易通过,因此许多厂商不断推出各种创新性的方法,然而大多数情况下机器根据一些标注数据进行训练后,也能够快速适应新的这些系统,反倒是对于用户而言,要花时间学习新的验证方式,徒增客户烦恼。
现在的一些验证码过于奇葩,例如:
在我看来,现在的验证码系统多多少少有些本末倒置了,把人类难住了,计算机却放过了。
因此有必要对现有的验证码系统做出改进。
工作量证明POW
原理
工作量证明即在获取我的服务之前,你需要向我证明你做了一定量的工作。
很常用的一种方法是通过寻找哈希值的方式,即你花费算力,寻找某些特定条件的哈希值,由于哈希函数的不可逆性和不可预测性,你不得不通过迭代的方式进行寻找,但是对于我而言,验证过程是很容易的,只需要将你给的值计算一遍哈希值,检查哈希值是否符合条件,只要符合条件,就说明你的确做了一定量的工作。
为了防止你使用彩虹表的方式记录下指定字符串的哈希值,我要给一个随机字符串prefix
,你再在这基础之上寻找一个后缀,使得两者拼接后的哈希值符合条件。
以上就是一个简单的基于pow的验证码系统的大致原理了,下面是实现过程
好吧,我承认这篇文章的确有些标题党了,这个方式并不是真正意义上的captcha,因为它并没有往区分人类与机器人的功能上走,但是今天的验证码系统的目的是什么?绝大多数是为了防止机器人访问吧,例如在登录页面放入验证码系统,防止机器人通过口令爆破的方式获取指定用户的密码,即先判断是人类,再判断输入的密码,因为人类输入内容速度较慢,以人工的方式爆破密码不现实。
而基于pow的系统,我并不关心访问者是人类还是机器,因为在验证你提交的密码之前,我先验证你的工作量,也可以说是“浪费”你的算力,对于人类而言,反正这个工作是计算机进行,用户无需干预,只需等一小会即可,反观机器人,为了爆破密码,每次提交不同的密码之前,都要花费一定时间解决pow难题,这么算下来,爆破速度和人类手工爆破速度一致,为了获取真实密码,花费的时间成本太大,那还不如不爆破了。
什么,你说美国人想用混合精度高达每秒1,000,000,000,000,000,000次的目前超算能力排名第一的超计算机Frontier来爆破我的博客系统管理员密码?
从这个意义上来说,基于pow的也算得上是一个称职的验证码系统,因为的确达到了减缓机器人爆破的目的,只不过这个验证码是一个哈希值,是由计算机去计算的,无需用户干预,提升了用户体验。
实现
为了实现这个过程,需要客户端和服务端共同努力,整个流程如下图:
首先客户端要请求服务端,服务端随机生成前缀字符串prefix
,将该字符串连同困难度difficult
返回给客户端,客户端以迭代方式计算后缀,使得前后缀的哈希值符合条件,然后将后缀以及对应的哈希值提交给服务端,服务端再结合之前生成的前缀,验证这个后缀是否符合条件,一方面,如果符合条件,在session中标记,然后通知客户端可以提交密码,收到用户提交的密码时候,第一件事就是判断session中有无标记,避免用户不进行pow而直接提交密码;另一方面,如果验证不通过,则重新生成一个随机前缀,重复上述步骤.
使用
目前客户端实现已开源至GitHub
可以根据下面的步骤使用:
git clone https://github.com/YaleXin/pow-captcha-js
npm install
npm run build
运行上述命令后,将会在dist
目录下产生pow-captcha-js.js
,将其复制到你需要的项目中即可,例如在Vue项目中,将其复制到目录static/js/
,在代码中使用:
<script>
import { Captcha } from "../../static/js/pow-captcha-js.js"
export default {
mounted() {
this.pow();
},
methods: {
pow() {
const CONFIG_URL = '/api/admin/powConfig';
const VERIFY_URL = '/api/admin/powVerify'
const cpt = new Captcha();
cpt.start(CONFIG_URL, VERIFY_URL).then(resobj=>{
console.log('obj==>', resobj);
}).catch(e=>{
console.log('pow e =>', e);
})
},
}
},
</script>
API
服务端
服务端必须实现两个接口:CONFIG_URL
和VERIFY_URL
在CONFIG_URL
接口中,需要返回一个json数据,格式如下:
{
"difficulty":5,
"prefix":"Ve03Plle"
}
在VERIFY_URL
接口中,需要接收一个json数据(该数据由客户端以post方式提交),格式如下:
{
"data":{
"md5Str":"00000119414c7a8c9678b96fbc4954be",
"paddingNum":300880
}
}
paddingNum
即用户暴力迭代寻找到的后缀.
服务端要在这个接口中完成两个验证:
md5Str==md5(prefix+paddingNum)
md5Str
前导零个数至少是difficulty
difficulty
和 prefix
对应于在接口CONFIG_URL
中返回的内容.
客户端
Captcha.start()
会返回一个promise对象,如果通过验证,则会在then
中返回一个对象resobj
,该对象内容如下:
{
verify: true,
tryServerCnt: tryServerCnt,
totalTryCnt: totalTryCnt
}
字段 | 描述 |
---|---|
verify | 服务端验证结果 |
totalTryCnt | 总迭代次数 |
总结
目前前端代码是单线程的,未来将考虑使用多线程.
其实我一开始就是以多线程的方式是写的,当时是用worker_threads
,但是打包过程发现我才发现这包是在nodejs
环境下的,在浏览器环境下无法执行,只好换成Web Worker
的方式,但是这种方式对于传入的脚本有同源限制,打包后总是提示找不到对应的worker.js
.最后不得不使用单线程的方式.
未来看一下怎么用借助webpack
的worker loader
解决使用worker
遇到的问题.
- 2023-06-20更新:✔ 已借助webpack完成多线程重塑代码