diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 3b10ce2c..89af1c48 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -8,16 +8,16 @@ use App\Mail\ForgotPassword; use App\Models\Player; use App\Models\User; use App\Rules; +use Auth; use Blessing\Filter; use Blessing\Rejection; +use Cache; use Carbon\Carbon; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Mail; -use Illuminate\Support\Facades\Session; -use Illuminate\Support\Facades\URL; +use Mail; +use Session; +use URL; use Vectorface\Whip\Whip; class AuthController extends Controller @@ -50,7 +50,7 @@ class AuthController extends Controller Request $request, Rules\Captcha $captcha, Dispatcher $dispatcher, - Filter $filter, + Filter $filter ) { $data = $request->validate([ 'identification' => 'required', @@ -151,7 +151,7 @@ class AuthController extends Controller Request $request, Rules\Captcha $captcha, Dispatcher $dispatcher, - Filter $filter, + Filter $filter ) { $can = $filter->apply('can_register', null); if ($can instanceof Rejection) { @@ -176,8 +176,8 @@ class AuthController extends Controller $dispatcher->dispatch('auth.registration.attempt', [$data]); if ( - option('register_with_player_name') - && Player::where('name', $playerName)->count() > 0 + option('register_with_player_name') && + Player::where('name', $playerName)->count() > 0 ) { return json(trans('user.player.add.repeated'), 1); } @@ -248,7 +248,7 @@ class AuthController extends Controller Request $request, Rules\Captcha $captcha, Dispatcher $dispatcher, - Filter $filter, + Filter $filter ) { $data = $request->validate([ 'email' => 'required|email', @@ -279,12 +279,25 @@ class AuthController extends Controller $dispatcher->dispatch('auth.forgot.ready', [$user]); - $url = URL::temporarySignedRoute( + // 生成带有时间戳的签名 + $timestamp = time(); + $uid = $user->uid; + + // 使用应用密钥、时间戳和用户ID生成签名 + $signature = hash_hmac('sha256', "{$uid}:{$timestamp}", config('app.key')); + + // 存储签名和过期时间到数据库 + $user->password_reset_signature = $signature; + $user->password_reset_expires_at = Carbon::now()->addHour(); + $user->save(); + + // 生成重置链接 + $url = URL::route( 'auth.reset', - Carbon::now()->addHour(), - ['uid' => $user->uid], + ['uid' => $uid, 'timestamp' => $timestamp, 'signature' => $signature], false ); + try { Mail::to($email)->send(new ForgotPassword(url($url))); } catch (\Exception $e) { @@ -302,22 +315,81 @@ class AuthController extends Controller public function reset(Request $request, $uid) { - abort_unless($request->hasValidSignature(false), 403, trans('auth.reset.invalid')); + $signature = $request->input('signature'); + $timestamp = $request->input('timestamp'); + + // 验证参数完整性 + if (!$signature || !$timestamp) { + abort(403, trans('auth.reset.invalid')); + } + + $user = User::find($uid); + if (!$user) { + abort(403, trans('auth.reset.invalid')); + } + + // 验证签名匹配 + if ($user->password_reset_signature !== $signature) { + abort(403, trans('auth.reset.invalid')); + } + + // 验证签名未过期 + if (Carbon::parse($user->password_reset_expires_at)->isPast()) { + abort(403, trans('auth.reset.expired')); + } + + // 验证时间戳是否匹配签名生成时的时间戳 + $expectedSignature = hash_hmac('sha256', "{$uid}:{$timestamp}", config('app.key')); + if ($signature !== $expectedSignature) { + abort(403, trans('auth.reset.invalid')); + } - return view('auth.reset')->with('user', User::find($uid)); + return view('auth.reset')->with('user', $user); } public function handleReset(Dispatcher $dispatcher, Request $request, $uid) { - abort_unless($request->hasValidSignature(false), 403, trans('auth.reset.invalid')); + $signature = $request->input('signature'); + $timestamp = $request->input('timestamp'); + + // 验证参数完整性 + if (!$signature || !$timestamp) { + return json(trans('auth.reset.invalid'), 1); + } + + $user = User::find($uid); + if (!$user) { + return json(trans('auth.reset.invalid'), 1); + } + + // 验证签名匹配 + if ($user->password_reset_signature !== $signature) { + return json(trans('auth.reset.invalid'), 1); + } + + // 验证签名未过期 + if (Carbon::parse($user->password_reset_expires_at)->isPast()) { + return json(trans('auth.reset.expired'), 1); + } + + // 验证时间戳是否匹配签名生成时的时间戳 + $expectedSignature = hash_hmac('sha256', "{$uid}:{$timestamp}", config('app.key')); + if ($signature !== $expectedSignature) { + return json(trans('auth.reset.invalid'), 1); + } ['password' => $password] = $request->validate([ 'password' => 'required|min:8|max:32', ]); - $user = User::find($uid); $dispatcher->dispatch('auth.reset.before', [$user, $password]); $user->changePassword($password); + + // 清除数据库中的签名,确保一次性使用 + $user->password_reset_signature = null; + $user->password_reset_expires_at = null; + $user->save(); + $dispatcher->dispatch('auth.reset.after', [$user, $password]); return json(trans('auth.reset.success'), 0); @@ -344,20 +416,74 @@ class AuthController extends Controller return redirect('/user'); } - public function verify(Request $request) + public function verify(Request $request, $uid) { if (!option('require_verification')) { throw new PrettyPageException(trans('user.verification.disabled'), 1); } - abort_unless($request->hasValidSignature(false), 403, trans('auth.verify.invalid')); + $signature = $request->input('signature'); + $timestamp = $request->input('timestamp'); + + // 验证参数完整性 + if (!$signature || !$timestamp) { + abort(403, trans('auth.verify.invalid')); + } + + $user = User::find($uid); + if (!$user) { + abort(403, trans('auth.verify.invalid')); + } + + // 验证签名匹配 + if ($user->email_verification_signature !== $signature) { + abort(403, trans('auth.verify.invalid')); + } + + // 验证签名未过期 + if (Carbon::parse($user->email_verification_expires_at)->isPast()) { + abort(403, trans('auth.verify.invalid')); + } + + // 验证时间戳是否匹配签名生成时的时间戳 + $expectedSignature = hash_hmac('sha256', "{$uid}:{$timestamp}", config('app.key')); + if ($signature !== $expectedSignature) { + abort(403, trans('auth.verify.invalid')); + } return view('auth.verify'); } - public function handleVerify(Request $request, User $user) + public function handleVerify(Request $request, $uid) { - abort_unless($request->hasValidSignature(false), 403, trans('auth.verify.invalid')); + $signature = $request->input('signature'); + $timestamp = $request->input('timestamp'); + + // 验证参数完整性 + if (!$signature || !$timestamp) { + abort(403, trans('auth.verify.invalid')); + } + + $user = User::find($uid); + if (!$user) { + abort(403, trans('auth.verify.invalid')); + } + + // 验证签名匹配 + if ($user->email_verification_signature !== $signature) { + abort(403, trans('auth.verify.invalid')); + } + + // 验证签名未过期 + if (Carbon::parse($user->email_verification_expires_at)->isPast()) { + abort(403, trans('auth.verify.invalid')); + } + + // 验证时间戳是否匹配签名生成时的时间戳 + $expectedSignature = hash_hmac('sha256', "{$uid}:{$timestamp}", config('app.key')); + if ($signature !== $expectedSignature) { + abort(403, trans('auth.verify.invalid')); + } ['email' => $email] = $request->validate(['email' => 'required|email']); @@ -366,6 +492,9 @@ class AuthController extends Controller } $user->verified = true; + // 清除数据库中的签名,确保一次性使用 + $user->email_verification_signature = null; + $user->email_verification_expires_at = null; $user->save(); return redirect()->route('user.home'); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index c464f992..22a5df3c 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -6,16 +6,16 @@ use App\Events\UserProfileUpdated; use App\Mail\EmailVerification; use App\Models\Texture; use App\Models\User; +use Auth; use Blessing\Filter; use Blessing\Rejection; use Carbon\Carbon; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Mail; -use Illuminate\Support\Facades\Session; -use Illuminate\Support\Facades\URL; use League\CommonMark\GithubFlavoredMarkdownConverter; +use Mail; +use Session; +use URL; class UserController extends Controller { @@ -157,7 +157,24 @@ class UserController extends Controller return json(trans('user.verification.verified'), 1); } - $url = URL::signedRoute('auth.verify', ['user' => $user], null, false); + // 生成带有时间戳的签名 + $timestamp = time(); + $uid = $user->uid; + + // 使用应用密钥、时间戳和用户ID生成签名 + $signature = hash_hmac('sha256', "{$uid}:{$timestamp}", config('app.key')); + + // 存储签名和过期时间到数据库 + $user->email_verification_signature = $signature; + $user->email_verification_expires_at = Carbon::now()->addHour(); + $user->save(); + + // 生成验证链接 + $url = URL::route( + 'auth.verify', + ['uid' => $uid, 'timestamp' => $timestamp, 'signature' => $signature], + false + ); try { Mail::to($user->email)->send(new EmailVerification(url($url))); @@ -330,9 +347,9 @@ class UserController extends Controller } if ( - !$texture->public - && $user->uid !== $texture->uploader - && !$user->isAdmin() + !$texture->public && + $user->uid !== $texture->uploader && + !$user->isAdmin() ) { return json(trans('skinlib.show.private'), 1); } diff --git a/app/Listeners/SendEmailVerification.php b/app/Listeners/SendEmailVerification.php index 097fd169..702b1a65 100644 --- a/app/Listeners/SendEmailVerification.php +++ b/app/Listeners/SendEmailVerification.php @@ -4,6 +4,7 @@ namespace App\Listeners; use App\Mail\EmailVerification; use App\Models\User; +use Carbon\Carbon; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\URL; @@ -12,7 +13,24 @@ class SendEmailVerification public function handle(User $user) { if (option('require_verification')) { - $url = URL::signedRoute('auth.verify', ['user' => $user->uid], null, false); + // 生成带有时间戳的签名 + $timestamp = time(); + $uid = $user->uid; + + // 使用应用密钥、时间戳和用户ID生成签名 + $signature = hash_hmac('sha256', "{$uid}:{$timestamp}", config('app.key')); + + // 存储签名和过期时间到数据库 + $user->email_verification_signature = $signature; + $user->email_verification_expires_at = Carbon::now()->addHour(); + $user->save(); + + // 生成验证链接 + $url = URL::route( + 'auth.verify', + ['uid' => $uid, 'timestamp' => $timestamp, 'signature' => $signature], + false + ); try { Mail::to($user->email)->send(new EmailVerification(url($url))); diff --git a/app/Models/User.php b/app/Models/User.php index 113314f2..9a81a026 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -46,6 +46,8 @@ class User extends Authenticatable protected $fillable = [ 'email', 'nickname', 'avatar', 'score', 'permission', 'last_sign_at', + 'password_reset_signature', 'password_reset_expires_at', + 'email_verification_signature', 'email_verification_expires_at', ]; protected $casts = [ diff --git a/database/migrations/2026_01_24_000000_add_verification_signature_fields.php b/database/migrations/2026_01_24_000000_add_verification_signature_fields.php new file mode 100644 index 00000000..de205cdd --- /dev/null +++ b/database/migrations/2026_01_24_000000_add_verification_signature_fields.php @@ -0,0 +1,22 @@ +string('verification_signature')->nullable(); + $table->timestamp('signature_expires_at')->nullable(); + }); + } + + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(['verification_signature', 'signature_expires_at']); + }); + } +} diff --git a/database/migrations/2026_01_24_000001_split_signature_fields.php b/database/migrations/2026_01_24_000001_split_signature_fields.php new file mode 100644 index 00000000..6ac36ce2 --- /dev/null +++ b/database/migrations/2026_01_24_000001_split_signature_fields.php @@ -0,0 +1,40 @@ +dropColumn(['verification_signature', 'signature_expires_at']); + + // 添加密码重置专用字段 + $table->string('password_reset_signature')->nullable()->after('verified'); + $table->timestamp('password_reset_expires_at')->nullable()->after('password_reset_signature'); + + // 添加邮箱验证专用字段 + $table->string('email_verification_signature')->nullable()->after('password_reset_expires_at'); + $table->timestamp('email_verification_expires_at')->nullable()->after('email_verification_signature'); + }); + } + + public function down() + { + Schema::table('users', function (Blueprint $table) { + // 删除新添加的独立字段 + $table->dropColumn([ + 'password_reset_signature', + 'password_reset_expires_at', + 'email_verification_signature', + 'email_verification_expires_at' + ]); + + // 恢复之前的通用字段 + $table->string('verification_signature')->nullable(); + $table->timestamp('signature_expires_at')->nullable(); + }); + } +} \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 06cb4ba9..3abbe09e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,7 +1,6 @@ name('home'); +Route::get('', 'HomeController@index'); Route::prefix('auth')->name('auth.')->group(function () { Route::middleware('guest')->group(function () { Route::get('login', 'AuthController@login')->name('login'); - Route::post('login', 'AuthController@handleLogin')->name('handle.login'); + Route::post('login', 'AuthController@handleLogin'); Route::get('register', 'AuthController@register')->name('register'); - Route::post('register', 'AuthController@handleRegister')->name('handle.register'); + Route::post('register', 'AuthController@handleRegister'); Route::get('forgot', 'AuthController@forgot')->name('forgot'); - Route::post('forgot', 'AuthController@handleForgot')->name('handle.forgot'); + Route::post('forgot', 'AuthController@handleForgot'); Route::get('reset/{uid}', 'AuthController@reset')->name('reset'); - Route::post('reset/{uid}', 'AuthController@handleReset')->name('handle.reset'); + Route::post('reset/{uid}', 'AuthController@handleReset'); }); Route::post('logout', 'AuthController@logout')->name('logout')->middleware('authorize'); - Route::any('captcha', 'AuthController@captcha')->name('captcha'); + Route::any('captcha', 'AuthController@captcha'); Route::middleware(['authorize', Middleware\EnsureEmailFilled::class]) - ->name('bind.') ->group(function () { - Route::view('bind', 'auth.bind')->name('view'); - Route::post('bind', 'AuthController@fillEmail')->name('verify'); + Route::view('bind', 'auth.bind'); + Route::post('bind', 'AuthController@fillEmail'); }); - Route::get('verify/{user}', 'AuthController@verify')->name('verify'); - Route::post('verify/{user}', 'AuthController@handleVerify')->name('handle.verify'); + Route::get('verify/{uid}', 'AuthController@verify')->name('verify'); + Route::post('verify/{uid}', 'AuthController@handleVerify'); }); Route::prefix('user') @@ -50,27 +48,27 @@ Route::prefix('user') ->middleware(['authorize']) ->group(function () { Route::get('', 'UserController@index')->name('home'); - Route::post('notifications/{id}', 'NotificationsController@read')->name('notification.read'); + Route::post('notifications/{id}', 'NotificationsController@read')->name('notification'); Route::get('score-info', 'UserController@scoreInfo')->name('score'); Route::post('sign', 'UserController@sign')->name('sign'); - Route::get('reports', 'ReportController@track')->name('list'); + Route::get('reports', 'ReportController@track'); Route::prefix('profile')->name('profile.')->group(function () { - Route::get('', 'UserController@profile')->name('view'); - Route::post('', 'UserController@handleProfile')->name('handle.profile'); + Route::get('', 'UserController@profile'); + Route::post('', 'UserController@handleProfile'); Route::post('avatar', 'UserController@setAvatar')->name('avatar'); }); - Route::post('email-verification', 'UserController@sendVerificationEmail')->name('email-verification'); + Route::post('email-verification', 'UserController@sendVerificationEmail'); - Route::put('dark-mode', 'UserController@toggleDarkMode')->name('dark-mode'); + Route::put('dark-mode', 'UserController@toggleDarkMode'); Route::prefix('player') ->name('player.') ->middleware('verified') ->group(function () { - Route::get('', 'PlayerController@index')->name('view'); + Route::get('', 'PlayerController@index')->name('page'); Route::get('list', 'PlayerController@list')->name('list'); Route::post('', 'PlayerController@add')->name('add'); Route::put('{player}/textures', 'PlayerController@setTexture')->name('set'); @@ -80,7 +78,7 @@ Route::prefix('user') }); Route::prefix('closet')->name('closet.')->group(function () { - Route::get('', 'ClosetController@index')->name('view'); + Route::get('', 'ClosetController@index')->name('page'); Route::get('list', 'ClosetController@getClosetData')->name('list'); Route::get('ids', 'ClosetController@allIds')->name('ids'); Route::post('', 'ClosetController@add')->name('add'); @@ -112,8 +110,8 @@ Route::prefix('skinlib')->name('skinlib.')->group(function () { Route::get('list', 'SkinlibController@library')->name('list'); Route::middleware(['authorize', 'verified'])->group(function () { - Route::get('upload', 'SkinlibController@upload')->name('upload'); - Route::post('report', 'ReportController@submit')->name('report'); + Route::get('upload', 'SkinlibController@upload'); + Route::post('report', 'ReportController@submit'); }); }); @@ -121,19 +119,19 @@ Route::prefix('admin') ->name('admin.') ->middleware(['authorize', 'role:admin']) ->group(function () { - Route::get('', 'AdminController@index')->name('view'); - Route::get('chart', 'AdminController@chartData')->name('chart'); - Route::post('notifications/send', 'NotificationsController@send')->name('notification.send'); + Route::get('', 'AdminController@index'); + Route::get('chart', 'AdminController@chartData'); + Route::post('notifications/send', 'NotificationsController@send'); - Route::any('customize', 'OptionsController@customize')->name('customize'); - Route::any('score', 'OptionsController@score')->name('score'); - Route::any('options', 'OptionsController@options')->name('options'); - Route::any('resource', 'OptionsController@resource')->name('resource'); + Route::any('customize', 'OptionsController@customize'); + Route::any('score', 'OptionsController@score'); + Route::any('options', 'OptionsController@options'); + Route::any('resource', 'OptionsController@resource'); - Route::get('status', 'AdminController@status')->name('status'); + Route::get('status', 'AdminController@status'); Route::prefix('users')->name('users.')->group(function () { - Route::view('', 'admin.users')->name('view'); + Route::view('', 'admin.users'); Route::get('list', 'UsersManagementController@list')->name('list'); Route::prefix('{user}')->group(function () { Route::put('email', 'UsersManagementController@email')->name('email'); @@ -147,7 +145,7 @@ Route::prefix('admin') }); Route::prefix('players')->name('players.')->group(function () { - Route::view('', 'admin.players')->name('view'); + Route::view('', 'admin.players'); Route::get('list', 'PlayersManagementController@list')->name('list'); Route::prefix('{player}')->group(function () { Route::put('name', 'PlayersManagementController@name')->name('name'); @@ -157,56 +155,56 @@ Route::prefix('admin') }); }); - Route::prefix('closet')->name('closet.')->group(function () { - Route::post('{user}', 'ClosetManagementController@add')->name('add'); - Route::delete('{user}', 'ClosetManagementController@remove')->name('remove'); + Route::prefix('closet')->group(function () { + Route::post('{user}', 'ClosetManagementController@add'); + Route::delete('{user}', 'ClosetManagementController@remove'); }); - Route::prefix('reports')->name('reports.')->group(function () { - Route::view('', 'admin.reports')->name('view'); - Route::put('{report}', 'ReportController@review')->name('review'); - Route::get('list', 'ReportController@manage')->name('list'); + Route::prefix('reports')->group(function () { + Route::view('', 'admin.reports'); + Route::put('{report}', 'ReportController@review'); + Route::get('list', 'ReportController@manage'); }); - Route::prefix('i18n')->name('i18n.')->group(function () { - Route::view('', 'admin.i18n')->name('view'); - Route::get('list', 'TranslationsController@list')->name('list'); - Route::post('', 'TranslationsController@create')->name('create'); - Route::put('{line}', 'TranslationsController@update')->name('update'); - Route::delete('{line}', 'TranslationsController@delete')->name('delete'); + Route::prefix('i18n')->group(function () { + Route::view('', 'admin.i18n'); + Route::get('list', 'TranslationsController@list'); + Route::post('', 'TranslationsController@create'); + Route::put('{line}', 'TranslationsController@update'); + Route::delete('{line}', 'TranslationsController@delete'); }); - Route::prefix('plugins')->name('plugins.')->group(function () { - Route::get('data', 'PluginController@getPluginData')->name('data'); + Route::prefix('plugins')->group(function () { + Route::get('data', 'PluginController@getPluginData'); - Route::view('manage', 'admin.plugins')->name('view'); - Route::post('manage', 'PluginController@manage')->name('view'); - Route::any('config/{name}', 'PluginController@config')->name('config'); - Route::get('readme/{name}', 'PluginController@readme')->name('readme'); + Route::view('manage', 'admin.plugins'); + Route::post('manage', 'PluginController@manage'); + Route::any('config/{name}', 'PluginController@config'); + Route::get('readme/{name}', 'PluginController@readme'); Route::middleware('role:super-admin')->group(function () { - Route::post('upload', 'PluginController@upload')->name('upload'); - Route::post('wget', 'PluginController@wget')->name('wget'); + Route::post('upload', 'PluginController@upload'); + Route::post('wget', 'PluginController@wget'); }); - Route::prefix('market')->name('market.')->group(function () { - Route::view('', 'admin.market')->name('view'); - Route::get('list', 'MarketController@marketData')->name('list'); - Route::post('download', 'MarketController@download')->name('download'); + Route::prefix('market')->group(function () { + Route::view('', 'admin.market'); + Route::get('list', 'MarketController@marketData'); + Route::post('download', 'MarketController@download'); }); }); - Route::prefix('update')->name('update.')->middleware('role:super-admin')->group(function () { - Route::get('', 'UpdateController@showUpdatePage')->name('view'); - Route::post('download', 'UpdateController@download')->name('download'); + Route::prefix('update')->middleware('role:super-admin')->group(function () { + Route::get('', 'UpdateController@showUpdatePage'); + Route::post('download', 'UpdateController@download'); }); }); -Route::prefix('setup')->name('setup.')->group(function () { +Route::prefix('setup')->group(function () { Route::middleware('setup')->group(function () { - Route::view('', 'setup.wizard.welcome')->name('view'); - Route::any('database', 'SetupController@database')->name('database'); - Route::view('info', 'setup.wizard.info')->name('info'); - Route::post('finish', 'SetupController@finish')->name('finish'); + Route::view('', 'setup.wizard.welcome'); + Route::any('database', 'SetupController@database'); + Route::view('info', 'setup.wizard.info'); + Route::post('finish', 'SetupController@finish'); }); });