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

259 lines
9.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
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,
]);
}
/**
* 发送邮箱找回密码链接。
*/
public function storeLink(SendPasswordResetLinkRequest $request): JsonResponse
{
if (! $this->isPasswordResetMailEnabled()) {
return response()->json([
'status' => 'error',
'message' => '系统暂未开启邮箱发信服务,当前无法通过邮箱找回密码。',
], 403);
}
$inputEmail = trim((string) $request->input('email', ''));
$username = trim((string) $request->input('username', ''));
$ip = $request->ip();
$user = User::query()->where('username', $username)->first();
if (! $user || empty($user->email)) {
return response()->json([
'status' => 'error',
'message' => '找不到绑定了邮箱的账号。',
], 422);
}
// 强行双向比对核对(忽略大小写和前后空白)
if (strcasecmp($user->email, $inputEmail) !== 0) {
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) {
return response()->json([
'status' => 'error',
'message' => '该邮箱绑定了多个账号,暂不支持自助找回,请联系管理员处理。',
], 422);
}
// 记录发信请求频率
RateLimiter::hit($ipKey, 60);
RateLimiter::hit($targetKey, 180);
$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 => '密码重置失败,请重新获取重置链接后再试。',
};
}
}