feat: 忘记密码升级为智能分流向导并部署IP/账号双层防扫防轰炸限流保护
This commit is contained in:
@@ -19,6 +19,7 @@ use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
@@ -37,6 +38,64 @@ class PasswordResetController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号检测接口:根据昵称检测是否绑定微信或邮箱,并提供分流依据(支持 IP 防扫限流)。
|
||||
*/
|
||||
public function checkAccount(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'username' => 'required|string|max:100',
|
||||
]);
|
||||
|
||||
$username = trim((string) $request->input('username'));
|
||||
$ip = $request->ip();
|
||||
|
||||
// IP 防扫描节流限制:每个 IP 每 1 分钟最多请求 5 次
|
||||
$ipKey = 'pw-check:ip:'.$ip;
|
||||
if (RateLimiter::tooManyAttempts($ipKey, 5)) {
|
||||
$seconds = RateLimiter::availableIn($ipKey);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => "账号检测请求过于频繁,请在 {$seconds} 秒后重试。",
|
||||
], 429);
|
||||
}
|
||||
RateLimiter::hit($ipKey, 60);
|
||||
|
||||
$user = User::query()->where('username', $username)->first();
|
||||
if (! $user) {
|
||||
return response()->json([
|
||||
'status' => 'not_found',
|
||||
'message' => '抱歉,没有找到该昵称对应的账号。请确认后再试。',
|
||||
]);
|
||||
}
|
||||
|
||||
$hasEmail = ! empty($user->email);
|
||||
$hasWechat = ! empty($user->wxid);
|
||||
|
||||
// 对邮箱地址进行安全脱敏(如 pllx@ay.lc -> p***x@ay.lc)
|
||||
$maskedEmail = '';
|
||||
if ($hasEmail) {
|
||||
$parts = explode('@', $user->email);
|
||||
$name = $parts[0] ?? '';
|
||||
$domain = $parts[1] ?? '';
|
||||
$len = strlen($name);
|
||||
if ($len <= 2) {
|
||||
$maskedEmail = substr($name, 0, 1).'*'.'@'.$domain;
|
||||
} else {
|
||||
$maskedEmail = substr($name, 0, 1).str_repeat('*', $len - 2).substr($name, -1).'@'.$domain;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'username' => $user->username,
|
||||
'has_email' => $hasEmail,
|
||||
'has_wechat' => $hasWechat,
|
||||
'masked_email' => $maskedEmail,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮箱找回密码链接。
|
||||
*/
|
||||
@@ -49,7 +108,42 @@ class PasswordResetController extends Controller
|
||||
], 403);
|
||||
}
|
||||
|
||||
$email = trim((string) $request->string('email'));
|
||||
$email = trim((string) $request->input('email', ''));
|
||||
$username = trim((string) $request->input('username', ''));
|
||||
$ip = $request->ip();
|
||||
|
||||
if ($username !== '') {
|
||||
$user = User::query()->where('username', $username)->first();
|
||||
if (! $user || empty($user->email)) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => '找不到绑定了邮箱的账号。',
|
||||
], 422);
|
||||
}
|
||||
$email = $user->email;
|
||||
}
|
||||
|
||||
// 1. IP 级别发信限流:限制单个 IP 每分钟最多请求 2 次
|
||||
$ipKey = 'pw-email:ip:'.$ip;
|
||||
if (RateLimiter::tooManyAttempts($ipKey, 2)) {
|
||||
$seconds = RateLimiter::availableIn($ipKey);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => "发送验证链接过于频繁,请在 {$seconds} 秒后重试。",
|
||||
], 429);
|
||||
}
|
||||
|
||||
// 2. 账号邮箱级别冷却锁:同一个邮箱每 3 分钟限发 1 次,防止狂刷邮件轰炸他人
|
||||
$targetKey = 'pw-email:target:'.md5($email);
|
||||
if (RateLimiter::tooManyAttempts($targetKey, 1)) {
|
||||
$seconds = RateLimiter::availableIn($targetKey);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => "该账号申请重置链接过于频繁,请在 {$seconds} 秒后重试。",
|
||||
], 429);
|
||||
}
|
||||
|
||||
// 邮箱找回必须保证一邮一号,否则重置目标会产生歧义。
|
||||
if (User::query()->where('email', $email)->count() > 1) {
|
||||
@@ -59,6 +153,10 @@ class PasswordResetController extends Controller
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 记录发信请求频率
|
||||
RateLimiter::hit($ipKey, 60);
|
||||
RateLimiter::hit($targetKey, 180);
|
||||
|
||||
$status = Password::sendResetLink(['email' => $email]);
|
||||
|
||||
if ($status === Password::RESET_LINK_SENT) {
|
||||
|
||||
@@ -31,7 +31,8 @@ class SendPasswordResetLinkRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
'email' => ['required_without:username', 'nullable', 'email', 'max:255'],
|
||||
'username' => ['required_without:email', 'nullable', 'string', 'max:100'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -43,9 +44,9 @@ class SendPasswordResetLinkRequest extends FormRequest
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'email.required' => '请输入已绑定账号的邮箱地址。',
|
||||
'email.email' => '邮箱格式不正确,请重新输入。',
|
||||
'email.max' => '邮箱长度不能超过 255 个字符。',
|
||||
'email.required_without' => '请输入绑定邮箱或用户昵称。',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user