使用React Next.js 14生成图像验证码并返回图像给客户端。无需第三方接口,直接本地生成,免费开源。生成结果为base64图像,可以直接放入src使用。
代码思路:
等客户端提交数据后,判断答案是否正确。如果自己有服务器的话可以把答案存在本地,如果是用的serverless 的话,可以把答案上传到数据库,或者做其他处理。
整体生成思路非常简单,使用canvas,随机生成数字,并进行随机变形,旋转,缩放,改变字体。之后再随机画一些曲线和直线进行干扰,最后降低图像对比度,增加识别难度。亲测生成结果百度(包括文心一言),谷歌(包括Bard),OpenAI,以及网上找到的OCR都无法识别。
可以根据自己的需求,调整验证码长度,调整随机变形的程度。我删除了0,O,o,I,l这几个连人类都难以辨认的字符,因为我体验过根本无法区分这几个字符而无法通过验证码的感觉,只感觉网站制作者是一坨**,所以轮到我制作的时候不会犯这个错误。
私认为此方法比谷歌验证码以及其他验证码接口都更好,一是人类可以非常简单辨认,不像谷歌或者京东的验证码,异常冗杂,有些我都看不懂是在干嘛。二是现有的OCR和AI完全无法识别。三是成本低,完全免费,单纯使用canvas和几个随机数,对服务器压力相对较小。
注释是英文的,但是是原创,别问为什么,问就是我想装逼。
import {createCanvas} from 'canvas'; export async function GET() { const canvas = createCanvas(400, 200); const context = canvas.getContext('2d'); context.fillStyle = getRandomColor(); // You can change 'lightgray' to any color you want context.fillRect(0, 0, canvas.width, canvas.height); const canvasWidth = canvas.width; const canvasHeight = canvas.height; let answer = '', tempLetter = ''; for (let i = 0; i < 6; i++) { const fontFamilies = ["Arial", "Bradley Hand ITC", "Century", "Century Gothic", "Comic Sans MS", "Courier", "Courier New", "Cursive", "fantasy", "Georgia", "Lucida Sans Unicode", "Papyrus", "Tahoma", "Times New Roman", "Trebuchet MS", "Verdana", "serif", "sans-serif", "monospace", "cursive", "fantasy"]; const randomFontSize = `${65 + 60 * Math.random()}px`; const randomFontFamily = fontFamilies[Math.floor(Math.random() * fontFamilies.length)]; context.font = randomFontSize + " " + randomFontFamily; context.fillStyle = getRandomColor(); tempLetter = generateCaptchaText(); answer += tempLetter; context.rotate(-0.7 + Math.random()*1.2); context.setTransform(Math.random() * 0.12 + 0.88, Math.random() * 0.25, Math.random() * 0.25, Math.random() * 0.15 + 0.85, Math.random() * 0.25, Math.random() * 0.25);//distortion, can change the value. Reduce the random range to make the image easier to recognize context.fillText(tempLetter, 10 + (50 + Math.random() * 5) * i, 75); context.setTransform(1, 0, 0, 1, 0, 0); } for (let i = 0; i < 3; i++) {//add some random curves context.beginPath(); context.moveTo(Math.random() * 20, Math.random() * 200); for (let j = 0; j < 5; j++) { const curveX = 10 + (45 + Math.random() * 15) * j + Math.random() * 30; const curveY = 75 + Math.random() * 30; const endX = 75 * j + Math.random() * 25 * j; const endY = Math.random() * 200; context.bezierCurveTo(curveX, curveY, curveX + 10, curveY + 10, endX, endY); } context.lineWidth = Math.random() * 3; context.strokeStyle = getRandomColor(); context.stroke(); } for (let i = 0; i < 6; i++) {//add some random lines context.beginPath(); context.moveTo(Math.random() * canvasWidth, Math.random() * canvasHeight); context.lineTo(Math.random() * canvasWidth, Math.random() * canvasHeight); context.lineWidth = Math.random() * 3; context.strokeStyle = getRandomColor(); context.stroke(); } adjustGlobalContrast(canvas, 0.4); // Convert the canvas to a DataURL, which is base64 const imageSrc = canvas.toDataURL(); console.log(answer) return new Response(imageSrc, { status: 200, headers: { 'Content-Type': 'image/png', "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "*", "Access-Control-Allow-Headers": "*", "Access-Control-Allow-Credentials": "true" } }); } function getRandomColor() { const letters = "0123456789ABCDEF"; let color = "#"; for (let i = 0; i < 6; i++) { color += letters[Math.floor(Math.random() * 16)]; } return color; } function generateCaptchaText() { const chars = "123456789abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"; return chars[Math.floor(Math.random() * chars.length)]; } function adjustGlobalContrast(canvas, contrast) { const context = canvas.getContext('2d'); // Get the image data const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; // Adjust contrast globally for (let i = 0; i < data.length; i += 4) { // R, G, and B values const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; // Adjust each color channel based on contrast data[i] = (r - 128) * contrast + 128; data[i + 1] = (g - 128) * contrast + 128; data[i + 2] = (b - 128) * contrast + 128; } // Put the modified image data back on the canvas context.putImageData(imageData, 0, 0); }