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