增强:聊天室内修改绑定邮箱时强制要求邮件验证码校验,并增加 60 秒发送频率限制防滥发机制

This commit is contained in:
2026-02-27 10:02:33 +08:00
parent 739ca15cba
commit ea7331dd98
4 changed files with 178 additions and 1 deletions
@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\SysParam;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
class VerificationController extends Controller
{
/**
* 发送绑定邮箱所需的验证码
*/
public function sendEmailCode(Request $request): JsonResponse
{
$request->validate([
'email' => 'required|email'
]);
$email = $request->input('email');
$user = $request->user();
// 1. 检查总控制开关
if (SysParam::where('alias', 'smtp_enabled')->value('body') !== '1') {
return response()->json([
'status' => 'error',
'message' => '抱歉,当前系统未开启外部邮件发信服务,请联系管理员。'
], 403);
}
// 2. 检查是否有频率限制(同一用户或同一邮箱,60秒只允许发1次)
$throttleKey = 'email_throttle_' . $user->id;
if (Cache::has($throttleKey)) {
$ttl = Cache::ttl($throttleKey);
return response()->json([
'status' => 'error',
'message' => "发送过于频繁,请等待 {$ttl} 秒后再试。"
], 429);
}
// 3. 生成 6 位随机验证码并缓存,有效期 5 分钟
$code = mt_rand(100000, 999999);
$codeKey = 'email_verify_code_' . $user->id . '_' . $email;
Cache::put($codeKey, $code, now()->addMinutes(5));
// 设置频率锁,过期时间 60 秒
Cache::put($throttleKey, true, now()->addSeconds(60));
// 4. 执行发信动作
try {
Mail::raw("【飘落的流星】聊天室\n\n您正在试图绑定或修改您的验证邮箱。\n该操作的验证码为:{$code}\n打死不要告诉其他人哦!验证码5分钟内有效。", function ($msg) use ($email) {
$msg->to($email)->subject('飘落流星聊天室 - 绑定邮箱验证码');
});
return response()->json([
'status' => 'success',
'message' => '验证码已发送,请注意查收邮件。'
]);
} catch (\Throwable $e) {
// 如果发信失败,主动接触频率限制锁方便用户下一次立重试
Cache::forget($throttleKey);
return response()->json([
'status' => 'error',
'message' => '邮件系统发送异常,请稍后再试: ' . $e->getMessage()
], 500);
}
}
}
+24
View File
@@ -64,6 +64,30 @@ class UserController extends Controller
{
$user = Auth::user();
$data = $request->validated();
// 当用户试图更新邮箱,并且新邮箱不等于当前旧邮箱时启动验证码拦截
if (isset($data['email']) && $data['email'] !== $user->email) {
// 首先判断系统开关是否开启,没开启直接禁止修改邮箱
if (\App\Models\SysParam::where('alias', 'smtp_enabled')->value('body') !== '1') {
return response()->json(['status' => 'error', 'message' => '系统未开启邮件服务,当前禁止绑定/修改邮箱。'], 403);
}
$emailCode = $request->input('email_code');
if (empty($emailCode)) {
return response()->json(['status' => 'error', 'message' => '新邮箱需要验证码,请先获取并填写验证码。'], 422);
}
// 获取缓存的验证码
$codeKey = 'email_verify_code_' . $user->id . '_' . $data['email'];
$cachedCode = \Illuminate\Support\Facades\Cache::get($codeKey);
if (!$cachedCode || $cachedCode != $emailCode) {
return response()->json(['status' => 'error', 'message' => '验证码不正确或已过期(有效期5分钟),请重新获取。'], 422);
}
// 验证成功后,立即核销该验证码防止二次利用
\Illuminate\Support\Facades\Cache::forget($codeKey);
}
$user->update($data);
@@ -117,9 +117,21 @@
</div>
<div style="display:flex; align-items:center; gap:8px;">
<label style="font-size:12px; width:50px; text-align:right;">邮箱:</label>
<input id="set-email" type="email" value="{{ Auth::user()->email ?? '' }}" placeholder="选填"
<input id="set-email" type="email" value="{{ Auth::user()->email ?? '' }}"
placeholder="用来找回密码(必填)"
style="flex:1; padding:5px; border:1px solid #ccc; border-radius:4px; font-size:12px;">
</div>
@if (\App\Models\SysParam::where('alias', 'smtp_enabled')->value('body') === '1')
<div style="display:flex; align-items:center; gap:8px; margin-top:6px;">
<label style="font-size:12px; width:50px; text-align:right;">验证码:</label>
<input id="set-email-code" type="text" placeholder="修改邮箱时必填" maxlength="6"
style="flex:1; padding:5px; border:1px solid #ccc; border-radius:4px; font-size:12px; max-width:100px;">
<button id="btn-send-code" type="button" onclick="sendEmailCode()"
style="padding:5px 10px; border:1px solid #336699; background:#eef5ff; color:#336699; border-radius:4px; font-size:12px; cursor:pointer;">
获取验证码
</button>
</div>
@endif
</div>
</div>
@@ -203,6 +215,8 @@
const profileData = {
sex: document.getElementById('set-sex').value,
email: document.getElementById('set-email').value,
email_code: document.getElementById('set-email-code') ? document.getElementById('set-email-code')
.value : '',
question: document.getElementById('set-question').value,
answer: document.getElementById('set-answer').value,
headface: @json(Auth::user()->usersf ?: '1.gif'),
@@ -229,4 +243,71 @@
alert('网络异常');
}
}
/**
* 发送邮箱验证码 (带有 60s 倒计时机制防灌水)
*/
async function sendEmailCode() {
const emailInput = document.getElementById('set-email').value.trim();
if (!emailInput) {
alert('请先填写邮箱地址后再获取验证码!');
return;
}
const btn = document.getElementById('btn-send-code');
btn.disabled = true;
btn.innerText = '正在发送...';
btn.style.opacity = '0.6';
btn.style.cursor = 'not-allowed';
try {
const res = await fetch('{{ route('user.send_email_code') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
email: emailInput
})
});
const data = await res.json();
if (res.ok && data.status === 'success') {
alert(data.message || '验证码发送成功,请前往邮箱查收!(有效期5分钟)');
// 开始 60 秒防暴力点击倒计时
let count = 60;
btn.innerText = count + 's 后重试';
const timer = setInterval(() => {
count--;
if (count <= 0) {
clearInterval(timer);
btn.innerText = '获取验证码';
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
} else {
btn.innerText = count + 's 后重试';
}
}, 1000);
} else {
alert('发送失败: ' + (data.message || '系统繁忙'));
// 失败了立刻解除禁用以重新尝试
btn.innerText = '获取验证码';
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
} catch (e) {
alert('网络异常,验证码发送请求失败。');
btn.innerText = '获取验证码';
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
}
</script>
+1
View File
@@ -49,6 +49,7 @@ Route::middleware(['chat.auth'])->group(function () {
// ---- 第七阶段:用户资料与特权管理 ----
Route::get('/user/{username}', [UserController::class, 'show'])->name('user.show');
Route::put('/user/profile', [UserController::class, 'updateProfile'])->name('user.update_profile');
Route::post('/user/send-email-code', [\App\Http\Controllers\Api\VerificationController::class, 'sendEmailCode'])->name('user.send_email_code');
Route::put('/user/password', [UserController::class, 'changePassword'])->name('user.update_password');
Route::post('/user/{username}/kick', [UserController::class, 'kick'])->name('user.kick');
Route::post('/user/{username}/mute', [UserController::class, 'mute'])->name('user.mute');