签入版本
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# 5.3 验证码机制
|
||||
|
||||
:::tip 概述
|
||||
CaptchaService 提供图片验证码的生成与验证,用于登录等需要人机校验的场景。验证码基于 PHP GD 库生成,采用 6 位字母数字混合字符(去除易混淆字符 0O、1lI),支持防刷限流和万能验证码。
|
||||
CaptchaService 提供图片验证码的生成与验证,用于登录等需要人机校验的场景。验证码基于 PHP GD 库生成,采用 6 位字母数字混合字符(去除易混淆字符 0O、1lI),Redis 存储、Lua 脚本原子校验(一次性),支持防刷限流和万能验证码(仅调试模式生效)。
|
||||
:::
|
||||
|
||||
## 工作流程
|
||||
@@ -13,18 +13,18 @@ CaptchaService 提供图片验证码的生成与验证,用于登录等需要
|
||||
|
||||
前端 后端
|
||||
│ │
|
||||
│── GET /api/captcha ──────────────────►│
|
||||
│ ?clientIp=192.168.1.1 │
|
||||
│── GET /api/captcha ──────────────────►│ (公开接口,无需认证)
|
||||
│ │
|
||||
│ ┌────────┴────────┐
|
||||
│ │ CaptchaService │
|
||||
│ │ CaptchaController│
|
||||
│ │ ::generate() │
|
||||
│ └────────┬────────┘
|
||||
│ │
|
||||
│ ┌────────┴────────┐
|
||||
│ │ 1. 防刷检查 │
|
||||
│ │ checkRateLimit() │
|
||||
│ │ 超限 → 返回等待 │
|
||||
│ │ checkRateLimit( │
|
||||
│ │ request()->ip())│ ← IP 经 ProxyMiddleware 还原
|
||||
│ │ 超限 → 返回等待 │
|
||||
│ └────────┬────────┘
|
||||
│ │
|
||||
│ ┌────────┴────────┐
|
||||
@@ -60,9 +60,9 @@ CaptchaService 提供图片验证码的生成与验证,用于登录等需要
|
||||
前端 后端
|
||||
│ │
|
||||
│── POST /api/login ──────────────────►│
|
||||
│ { username, password, │
|
||||
│ code: "AB3K7M", │
|
||||
│ key: "550e8400-..." } │
|
||||
│ { username, password, │
|
||||
│ code: "AB3K7M", │
|
||||
│ key: "550e8400-..." } │
|
||||
│ │
|
||||
│ ┌────────┴────────┐
|
||||
│ │ CaptchaService │
|
||||
@@ -71,13 +71,17 @@ CaptchaService 提供图片验证码的生成与验证,用于登录等需要
|
||||
│ │
|
||||
│ ┌────────┴────────┐
|
||||
│ │ 1. 万能验证码? │
|
||||
│ │ 匹配 → 直接通过 │
|
||||
│ │ (仅 app.debug) │
|
||||
│ │ 匹配 → 直接通过 │
|
||||
│ └────────┬────────┘
|
||||
│ │
|
||||
│ ┌────────┴────────┐
|
||||
│ │ 2. Redis 查询 │
|
||||
│ │ 2. Lua 原子脚本 │
|
||||
│ │ GETDEL │
|
||||
│ │ captcha:{key} │
|
||||
│ │ 不存在 → 已过期 │
|
||||
│ │ (取值即删除, │
|
||||
│ │ 一次性使用) │
|
||||
│ └────────┬────────┘
|
||||
│ │
|
||||
│ ┌────────┴────────┐
|
||||
@@ -86,17 +90,12 @@ CaptchaService 提供图片验证码的生成与验证,用于登录等需要
|
||||
│ └────────┬────────┘
|
||||
│ │
|
||||
│ ┌────────┴────────┐
|
||||
│ │ 4. 删除缓存 │
|
||||
│ │ 一次性使用 │
|
||||
│ │ 4. 继续登录流程 │
|
||||
│ │ JwtService:: │
|
||||
│ │ login() │
|
||||
│ └────────┬────────┘
|
||||
│ │
|
||||
│ ┌────────┴────────┐
|
||||
│ │ 5. 继续登录流程 │
|
||||
│ │ JwtService:: │
|
||||
│ │ login() │
|
||||
│ └────────┬────────┘
|
||||
│ │
|
||||
│◄── { access_token, ... } ────────────│
|
||||
│◄── { access_token, ... } ─────────────│
|
||||
```
|
||||
|
||||
## 接口说明
|
||||
@@ -124,6 +123,12 @@ GET /api/captcha
|
||||
- `key`:验证码唯一标识(UUID v4 格式),登录时需回传
|
||||
- `captcha`:Base64 编码的图片,可直接用于 `<img src="...">`
|
||||
|
||||
获取超限时:
|
||||
|
||||
```json
|
||||
{ "code": 1, "ok": false, "msg": "请求过于频繁,请 43 秒后重试", "data": null }
|
||||
```
|
||||
|
||||
### 登录时验证
|
||||
|
||||
```bash
|
||||
@@ -138,13 +143,18 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 参数命名约定
|
||||
登录参数 `key` / `code` 与 `/api/captcha` 响应字段命名保持一致(不是 `captchaKey` / `captchaCode`)。
|
||||
:::
|
||||
|
||||
### 错误响应
|
||||
|
||||
| 场景 | code | msg |
|
||||
|------|------|-----|
|
||||
| 验证码过期/不存在 | 1 | 验证码错误 |
|
||||
| 验证码不匹配 | 1 | 验证码错误 |
|
||||
| 获取频率超限 | 1 | 请求过于频繁,请等待 N 秒 |
|
||||
| 获取频率超限 | 1 | 请求过于频繁,请 N 秒后重试 |
|
||||
| 未配置验证码长度(=0) | — | 跳过验证码校验(仅调试用途) |
|
||||
|
||||
:::tip 验证码错误统一提示
|
||||
无论验证码是过期、不存在还是不匹配,统一提示"验证码错误",不暴露具体原因,防止攻击者探测。
|
||||
@@ -155,11 +165,11 @@ Content-Type: application/json
|
||||
```text
|
||||
Redis Key 格式:
|
||||
|
||||
captcha:{uuid-key}
|
||||
captcha:{uuid-key} ← KEY_PREFIX = 'captcha:'
|
||||
值:验证码小写(如 "ab3k7m")
|
||||
TTL:300 秒(5分钟)
|
||||
|
||||
captcha_rate:{client-ip}
|
||||
captcha:rate:{client-ip} ← RATE_KEY_PREFIX = 'captcha:rate:'
|
||||
值:请求次数计数
|
||||
TTL:60 秒(窗口期)
|
||||
```
|
||||
@@ -167,16 +177,19 @@ captcha_rate:{client-ip}
|
||||
```text
|
||||
示例:
|
||||
captcha:550e8400-e29b-41d4-a716-446655440000 → "ab3k7m" (TTL 300s)
|
||||
captcha_rate:192.168.1.1 → "3" (TTL 60s)
|
||||
captcha:rate:192.168.1.1 → "3" (TTL 60s)
|
||||
```
|
||||
|
||||
## 核心代码
|
||||
|
||||
```php
|
||||
// app/service/CaptchaService.php
|
||||
// app/Services/CaptchaService.php(节选)
|
||||
|
||||
class CaptchaService
|
||||
{
|
||||
const KEY_PREFIX = 'captcha:';
|
||||
const RATE_KEY_PREFIX = 'captcha:rate:';
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*
|
||||
@@ -185,33 +198,21 @@ class CaptchaService
|
||||
public static function generate(): array
|
||||
{
|
||||
// 1. 从配置读取字符集和长度(默认 6 位,字符集去除易混淆字符 0O、1lI)
|
||||
$length = (int)env('captcha.captcha_length', 6);
|
||||
$chars = env('captcha.captcha_chars', 'ABCDEFGHJKLMNPQRSTUVWXYZ2346789');
|
||||
$expire = (int)env('captcha.captcha_expire', 300);
|
||||
|
||||
// 2. 生成随机字符
|
||||
$code = '';
|
||||
$charsLen = strlen($chars);
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$code .= $chars[random_int(0, $charsLen - 1)];
|
||||
}
|
||||
$length = config('captcha.length', 6);
|
||||
$chars = config('captcha.chars', 'ABCDEFGHJKLMNPQRSTUVWXYZ2346789');
|
||||
$expire = config('captcha.expire', 300);
|
||||
|
||||
// 2. 生成随机字符(random_int 密码学安全随机)
|
||||
// 3. 生成 UUID v4 格式的唯一标识
|
||||
$key = self::uuid();
|
||||
|
||||
// 4. 存入 Redis(小写存储,5 分钟过期)
|
||||
$redis = self::getRedis();
|
||||
$redis->setex(self::KEY_PREFIX . $key, $expire, strtolower($code));
|
||||
Redis::setex(self::KEY_PREFIX.$key, $expire, strtolower($code));
|
||||
|
||||
// 5. 从配置读取图片尺寸,渲染验证码图片并转为 base64
|
||||
$width = (int)env('captcha.captcha_image_width', 160);
|
||||
$height = (int)env('captcha.captcha_image_height', 50);
|
||||
// 5. 从配置读取图片尺寸,GD 渲染验证码图片并转为 base64
|
||||
$width = config('captcha.image_width', 160);
|
||||
$height = config('captcha.image_height', 50);
|
||||
$imageBase64 = self::renderImage($code, $width, $height);
|
||||
|
||||
return [
|
||||
'key' => $key,
|
||||
'captcha' => $imageBase64,
|
||||
];
|
||||
return ['key' => $key, 'captcha' => $imageBase64];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,43 +223,38 @@ class CaptchaService
|
||||
*/
|
||||
public static function checkRateLimit(string $clientIp): int
|
||||
{
|
||||
$window = (int)env('captcha.captcha_rate_window', 60);
|
||||
$limit = (int)env('captcha.captcha_rate_limit', 10);
|
||||
$window = config('captcha.rate_window', 60);
|
||||
$limit = config('captcha.rate_limit', 10);
|
||||
|
||||
$redis = self::getRedis();
|
||||
$rateKey = self::RATE_KEY_PREFIX . $clientIp;
|
||||
$count = (int)$redis->get($rateKey);
|
||||
$rateKey = self::RATE_KEY_PREFIX.$clientIp;
|
||||
$count = (int) Redis::get($rateKey);
|
||||
|
||||
if ($count >= $limit) {
|
||||
$ttl = $redis->ttl($rateKey);
|
||||
$ttl = Redis::ttl($rateKey);
|
||||
return $ttl > 0 ? $ttl : $window;
|
||||
}
|
||||
|
||||
$redis->incr($rateKey);
|
||||
Redis::incr($rateKey);
|
||||
if ($count === 0) {
|
||||
$redis->expire($rateKey, $window);
|
||||
Redis::expire($rateKey, $window);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证验证码(一次性使用)
|
||||
*
|
||||
* @param string $key 验证码 key
|
||||
* @param string $code 用户输入的验证码
|
||||
* @param bool $deleteVerify 验证后是否删除
|
||||
* @return bool 是否验证通过
|
||||
* 验证验证码(Lua 原子取值即删除,一次性使用)
|
||||
*/
|
||||
public static function verify(string $key, string $code, bool $deleteVerify = true): bool
|
||||
public static function verify(string $key, string $code): bool
|
||||
{
|
||||
if (empty($code)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 万能验证码:配置了且匹配则跳过验证
|
||||
$bypassCode = env('captcha.captcha_bypass_code', '');
|
||||
if (!empty($bypassCode) && strtolower($code) === strtolower($bypassCode)) {
|
||||
// 万能验证码:配置了且匹配则跳过验证(仅 app.debug=true 时生效)
|
||||
$bypassCode = config('captcha.bypass_code', '');
|
||||
if (! empty($bypassCode) && config('app.debug', false)
|
||||
&& strcasecmp($code, $bypassCode) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -266,31 +262,28 @@ class CaptchaService
|
||||
return false;
|
||||
}
|
||||
|
||||
$redis = self::getRedis();
|
||||
$redisKey = self::KEY_PREFIX . $key;
|
||||
$cachedCode = $redis->get($redisKey);
|
||||
|
||||
if ($cachedCode === null || $cachedCode === false) {
|
||||
return false; // 已过期或不存在
|
||||
}
|
||||
|
||||
// 验证后删除,确保一次性使用
|
||||
if ($deleteVerify) {
|
||||
$redis->del($redisKey);
|
||||
// Lua 脚本原子完成"读取并删除",杜绝并发重放
|
||||
$cached = Redis::eval($script, 1, self::KEY_PREFIX.$key);
|
||||
if (! $cached) {
|
||||
return false; // 已过期或不存在
|
||||
}
|
||||
|
||||
// 大小写不敏感比较(Redis 中已存为小写)
|
||||
return strtolower($code) === $cachedCode;
|
||||
return strcasecmp($code, $cached) === 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip Lua 原子性
|
||||
校验使用 `Redis::eval` 执行 Lua 脚本原子完成 GETDEL(取值并删除)——即使两个并发请求携带同一验证码,也只有一个能通过,彻底杜绝验证阶段的重放。
|
||||
:::
|
||||
|
||||
## 前端集成
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-form-item>
|
||||
<el-input v-model="form.captchaCode" placeholder="验证码" />
|
||||
<el-input v-model="form.code" placeholder="验证码" />
|
||||
<img :src="captchaImage" @click="refreshCaptcha" style="cursor: pointer;" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
@@ -299,141 +292,12 @@ class CaptchaService
|
||||
import { getCaptcha } from '@/api/common/user';
|
||||
|
||||
const captchaImage = ref('');
|
||||
const form = ref({ captchaKey: '', captchaCode: '' });
|
||||
const form = ref({ key: '', code: '' });
|
||||
|
||||
const refreshCaptcha = async () => {
|
||||
const res = await getCaptcha();
|
||||
form.value.captchaKey = res.key;
|
||||
captchaImage.value = res.captcha; // 注意:返回字段是 captcha,不是 image
|
||||
};
|
||||
|
||||
onMounted(() => refreshCaptcha());
|
||||
</script>
|
||||
```
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
```ini
|
||||
; .env
|
||||
[CAPTCHA]
|
||||
; 验证码长度(默认 6)
|
||||
CAPTCHA_LENGTH = 6
|
||||
; 字符集(默认已去除易混淆字符 0O、1lI)
|
||||
CAPTCHA_CHARS = ABCDEFGHJKLMNPQRSTUVWXYZ2346789
|
||||
; 过期时间,秒(默认 300)
|
||||
CAPTCHA_EXPIRE = 300
|
||||
; 图片宽度(默认 160)
|
||||
CAPTCHA_IMAGE_WIDTH = 160
|
||||
; 图片高度(默认 50)
|
||||
CAPTCHA_IMAGE_HEIGHT = 50
|
||||
; 字体大小(默认 28)
|
||||
CAPTCHA_FONT_SIZE = 28
|
||||
; 干扰线数量(默认 8)
|
||||
CAPTCHA_NOISE_LINES = 8
|
||||
; 噪点数量(默认 500)
|
||||
CAPTCHA_NOISE_DOTS = 500
|
||||
; 万能验证码(为空时不生效,仅开发/调试环境使用)
|
||||
CAPTCHA_BYPASS_CODE =
|
||||
; 防刷窗口期,秒(默认 60)
|
||||
CAPTCHA_RATE_WINDOW = 60
|
||||
; 窗口期内最大请求数(默认 20)
|
||||
CAPTCHA_RATE_LIMIT = 20
|
||||
```
|
||||
|
||||
## 安全特性
|
||||
|
||||
| 特性 | 说明 | 防御目标 |
|
||||
|------|------|---------|
|
||||
| 一次性使用 | 验证后 `$redis->del()` 删除缓存 | 防止重放攻击 |
|
||||
| 过期机制 | 5 分钟 TTL,超时需重新获取 | 防止长时间尝试破解 |
|
||||
| 大小写不敏感 | 存储转小写,验证时小写比较 | 降低用户输入门槛 |
|
||||
| 去除易混淆字符 | 字符集去掉 0O、1lI | 减少输入错误 |
|
||||
| 防刷限流 | 滑动窗口计数,60 秒内最多 10 次 | 防止暴力获取验证码 |
|
||||
| 万能验证码 | `captcha_bypass_code` 配置 | 开发调试便利 |
|
||||
| UUID v4 key | 验证码标识不可预测 | 防止枚举攻击 |
|
||||
| Base64 图片 | 图片以 Base64 返回 | 避免跨域和 URL 泄露 |
|
||||
|
||||
## 前端集成
|
||||
|
||||
### 登录页完整示例
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-form :model="form" :rules="rules" ref="formRef">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="form.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入密码" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="验证码" prop="code">
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<el-input v-model="form.code" placeholder="请输入验证码"
|
||||
style="flex: 1;" @keyup.enter="handleLogin" />
|
||||
<img :src="captchaImage" @click="refreshCaptcha"
|
||||
style="cursor: pointer; height: 40px;" title="点击刷新" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleLogin" :loading="loading">
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getCaptcha } from '@/api/common/user';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const captchaImage = ref('');
|
||||
|
||||
const form = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
code: '',
|
||||
key: '',
|
||||
});
|
||||
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
// 获取验证码
|
||||
const refreshCaptcha = async () => {
|
||||
const res = await getCaptcha();
|
||||
form.value.key = res.key;
|
||||
captchaImage.value = res.captcha;
|
||||
};
|
||||
|
||||
// 登录
|
||||
const handleLogin = async () => {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
await userStore.login(form.value);
|
||||
ElMessage.success('登录成功');
|
||||
router.push('/');
|
||||
} catch (e: any) {
|
||||
// 登录失败(包括验证码错误),刷新验证码
|
||||
ElMessage.error(e.message || '登录失败');
|
||||
refreshCaptcha();
|
||||
form.value.code = '';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
captchaImage.value = res.captcha; // 返回字段是 captcha(Base64),不是 image
|
||||
};
|
||||
|
||||
onMounted(() => refreshCaptcha());
|
||||
@@ -446,35 +310,73 @@ onMounted(() => refreshCaptcha());
|
||||
|------|------|
|
||||
| 页面加载时 | `onMounted` 自动获取 |
|
||||
| 点击图片时 | 用户手动刷新 |
|
||||
| 登录失败后 | 自动刷新并清空输入 |
|
||||
| 登录失败后 | 自动刷新并清空输入(一次性已消费) |
|
||||
| 验证码过期后 | 用户点击图片重新获取 |
|
||||
|
||||
## 配置项
|
||||
|
||||
```ini
|
||||
# .env(config/captcha.php 读取)
|
||||
CAPTCHA_LENGTH=6 # 验证码字符长度(4-6 位)
|
||||
CAPTCHA_CHARS=ABCDEFGHJKLMNPQRSTUVWXYZ2346789 # 字符集(已排除 0O、1lI)
|
||||
CAPTCHA_EXPIRE=300 # 有效期(秒),默认 5 分钟
|
||||
CAPTCHA_IMAGE_WIDTH=160 # 图片宽度
|
||||
CAPTCHA_IMAGE_HEIGHT=50 # 图片高度
|
||||
CAPTCHA_FONT_SIZE=28 # 字体大小
|
||||
CAPTCHA_FONT_PATH=static/fonts/Vera.ttf # 字体路径(public/ 下)
|
||||
CAPTCHA_NOISE_LINES=6 # 干扰线数量
|
||||
CAPTCHA_NOISE_DOTS=300 # 噪点数量
|
||||
CAPTCHA_RATE_WINDOW=60 # 防刷窗口期(秒)
|
||||
CAPTCHA_RATE_LIMIT=10 # 窗口期内最大获取次数
|
||||
CAPTCHA_BYPASS_CODE= # 万能验证码(仅 APP_DEBUG=true 时生效)
|
||||
```
|
||||
|
||||
:::warning 万能验证码安全边界
|
||||
`CAPTCHA_BYPASS_CODE` 双重保护:**仅 `APP_DEBUG=true` 时生效**,且生产环境必须留空。即使误配,`APP_DEBUG=false` 也会自动失效。
|
||||
:::
|
||||
|
||||
## 安全特性
|
||||
|
||||
| 特性 | 说明 | 防御目标 |
|
||||
|------|------|---------|
|
||||
| 原子一次性 | Lua GETDEL 取值即删除 | 防止并发重放 |
|
||||
| 过期机制 | 5 分钟 TTL,超时需重新获取 | 防止长时间尝试破解 |
|
||||
| 大小写不敏感 | 存储转小写,验证时忽略大小写 | 降低用户输入门槛 |
|
||||
| 去除易混淆字符 | 字符集去掉 0O、1lI | 减少输入错误 |
|
||||
| 防刷限流 | 滑动窗口计数,60 秒内最多 10 次/IP | 防止暴力获取验证码 |
|
||||
| 万能验证码 | 配置 + `app.debug` 双重开关 | 开发调试便利、生产安全 |
|
||||
| UUID v4 key | 验证码标识不可预测 | 防止枚举攻击 |
|
||||
| Base64 图片 | 图片以 Base64 返回 | 避免跨域和 URL 泄露 |
|
||||
| 真实 IP 限流 | `request()->ip()`(ProxyMiddleware 还原) | 防止伪造 IP 头绕过(受信代理校验) |
|
||||
|
||||
## 环境配置建议
|
||||
|
||||
### 开发环境
|
||||
|
||||
```ini
|
||||
[CAPTCHA]
|
||||
; 万能验证码,方便调试(登录时输入此码即可跳过验证码)
|
||||
CAPTCHA_BYPASS_CODE = 888888
|
||||
; 验证码长度可以短一些
|
||||
CAPTCHA_LENGTH = 4
|
||||
# 万能验证码,方便调试(登录时输入此码即可跳过验证码,仅 debug 生效)
|
||||
CAPTCHA_BYPASS_CODE=888888
|
||||
# 验证码长度可以短一些
|
||||
CAPTCHA_LENGTH=4
|
||||
```
|
||||
|
||||
### 生产环境
|
||||
|
||||
```ini
|
||||
[CAPTCHA]
|
||||
; 不设置万能验证码(留空)
|
||||
CAPTCHA_BYPASS_CODE =
|
||||
; 标准 6 位验证码
|
||||
CAPTCHA_LENGTH = 6
|
||||
; 适当降低防刷限制
|
||||
CAPTCHA_RATE_WINDOW = 60
|
||||
CAPTCHA_RATE_LIMIT = 10
|
||||
; 图片尺寸和干扰强度
|
||||
CAPTCHA_IMAGE_WIDTH = 160
|
||||
CAPTCHA_IMAGE_HEIGHT = 50
|
||||
CAPTCHA_NOISE_LINES = 8
|
||||
CAPTCHA_NOISE_DOTS = 500
|
||||
```
|
||||
CAPTCHA_BYPASS_CODE= # 必须留空
|
||||
CAPTCHA_LENGTH=6
|
||||
CAPTCHA_RATE_WINDOW=60
|
||||
CAPTCHA_RATE_LIMIT=10
|
||||
CAPTCHA_NOISE_LINES=6
|
||||
CAPTCHA_NOISE_DOTS=300
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
| 现象 | 原因 | 处理 |
|
||||
|------|------|------|
|
||||
| 验证码接口 500 | Redis 未连接 / phpredis 扩展缺失 | 见 [2.2 Laragon 环境搭建](/zh/quickstart/laragon.html) Redis 章节 |
|
||||
| 图片显示空白 | 字体文件缺失 | 确认 `public/static/fonts/Vera.ttf` 存在 |
|
||||
| 一直提示验证码错误 | 前端传参名错误 | 必须是 `key` / `code`(非 captchaKey/captchaCode) |
|
||||
| 频繁提示"请求过于频繁" | 同 IP 短时间多次获取 | 等待窗口期结束,或调整 `CAPTCHA_RATE_LIMIT` |
|
||||
| 万能验证码不生效 | `APP_DEBUG=false` 或未配置 | 仅调试环境可用,生产无法启用 |
|
||||
|
||||
Reference in New Issue
Block a user