- 前台:问题列表 / 详情 / 发布与回复(趣味问答验证码、蜜罐、IP 限流) - 后台:先审后显、站点设置(站点标题/描述、附件存储、OSS 参数)、操作日志 - 附件:关闭 / 本机硬盘 / 阿里云 OSS 浏览器直传(ECS 实例 RAM 角色)三种模式 - 品牌资源:全套图标(favicon.ico / SVG / PNG 各尺寸 / manifest) - 安全:CSRF、session 加固、.htaccess 屏蔽敏感文件、口令仅存哈希 - 配置:config.example.php 为模板,config.php 不入库
131 lines
5.3 KiB
PHP
131 lines
5.3 KiB
PHP
<?php
|
||
/**
|
||
* 人机验证:趣味问答
|
||
* ------------------------------------------------------------------
|
||
* 无需外部服务、无需图片库。每次生成一道随机小题(算术为主,
|
||
* 混入日历 / 生活常识题),答案存放在 Session 中,一次性使用。
|
||
*/
|
||
if (!defined('FB_ROOT')) { http_response_code(403); exit('forbidden'); }
|
||
|
||
/** 静态趣味题库:[题目, 可接受的答案(数组,忽略大小写与标点)] */
|
||
function fb_captcha_bank() {
|
||
return [
|
||
['一年有几个月?', ['12', '十二']],
|
||
['闰年的二月有多少天?', ['29', '二十九']],
|
||
['一星期有几天?', ['7', '七']],
|
||
['一年有几个季节?', ['4', '四']],
|
||
['一个季度有几个月?', ['3', '三']],
|
||
['时针从 3 点走到 9 点,一共走了几个小时?', ['6', '六']],
|
||
['钟表从 12 点顺时针走 3 个小时,是几点?', ['3', '三点', '3点']],
|
||
['把 「789」 倒过来念,是多少?', ['987']],
|
||
['「轻反馈」这几个字,一共几个字?', ['3', '三']],
|
||
['汉字「五」表示的数字是几?', ['5', '五']],
|
||
['2、4、6、8、10 之后的下一个数是多少?', ['12', '十二']],
|
||
['从 10 倒着数,10 之后是几?', ['9', '九']],
|
||
['「日月」两个字里,有几个「日」字?', ['1', '一']],
|
||
['把 3 个 6 相加,结果是多少?', ['18', '十八']],
|
||
['100 减去 100 等于多少?', ['0', '零']],
|
||
['星期三之后是星期几?', ['4', '四', '星期四', '周四']],
|
||
['把 4 个苹果平均分给 2 个人,每人几个?', ['2', '二', '两']],
|
||
['「日历」这个词的第一个字是什么?', ['日']],
|
||
];
|
||
}
|
||
|
||
/** 生成一道动态算术题 */
|
||
function fb_captcha_arith() {
|
||
$op = random_int(1, 4);
|
||
if ($op === 1) {
|
||
$a = random_int(3, 30); $b = random_int(2, 30);
|
||
return ['题目: ' . $a . ' 加 ' . $b . ' 等于多少?', [(string)($a + $b)]];
|
||
}
|
||
if ($op === 2) {
|
||
$a = random_int(10, 40); $b = random_int(1, 9);
|
||
if ($b > $a) { $t = $a; $a = $b; $b = $t; }
|
||
return ['题目: ' . $a . ' 减 ' . $b . ' 等于多少?', [(string)($a - $b)]];
|
||
}
|
||
if ($op === 3) {
|
||
$a = random_int(2, 9); $b = random_int(2, 9);
|
||
return ['题目: ' . $a . ' 乘以 ' . $b . ' 等于多少?', [(string)($a * $b)]];
|
||
}
|
||
// 先乘后加
|
||
$a = random_int(1, 5); $b = random_int(2, 6); $c = random_int(2, 6);
|
||
return ['题目: ' . $a . ' 加 ' . $b . ' 乘 ' . $c . ' 等于多少?(先乘后加)', [(string)($a + $b * $c)]];
|
||
}
|
||
|
||
/** 归一化答案:全角转半角、去空白与常见标点、转小写 */
|
||
function fb_captcha_norm($s) {
|
||
$s = (string)$s;
|
||
if (function_exists('mb_convert_kana')) {
|
||
$s = mb_convert_kana($s, 'as', 'UTF-8'); // 全角字母/数字/空格 -> 半角
|
||
}
|
||
$s = str_replace(
|
||
['?', '?', '。', '.', '.', ',', ',', '!', '!', '、', ':', ':', ';', ';', '"', "'", '“', '”', '‘', '’', ' ', ' ', "\r", "\n", "\t"],
|
||
'',
|
||
$s
|
||
);
|
||
if (function_exists('mb_strtolower')) {
|
||
$s = mb_strtolower($s, 'UTF-8');
|
||
} else {
|
||
$s = strtolower($s);
|
||
}
|
||
return $s;
|
||
}
|
||
|
||
/** 生成新的验证题,写入 Session,返回题面文本 */
|
||
function fb_captcha_new() {
|
||
if (random_int(1, 100) <= 65) {
|
||
list($q, $answers) = fb_captcha_arith();
|
||
} else {
|
||
$bank = fb_captcha_bank();
|
||
list($q, $answers) = $bank[array_rand($bank)];
|
||
}
|
||
$_SESSION['fb_cap'] = [
|
||
'q' => $q,
|
||
'answers'=> $answers,
|
||
'ts' => time(),
|
||
'tries' => 0,
|
||
];
|
||
return $q;
|
||
}
|
||
|
||
/** 取当前题目(不存在则新建) */
|
||
function fb_captcha_question() {
|
||
if (empty($_SESSION['fb_cap']['q']) || !empty($_SESSION['fb_cap']['used'])) {
|
||
return fb_captcha_new();
|
||
}
|
||
// 超过 30 分钟重新出题
|
||
if (time() - (int)$_SESSION['fb_cap']['ts'] > 1800) {
|
||
return fb_captcha_new();
|
||
}
|
||
return (string)$_SESSION['fb_cap']['q'];
|
||
}
|
||
|
||
/** 校验用户答案;一次性使用,答错累计,超过 5 次作废需换题 */
|
||
function fb_captcha_check($input) {
|
||
if (empty($_SESSION['fb_cap']['answers'])) { return false; }
|
||
$cap = $_SESSION['fb_cap'];
|
||
if (time() - (int)$cap['ts'] > 1800) {
|
||
unset($_SESSION['fb_cap']);
|
||
return false;
|
||
}
|
||
$n = fb_captcha_norm($input);
|
||
$ok = false;
|
||
if ($n !== '') {
|
||
foreach ((array)$cap['answers'] as $a) {
|
||
$an = fb_captcha_norm($a);
|
||
if ($an === '') { continue; }
|
||
if ($n === $an) { $ok = true; break; }
|
||
if (is_numeric($n) && is_numeric($an) && (float)$n === (float)$an) { $ok = true; break; }
|
||
}
|
||
}
|
||
$_SESSION['fb_cap']['tries'] = (int)$cap['tries'] + 1;
|
||
if ($ok) {
|
||
unset($_SESSION['fb_cap']); // 用后即焚
|
||
return true;
|
||
}
|
||
if ($_SESSION['fb_cap']['tries'] >= 5) {
|
||
unset($_SESSION['fb_cap']); // 作废,下次自动换题
|
||
}
|
||
return false;
|
||
}
|