Next.js 实现网页验证码/CAPTCHA,在网页中加入自制验证码/图灵验证

使用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);
}