feat: SVG image captcha with noise and color distortion
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const svgCaptcha = require('svg-captcha');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const { query } = require('../db');
|
const { query } = require('../db');
|
||||||
|
|
||||||
@@ -7,16 +8,19 @@ const router = express.Router();
|
|||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await query("DELETE FROM captchas WHERE created_at < NOW() - INTERVAL 5 MINUTE");
|
await query("DELETE FROM captchas WHERE created_at < NOW() - INTERVAL 5 MINUTE");
|
||||||
|
const captcha = svgCaptcha.create({
|
||||||
|
size: 4,
|
||||||
|
noise: 3,
|
||||||
|
color: true,
|
||||||
|
background: '#f4f4f4',
|
||||||
|
width: 120,
|
||||||
|
height: 44,
|
||||||
|
fontSize: 40,
|
||||||
|
ignoreChars: '0oO1iIl',
|
||||||
|
});
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
const ops = ['+', '-', '\u00d7'];
|
await query('INSERT INTO captchas(id, question, answer) VALUES (?,?,?)', [id, '', captcha.text.toLowerCase()]);
|
||||||
const op = ops[Math.floor(Math.random() * ops.length)];
|
res.json({ id, svg: captcha.data });
|
||||||
let a, b;
|
|
||||||
if (op === '\u00d7') { a = Math.floor(Math.random() * 9) + 1; b = Math.floor(Math.random() * 9) + 1; }
|
|
||||||
else { a = Math.floor(Math.random() * 20) + 1; b = Math.floor(Math.random() * 10) + 1; if (op === '-' && a < b) [a, b] = [b, a]; }
|
|
||||||
const answer = op === '+' ? a + b : op === '-' ? a - b : a * b;
|
|
||||||
const question = `${a} ${op} ${b} = ?`;
|
|
||||||
await query('INSERT INTO captchas(id, question, answer) VALUES (?,?,?)', [id, question, String(answer)]);
|
|
||||||
res.json({ id, question });
|
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,7 +28,7 @@ async function verify(id, answer) {
|
|||||||
const rows = await query("SELECT * FROM captchas WHERE id = ? AND created_at > NOW() - INTERVAL 5 MINUTE", [id]);
|
const rows = await query("SELECT * FROM captchas WHERE id = ? AND created_at > NOW() - INTERVAL 5 MINUTE", [id]);
|
||||||
if (!rows.length) return false;
|
if (!rows.length) return false;
|
||||||
await query('DELETE FROM captchas WHERE id = ?', [id]);
|
await query('DELETE FROM captchas WHERE id = ?', [id]);
|
||||||
return String(answer).trim() === rows[0].answer;
|
return String(answer).trim().toLowerCase() === rows[0].answer;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
31
package-lock.json
generated
31
package-lock.json
generated
@@ -18,6 +18,7 @@
|
|||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.11.0",
|
"mysql2": "^3.11.0",
|
||||||
"nodemailer": "^6.9.8",
|
"nodemailer": "^6.9.8",
|
||||||
|
"svg-captcha": "^1.4.0",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"xss": "^1.0.15"
|
"xss": "^1.0.15"
|
||||||
}
|
}
|
||||||
@@ -996,6 +997,18 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/opentype.js": {
|
||||||
|
"version": "0.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-0.7.3.tgz",
|
||||||
|
"integrity": "sha512-Veui5vl2bLonFJ/SjX/WRWJT3SncgiZNnKUyahmXCc2sa1xXW15u3R/3TN5+JFiP7RsjK5ER4HA5eWaEmV9deA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tiny-inflate": "^1.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ot": "bin/ot"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -1263,6 +1276,24 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svg-captcha": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg-captcha/-/svg-captcha-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-/fkkhavXPE57zRRCjNqAP3txRCSncpMx3NnNZL7iEoyAtYwUjPhJxW6FQTQPG5UPEmCrbFoXS10C3YdJlW7PDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"opentype.js": "^0.7.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tiny-inflate": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/toidentifier": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -7,17 +7,18 @@
|
|||||||
"start": "node backend/server.js"
|
"start": "node backend/server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mysql2": "^3.11.0",
|
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express-validator": "^7.0.1",
|
"express": "^4.18.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
|
||||||
"nodemailer": "^6.9.8",
|
|
||||||
"uuid": "^9.0.0",
|
|
||||||
"helmet": "^7.1.0",
|
|
||||||
"express-rate-limit": "^7.3.1",
|
"express-rate-limit": "^7.3.1",
|
||||||
|
"express-validator": "^7.0.1",
|
||||||
|
"helmet": "^7.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"mysql2": "^3.11.0",
|
||||||
|
"nodemailer": "^6.9.8",
|
||||||
|
"svg-captcha": "^1.4.0",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
"xss": "^1.0.15"
|
"xss": "^1.0.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,13 @@ const RegisterPage = {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group"><label>密码 *</label><input type="password" id="rp" required placeholder="至少6位"></div>
|
<div class="form-group"><label>密码 *</label><input type="password" id="rp" required placeholder="至少6位"></div>
|
||||||
${this.captcha ? `
|
${this.captcha ? `
|
||||||
<div class="form-group" style="display:flex;align-items:center;gap:10px">
|
<div class="form-group"><label>验证码</label>
|
||||||
<div style="flex-shrink:0;background:var(--g100);padding:6px 14px;border-radius:var(--r);font-weight:700;font-size:16px;letter-spacing:1px;user-select:none" id="captcha-q">${this.captcha.question}</div>
|
<div style="display:flex;align-items:center;gap:10px">
|
||||||
<input type="text" id="captcha-a" placeholder="输入答案" required style="max-width:100px">
|
<div id="captcha-img" style="flex-shrink:0;border:1px solid var(--g200);border-radius:var(--r);overflow:hidden;cursor:pointer" onclick="RegisterPage.refreshCaptcha()" title="点击刷新">${this.captcha.svg}</div>
|
||||||
<input type="hidden" id="captcha-id" value="${this.captcha.id}">
|
<input type="text" id="captcha-a" placeholder="输入图片字符" required style="max-width:100px">
|
||||||
|
<input type="hidden" id="captcha-id" value="${this.captcha.id}">
|
||||||
|
</div>
|
||||||
|
<div class="help">点击图片刷新验证码</div>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
<div id="regerr" class="alert alert-e hidden"></div>
|
<div id="regerr" class="alert alert-e hidden"></div>
|
||||||
<div id="regok" class="alert alert-s hidden"></div>
|
<div id="regok" class="alert alert-s hidden"></div>
|
||||||
@@ -34,6 +37,15 @@ const RegisterPage = {
|
|||||||
</div>`;
|
</div>`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async refreshCaptcha() {
|
||||||
|
try {
|
||||||
|
const c = await API.get('/captcha');
|
||||||
|
document.getElementById('captcha-img').innerHTML = c.svg;
|
||||||
|
document.getElementById('captcha-id').value = c.id;
|
||||||
|
document.getElementById('captcha-a').value = '';
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
|
||||||
mount() {
|
mount() {
|
||||||
document.getElementById('reg-form').addEventListener('submit', async e => {
|
document.getElementById('reg-form').addEventListener('submit', async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
Reference in New Issue
Block a user