Files
chatroom/app/Http/Controllers/PasswordResetController.php
T

252 lines
8.7 KiB
PHP
Raw Normal View History

2026-04-19 16:10:41 +08:00
<?php
/**
* 文件功能:前台邮箱找回密码控制器
*
* 提供独立的找回密码页、发送邮箱重置链接、展示重置页以及提交新密码功能。
*/
namespace App\Http\Controllers;
use App\Http\Requests\ResetPasswordRequest;
use App\Http\Requests\SendPasswordResetLinkRequest;
use App\Models\Sysparam;
use App\Models\User;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\RateLimiter;
2026-04-19 16:10:41 +08:00
use Illuminate\Support\Str;
/**
* 类功能:处理首页邮箱找回密码的完整流程。
*/
class PasswordResetController extends Controller
{
/**
* 展示独立的邮箱找回密码页面。
*/
public function create(): View
{
return view('password-forgot', [
'systemName' => Sysparam::where('alias', 'sys_name')->value('body') ?? '和平聊吧',
'smtpEnabled' => $this->isPasswordResetMailEnabled(),
]);
}
/**
* 账号检测接口:根据昵称检测是否绑定微信或邮箱,并提供分流依据(支持 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,
]);
}
2026-04-19 16:10:41 +08:00
/**
* 发送邮箱找回密码链接。
*/
public function storeLink(SendPasswordResetLinkRequest $request): JsonResponse
{
if (! $this->isPasswordResetMailEnabled()) {
return response()->json([
'status' => 'error',
'message' => '系统暂未开启邮箱发信服务,当前无法通过邮箱找回密码。',
], 403);
}
$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);
}
2026-04-19 16:10:41 +08:00
// 邮箱找回必须保证一邮一号,否则重置目标会产生歧义。
if (User::query()->where('email', $email)->count() > 1) {
return response()->json([
'status' => 'error',
'message' => '该邮箱绑定了多个账号,暂不支持自助找回,请联系管理员处理。',
], 422);
}
// 记录发信请求频率
RateLimiter::hit($ipKey, 60);
RateLimiter::hit($targetKey, 180);
2026-04-19 16:10:41 +08:00
$status = Password::sendResetLink(['email' => $email]);
if ($status === Password::RESET_LINK_SENT) {
return response()->json([
'status' => 'success',
'message' => '如果该邮箱已绑定账号,系统已发送重置邮件。链接 60 分钟内有效,请注意查收。',
]);
}
if ($status === Password::RESET_THROTTLED) {
return response()->json([
'status' => 'error',
'message' => '发送过于频繁,请稍后再试。',
], 429);
}
if ($status === Password::INVALID_USER) {
return response()->json([
'status' => 'success',
'message' => '如果该邮箱已绑定账号,系统已发送重置邮件。请检查收件箱与垃圾邮件箱。',
]);
}
return response()->json([
'status' => 'error',
'message' => '找回密码邮件发送失败,请稍后重试。',
], 500);
}
/**
* 展示独立的重置密码页面。
*/
public function edit(Request $request, string $token): View
{
return view('password-reset', [
'systemName' => Sysparam::where('alias', 'sys_name')->value('body') ?? '和平聊吧',
'token' => $token,
'email' => (string) $request->query('email', ''),
]);
}
/**
* 提交新的登录密码并完成重置。
*/
public function update(ResetPasswordRequest $request): RedirectResponse
{
$credentials = $request->validated();
$status = Password::reset(
$credentials,
function (User $user, #[\SensitiveParameter] string $password): void {
// 重置成功后同步刷新 remember_token,避免旧设备继续沿用旧令牌。
$user->forceFill([
'password' => Hash::make($password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
if ($status === Password::PASSWORD_RESET) {
return redirect()->route('home')->with('status', '密码已重置成功,请使用原昵称和新密码重新登录。');
}
return back()
->withInput($request->except('password', 'password_confirmation'))
->withErrors([
'email' => $this->resolveResetFailureMessage($status),
]);
}
/**
* 判断系统是否已开启邮箱发信服务。
*/
private function isPasswordResetMailEnabled(): bool
{
return Sysparam::where('alias', 'smtp_enabled')->value('body') === '1';
}
/**
* 将 Laravel 密码重置状态码转换为中文错误提示。
*/
private function resolveResetFailureMessage(string $status): string
{
return match ($status) {
Password::INVALID_TOKEN => '重置链接无效或已过期,请重新申请邮箱找回。',
Password::INVALID_USER => '该邮箱未绑定可重置的账号,请确认后再试。',
default => '密码重置失败,请重新获取重置链接后再试。',
};
}
}