这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
最近受够了公司内部站点每次登陆都需要填写用户名和密码,还有输入验证码。
要是能够直接跳过登陆页面就好啦。
说干就干,决定使用油猴插件实现自动登陆功能。
其中最难解决的就是验证码破解,花了一天的时间完美解决,现在整理出来。
1.分析验证码
分析验证码,是破解验证码一切工作的开始。
- 验证码有哪些特征?
- 是否容易破解?
- 采用什么策略破解?
特征总结
这里仅是总结一下公司网站验证码(上面验证码图片)的特征。
- 仅有字母(大小写)和数字,并且剔除了难以区分的字符:
1
、i
、I
、l
、L
、0
、o
、O
。 - 同一字符每次出现的大小、粗细、倾斜都一致(容易做成标准的字符样本库)
- 首字符开始的位置一致(方便裁剪左侧背景)
- 有干扰线和背景色,颜色相较于字符都比较亮(方便通过阈值来区分像素是否属于字符)
制定破解策略
根据上一步分析的验证码特征来制定破解该验证码的策略。
- 制作标准样本库
- 使用标准样本对验证码图片进行卷积比对(下面会有介绍)
2.制作样本库
- 请求获取验证码
- 提取图片像素
- 二值化(将像素处理成0和1)
- 用canvas绘制二值化后的验证码(白底黑字,也可等比放大以便查看和截图)
- 从绘制的二值化后的验证码上截取合适的字符
- 处理字符截图(去白边,去噪点)
- 还原图片的放大比例(若之前有放大处理)
- 保存为模板字符串
获取验证码
// 返回图片base64数据 function getVerifyCode() { return fetch(VERIFY_CODE_API) .then(rsp => rsp.json()) .then(data => `data:image/png;base64,${data.data}`) }
将base64数据转成像素
使用canvas。
// 支持base64数据或本地图片路径 async function getImageData(imageSrc) { const image = new Image(); image.src = imageSrc; // 等待图片加载完成 await new Promise(resolve => { image.onload = resolve; }); // 创建canvas const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); context.drawImage(image, 0, 0); return context.getImageData(0, 0, image.width, image.height); }
返回ImageData
类型的对象。
data
是一个Uint8ClampedArray
,一个类型数组,每4位表示一个像素的rgba值(0-255)。
二值化处理
首先需设置好一个阈值,亮度高于阈值认定为背景,低于阈值暂认定为字符(有可能是噪点或干扰线)。
阈值需要根据实际效果进行调优(不断修改)。
推荐初始阈值可以设置为[130, 130, 130]
(rgb通道值,alpha固定是255就不设置了),约是0-255的中间数。
const threshold = [130, 130, 130]; // 返回每一项都是0或1的二维数组 function binarization(imageData) { const pixel2binary = pixel => pixel.every((chValue, index) => chValue > threshold[index]) ? '0' : '1'; // data中每4位表示一个像素 const { data, width, height } = imageData; const binaryData = []; let x, y, row, rowLoc, pixel, pixelLoc; for (y = 0; y < height; y++) { row = []; // 当前行起始位置 rowLoc = y * width * 4; for (x = 0; x < width; x++) { pixelLoc = rowLoc + x * 4; // 取该点的rgb色值 pixel = imageData.slice(pixelLoc, 3); row.push(pixel2binary(pixel)); } binaryData.push(row); } return binaryData; }
绘制二值化的数据(黑字白底)
function drawBinaryData(context, data, scale = 1) { const binary2pixel = binary => binary === '0' ? [255, 255, 255, 255] : [0, 0, 0, 255]; const repeatAction = (action) => { for (let i = 0; i < scale; i++) action(); }; const h = data.length; const w = data[0].length; let x, y, row; cosnt pixelData = []; for (y = 0; y < h; y++) repeatAction(() => { for (x = 0; x < w; x++) repeatAction(() => { pixelData.push(...binary2pixel(data[y][x])); }); }); // 创建ImageData实例 const imageData = new ImageData( Uint8ClampedArray.from(pixelData), w * scale, h * scale ); return context.putImageData(imageData, 0, 0); }
输出宽高都放大4倍的验证码:
截图保存样本
挑选合适的验证码将字符截图出来。
上面验证码中的字符5就不适合作为样本,因为截取后右下方会有其它字符的点。当然也可以使用工具或写代码去除.
将所有字符样本都保存下来。这需要不断请求获取验证码图片。
去掉字符截图白边
function cutWhiteEdge(data) { let edge; const isWhiteEdge = () => edge.every(binary => binary === '0'); // 连续切边 const cutEdgeContinuous = (resetEdge, cutEdge) => { const _resetEdge = () => (edge = resetEdge()); for (_resetEdge(); isWhiteEdge(); cutEdge(), _resetEdge()); }; // 切边顺序:上下左右 // 上 cutEdgeContinuous( () => data[0], () => data.shift() ); // 下 cutEdgeContinuous( () => data[data.length - 1], () => data.pop() ); // 左 cutEdgeContinuous( () => data.map(r => r[0]), () => data.forEach(r => r.shift()) ); // 右 cutEdgeContinuous( () => data.map(r => r[r.length - 1]), () => data.forEach(r => r.pop()) ); }
还原二值化数据的缩放
function restoreDataScale(data, scale) { const scaleData = []; let x, y, row; const h = data.length; const w = data[0].length; for (y = 0; y < h; y += scale) { row = []; for (x = 0; x < w; x += scale) { row.push(data[y][x]); } scaleData.push(row); } return scaleData; }
保存模板字符串
就是将处理后的二值化数组,转为字符串形式,方便保存(数据库等)。
function binaryData2Template(data) { return data.map(r => r.join('')).join(' '); }
右侧控制台打印出的就是模板字符串,不过是使用换行符进行每行的分隔。
读取字符截图
上面刚刚介绍了字符截图和处理截图,当中少了读取字符截图这一步。
可以写代码直接读取字符截图的文件夹,一次性处理所有字符截图。
我在做这一步时,是使用input[type=file]
手动每次选择一张字符截图进行处理的(时间紧张),这里贴一下代码。
fileInput.addEventListener('change', e => { // 获取文件 if (fileInput.files.length === 0) return; const file = fileInput.files[0]; const reader = new FileReader(); reader.addEventListener('load', async e => { // e.target.result是图片的base64资源 const imageData = getImageData(e.target.result); const binaryData = binarization(imageData); cutWhiteEdge(binaryData); // 还原之前对图片的放大 const restoreData = restoreDataScale(binaryData, 4); const template = binaryData2Template(restoreData); // 使用clipboard将模板写入剪切板 navigator.clipboard.writeText(template); // 也可以发接口写入数据库... }); reader.readAsDataURL(file); });
FileReader的load事件
二值化阈值调整
经过多次获取验证码、二值化、然后输出查看发现,有些验证码的图片二值化后有的字符被去除了或去除了部分,原因是这些字符的颜色也比较亮。
比如这一张验证码,打印出来是这样的(字符S亮度较高):
此时需要调整阈值(调高一点):
const threshold = [140, 140, 140];
3.卷积比对
上面介绍了如何获取字符模板。在进行卷积比对前,需要处理和保存好所有字符的模板(这是一个辛苦活
标签:字符,code,const,--,验证码,length,data,破解 From: https://www.cnblogs.com/smileZAZ/p/17923451.html