سی ام اس نویسسی ام اس نویسیاد بگیر، سیستم خودتو بنویس
مجلهتماس با ما
سی ام اس نویسسی ام اس نویس

CMS نویس مرجعی برای آموزش برنامه نویسی وب و PHP، توسعه افزونه‌های اختصاصی وردپرس، طراحی ورک‌فلو و اتوماسیون با n8n و توسعه CMS های اختصاصی است. هدف ما ارائه آموزش‌های کاربردی و راهکارهای حرفه‌ای برای توسعه‌دهندگان، کسب‌وکارها و علاقه‌مندان به دنیای برنامه‌نویسی است.

پیوندهای سریع
محصولات فیزیکیمحصولات دیجیتالدوره‌های آموزشی
سی ام اس نویس
مجله سی ام اس نویستماس با ماپنل کاربری
سی ام اس نویس — تمامی حقوق برای سی ام اس نویس محفوظ است. © 1406 تماس با ما09307711941
Password Reset در PHP چیست؟ آموزش ساخت سیستم فراموشی رمز عبور امن

Password Reset در PHP چیست؟ آموزش ساخت سیستم فراموشی رمز عبور امن

ممدیر کل سیستم ۲۸ شهریور ۱۴۰۵ ۲۳ دقیقه مطالعه ۶ بازدید
Password Reset در PHP چیست؟ آموزش ساخت سیستم فراموشی رمز عبور امن

فراموش کردن رمز عبور یکی از رایج‌ترین اتفاقات در هر سیستم کاربری است. تقریباً هر وب‌سایتی که امکان ثبت‌نام و ورود کاربران را دارد، به یک سیستم Password Reset یا «فراموشی رمز عبور» نیاز دارد.

اما پیاده‌سازی Password Reset فقط به این معنی نیست که یک لینک برای کاربر ارسال کنیم و اجازه دهیم رمز عبورش را تغییر دهد. اگر این سیستم به شکل نادرست طراحی شود، مهاجم می‌تواند با سرقت یا حدس Token، سوءاستفاده از لینک بازیابی، User Enumeration یا حملات مختلف، کنترل حساب کاربر را به دست بگیرد.

در این مقاله با PHP و PDO یک سیستم ساده اما اصولی برای فراموشی و بازیابی رمز عبور طراحی می‌کنیم و با مفاهیمی مثل Token تصادفی، Hash کردن Token، تاریخ انقضا، Token یک‌بارمصرف، password_hash()، Session و Rate Limiting آشنا می‌شویم.


Password Reset چیست؟

Password Reset فرآیندی است که به کاربر اجازه می‌دهد بدون دانستن رمز عبور فعلی، در صورت اثبات دسترسی به ایمیل یا روش بازیابی معتبر، یک رمز عبور جدید برای حساب خود تعیین کند.

فرآیند معمول به این شکل است:


کاربر رمز عبور را فراموش کرده است

              ↓

      وارد کردن ایمیل

              ↓

       تولید Token تصادفی

              ↓

    ذخیره Hash توکن در دیتابیس

              ↓

      ارسال لینک به ایمیل

              ↓

       کاربر روی لینک کلیک می‌کند

              ↓

       بررسی Token و انقضا

              ↓

      دریافت رمز عبور جدید

              ↓

       Hash کردن رمز جدید

              ↓

       تغییر رمز عبور

              ↓

       باطل کردن Token

              ↓

    ابطال Sessionهای قبلی

نکته مهم این است که در این فرآیند، ما رمز عبور قبلی را بازیابی نمی‌کنیم.


# چرا نباید رمز عبور قبلی را بازیابی کنیم؟

رمزهای عبور نباید به شکل قابل بازیابی ذخیره شوند.

برای مثال، این طراحی کاملاً اشتباه است:


users

-----------------------------

id

email

password

اگر مقدار password شامل رمز واقعی کاربر باشد، با لو رفتن دیتابیس، رمز کاربران نیز لو می‌رود.

در طراحی امن، رمز عبور با تابعی مانند password_hash() ذخیره می‌شود:


$passwordHash = password_hash(

    $password,

    PASSWORD_DEFAULT

);

بنابراین سیستم باید به جای بازیابی رمز قبلی، به کاربر اجازه دهد یک رمز جدید تعیین کند.


# Password Reset امن چه مراحلی دارد؟

یک سیستم مناسب می‌تواند این مراحل را داشته باشد:

  1. کاربر ایمیل خود را وارد می‌کند.
  2. سیستم بررسی می‌کند آیا حسابی با این ایمیل وجود دارد یا خیر.
  3. در هر دو حالت، پیام عمومی یکسانی نمایش داده می‌شود.
  4. در صورت وجود حساب، یک Token امن تولید می‌شود.
  5. Hash توکن در دیتابیس ذخیره می‌شود.
  6. Token خام داخل لینک ایمیل قرار می‌گیرد.
  7. Token دارای تاریخ انقضا است.
  8. کاربر لینک را باز می‌کند.
  9. سیستم Token را بررسی می‌کند.
  10. کاربر رمز جدید وارد می‌کند.
  11. رمز جدید با password_hash() ذخیره می‌شود.
  12. Token بلافاصله باطل می‌شود.
  13. Sessionهای قبلی کاربر نیز بهتر است باطل شوند.

# چرا نباید وجود یا عدم وجود ایمیل را اعلام کنیم؟

فرض کنید کاربر ایمیل زیر را وارد کند:


test@example.com

اگر سیستم بگوید:

> این ایمیل در سیستم وجود ندارد.

مهاجم می‌تواند تعداد زیادی ایمیل را امتحان کند و متوجه شود کدام ایمیل‌ها در سایت حساب کاربری دارند.

این حمله را User Enumeration می‌نامیم.

بهتر است در هر دو حالت یک پیام مشابه نمایش داده شود:


اگر حسابی با این ایمیل وجود داشته باشد،

لینک بازیابی رمز عبور برای شما ارسال خواهد شد.

بنابراین مهاجم نمی‌تواند از پاسخ سیستم متوجه شود که یک حساب وجود دارد یا خیر.


# ساخت جدول Password Reset

یکی از روش‌های مناسب این است که Tokenهای بازیابی را در یک جدول جداگانه نگهداری کنیم.

برای مثال:


CREATE TABLE password_reset_tokens (

    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,

    user_id BIGINT UNSIGNED NOT NULL,

    token_hash CHAR(64) NOT NULL,

    expires_at DATETIME NOT NULL,

    used_at DATETIME NULL,

    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,



    INDEX (user_id),

    INDEX (token_hash)

);

ساختار این جدول تقریباً به این شکل است:

ستونکاربرد
idشناسه رکورد
user_idکاربری که Token متعلق به او است
token_hashHash توکن
expires_atزمان انقضا
used_atزمان مصرف شدن Token
created_atزمان ایجاد Token

# چرا Token خام را در دیتابیس ذخیره نکنیم؟

فرض کنید Token این باشد:


8d4e7c9f...

بهتر است همین مقدار خام را در دیتابیس قرار ندهیم.

ابتدا Token را تولید می‌کنیم:


$token = bin2hex(random_bytes(32));

سپس Hash آن را می‌سازیم:


$tokenHash = hash('sha256', $token);

و فقط Hash را ذخیره می‌کنیم:


INSERT INTO password_reset_tokens (...)

VALUES (..., $tokenHash, ...)

اما Token خام را داخل لینک ایمیل قرار می‌دهیم:


https://example.com/reset-password?token=...

در نتیجه:


Token خام

   ↓

ایمیل / لینک

   ↓

Hash Token

   ↓

دیتابیس

اگر دیتابیس لو رفت، مهاجم مستقیماً Token قابل استفاده را در اختیار ندارد.


# چرا از random_bytes() استفاده کنیم؟

برای تولید Token امنیتی نباید از توابعی مثل این‌ها استفاده کنیم:


rand();

یا:


mt_rand();

یا:


time();

این توابع برای تولید Token امنیتی مناسب نیستند.

روش مناسب:


$token = bin2hex(random_bytes(32));

تابع random_bytes() برای تولید داده تصادفی مناسب کاربردهای امنیتی در PHP طراحی شده است.

32 بایت داده تصادفی پس از تبدیل با bin2hex() به یک رشته 64 کاراکتری تبدیل می‌شود.


# ساخت فرم «فراموشی رمز عبور»

یک فرم ساده می‌تواند به این شکل باشد:


<form method="POST" action="/forgot-password">

    <label for="email">

        ایمیل

    </label>



    <input

        type="email"

        id="email"

        name="email"

        required

    >



    <button type="submit">

        ارسال لینک بازیابی

    </button>

</form>

در پروژه واقعی باید برای این فرم نیز CSRF Protection داشته باشیم.


# دریافت ایمیل در PHP

برای مثال:


$email = trim($_POST['email'] ?? '');

سپس اعتبارسنجی:


if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {

    exit('ایمیل نامعتبر است.');

}

در سیستم واقعی بهتر است پیام خطا نیز متناسب با UX طراحی شود.


# پیدا کردن کاربر

با PDO می‌توانیم کاربر را پیدا کنیم:


$stmt = $pdo->prepare(

    'SELECT id, email

     FROM users

     WHERE email = :email

     LIMIT 1'

);



$stmt->execute([

    'email' => $email

]);



$user = $stmt->fetch(PDO::FETCH_ASSOC);

در اینجا از Prepared Statement استفاده کرده‌ایم تا Query در برابر SQL Injection ایمن‌تر باشد.


# پیام عمومی برای جلوگیری از User Enumeration

حتی اگر کاربر پیدا نشد، بهتر است پاسخ عمومی بدهیم:


$message = 'اگر حسابی با این ایمیل وجود داشته باشد، لینک بازیابی رمز عبور برای شما ارسال خواهد شد.';

اگر کاربر وجود نداشت، می‌توانیم بدون انجام عملیات حساس همین پیام را نمایش دهیم.


# حذف Tokenهای قبلی

قبل از تولید Token جدید می‌توانیم Tokenهای فعال قبلی همان کاربر را باطل کنیم.

برای مثال:


if ($user) {

    $stmt = $pdo->prepare(

        'UPDATE password_reset_tokens

         SET used_at = NOW()

         WHERE user_id = :user_id

         AND used_at IS NULL'

    );



    $stmt->execute([

        'user_id' => $user['id']

    ]);

}

این کار باعث می‌شود Tokenهای قبلی قابل استفاده نباشند.


# تولید Token جدید

حالا Token جدید را تولید می‌کنیم:


$token = bin2hex(random_bytes(32));

سپس Hash:


$tokenHash = hash('sha256', $token);


# تعیین زمان انقضا

برای مثال می‌توانیم Token را 30 دقیقه معتبر کنیم:


$expiresAt = date(

    'Y-m-d H:i:s',

    time() + 1800

);

عدد 1800 برابر با 30 دقیقه است.

در سیستم‌های واقعی می‌توانید مدت اعتبار را بر اساس نیاز پروژه تعیین کنید.

مثلاً:


15 دقیقه

30 دقیقه

60 دقیقه

Tokenهای Password Reset بهتر است عمر محدودی داشته باشند.


# ذخیره Token در دیتابیس

اکنون:


$stmt = $pdo->prepare(

    'INSERT INTO password_reset_tokens

     (user_id, token_hash, expires_at)

     VALUES

     (:user_id, :token_hash, :expires_at)'

);



$stmt->execute([

    'user_id' => $user['id'],

    'token_hash' => $tokenHash,

    'expires_at' => $expiresAt

]);

در دیتابیس فقط Hash ذخیره می‌شود.


# ساخت لینک بازیابی

لینک را می‌توانیم به شکل زیر بسازیم:


$resetUrl =

    'https://example.com/reset-password?token='

    . urlencode($token);

نکته امنیتی مهم:

دامنه لینک را بهتر است از Configuration ثابت برنامه دریافت کنیم.

مثلاً:


$baseUrl = 'https://example.com';



$resetUrl =

    $baseUrl

    . '/reset-password?token='

    . urlencode($token);

نباید بدون بررسی از مقدارهایی مانند:


$_SERVER['HTTP_HOST']

برای ساخت لینک حساس استفاده کنیم؛ زیرا در بعضی معماری‌ها Host Header می‌تواند دستکاری شود.


# ارسال لینک از طریق ایمیل

برای نمونه:


$subject = 'بازیابی رمز عبور';



$message = "

برای تغییر رمز عبور خود روی لینک زیر کلیک کنید:



$resetUrl



این لینک تا 30 دقیقه معتبر است.

";

در پروژه واقعی بهتر است ارسال ایمیل را به سرویس یا Mailer مناسب بسپاریم.

همچنین بهتر است ارسال ایمیل در سیستم‌های پرترافیک به صورت Queue/Job انجام شود تا درخواست HTTP کاربر منتظر ارسال ایمیل نماند.


# صفحه Reset Password

کاربر بعد از کلیک روی لینک وارد صفحه‌ای مانند زیر می‌شود:


https://example.com/reset-password?token=...

در PHP:


$token = $_GET['token'] ?? '';

سپس باید Token را بررسی کنیم.


# Hash کردن Token دریافتی

Token دریافت‌شده را دوباره Hash می‌کنیم:


$tokenHash = hash('sha256', $token);

سپس آن را با مقدار ذخیره‌شده در دیتابیس مقایسه می‌کنیم.


# پیدا کردن Token


$stmt = $pdo->prepare(

    'SELECT id, user_id, token_hash, expires_at, used_at

     FROM password_reset_tokens

     WHERE token_hash = :token_hash

     LIMIT 1'

);



$stmt->execute([

    'token_hash' => $tokenHash

]);



$resetToken = $stmt->fetch(PDO::FETCH_ASSOC);


# بررسی وجود Token

اگر رکورد پیدا نشد:


if (!$resetToken) {

    exit('لینک بازیابی نامعتبر است.');

}


# بررسی Token مصرف‌شده

اگر Token قبلاً استفاده شده باشد:


if ($resetToken['used_at'] !== null) {

    exit('این لینک قبلاً استفاده شده است.');

}

این قسمت بسیار مهم است.

Token Password Reset باید یک‌بارمصرف باشد.


# بررسی تاریخ انقضا

برای مثال:


if (

    strtotime($resetToken['expires_at']) < time()

) {

    exit('این لینک منقضی شده است.');

}

بنابراین حتی اگر Token در اختیار فرد دیگری قرار بگیرد، بعد از پایان زمان اعتبار قابل استفاده نخواهد بود.


# مقایسه امن Token

در صورت نیاز به مقایسه مستقیم مقادیر حساس، می‌توانیم از:


hash_equals()

استفاده کنیم.

مثلاً:


if (!hash_equals(

    $storedHash,

    $tokenHash

)) {

    exit('Token نامعتبر است.');

}

در طراحی بالا چون Token Hash شده و Query بر اساس Hash انجام شده است، بررسی وجود رکورد نیز بخش اصلی اعتبارسنجی است.


# نمایش فرم تعیین رمز جدید

بعد از معتبر بودن Token، فرم رمز جدید را نمایش می‌دهیم:


<form method="POST" action="/reset-password">

    <input

        type="hidden"

        name="token"

        value="..."

    >



    <label>

        رمز عبور جدید

    </label>



    <input

        type="password"

        name="password"

        required

    >



    <label>

        تکرار رمز عبور

    </label>



    <input

        type="password"

        name="password_confirmation"

        required

    >



    <button type="submit">

        تغییر رمز عبور

    </button>

</form>

این فرم نیز باید دارای CSRF Protection باشد.


# اعتبارسنجی رمز جدید

مثلاً:


$password = $_POST['password'] ?? '';

$confirmation = $_POST['password_confirmation'] ?? '';



if ($password !== $confirmation) {

    exit('رمزهای عبور یکسان نیستند.');

}

می‌توانیم حداقل طول رمز را نیز بررسی کنیم:


if (strlen($password) < 12) {

    exit('رمز عبور باید حداقل 12 کاراکتر باشد.');

}

البته سیاست دقیق Password Policy باید متناسب با پروژه تعیین شود.


# Hash کردن رمز جدید

هرگز رمز جدید را مستقیماً در دیتابیس ذخیره نکنید.

روش صحیح:


$passwordHash = password_hash(

    $password,

    PASSWORD_DEFAULT

);

سپس:


$stmt = $pdo->prepare(

    'UPDATE users

     SET password = :password

     WHERE id = :user_id'

);



$stmt->execute([

    'password' => $passwordHash,

    'user_id' => $resetToken['user_id']

]);


# چرا PASSWORD_DEFAULT؟

به جای اینکه الگوریتم Hash را در همه جای برنامه به صورت سخت‌کد کنیم، می‌توانیم از:


PASSWORD_DEFAULT

استفاده کنیم.

این کار باعث می‌شود کد با سیاست پیش‌فرض PHP برای Password Hashing هماهنگ باشد.


# باطل کردن Token بعد از تغییر رمز

بعد از تغییر موفق رمز عبور، Token باید غیرقابل استفاده شود:


$stmt = $pdo->prepare(

    'UPDATE password_reset_tokens

     SET used_at = NOW()

     WHERE id = :id

     AND used_at IS NULL'

);



$stmt->execute([

    'id' => $resetToken['id']

]);

وجود شرط:


AND used_at IS NULL

باعث می‌شود Token قبلاً مصرف‌شده دوباره فعال نشود.


# آیا باید Sessionهای قبلی را نیز باطل کنیم؟

بله، بهتر است بعد از Reset Password، Sessionهای فعال کاربر نیز مدیریت شوند.

فرض کنید مهاجم قبلاً Session کاربر را سرقت کرده باشد.

کاربر رمز خود را تغییر می‌دهد، اما Session مهاجم همچنان معتبر است.

بنابراین تغییر رمز به تنهایی همیشه کافی نیست.

حداقل برای Session فعلی می‌توانیم از:


session_regenerate_id(true);

استفاده کنیم.

اما در سیستم‌های حرفه‌ای بهتر است مکانیزمی برای Invalidation کردن تمام Sessionهای فعال کاربر داشته باشیم.

برای مثال:


user_sessions

----------------

id

user_id

session_id

created_at

last_activity

revoked_at

و بعد از Reset Password:


UPDATE user_sessions

SET revoked_at = NOW()

WHERE user_id = ?

AND revoked_at IS NULL;

در این حالت کاربر باید دوباره Login کند.

این رفتار معمولاً از Login خودکار بعد از Password Reset قابل کنترل‌تر است.


# چرا بهتر است بعد از Reset به صورت خودکار Login نکنیم؟

ممکن است وسوسه شویم بعد از تغییر رمز:


Password Reset

      ↓

Login خودکار

      ↓

Dashboard

انجام دهیم.

اما در بسیاری از سیستم‌ها بهتر است:


Password Reset

      ↓

Invalidation Sessionها

      ↓

پیام موفقیت

      ↓

Login مجدد

داشته باشیم.

این طراحی ساده‌تر و قابل کنترل‌تر است.


# نمونه کامل Flow

فرآیند کامل سیستم ما:


                  Forgot Password

                         │

                         ▼

                  دریافت Email

                         │

                         ▼

                 پیدا کردن User

                         │

             ┌───────────┴───────────┐

             │                       │

          وجود دارد                وجود ندارد

             │                       │

             ▼                       ▼

       تولید Token              پاسخ عمومی

             │

             ▼

        Hash کردن Token

             │

             ▼

       ذخیره در Database

             │

             ▼

       ارسال Email

             │

             ▼

       کلیک روی لینک

             │

             ▼

      بررسی Token + Expiry

             │

             ▼

      دریافت Password جدید

             │

             ▼

       password_hash()

             │

             ▼

       تغییر Password

             │

             ▼

       مصرف Token

             │

             ▼

     ابطال Sessionهای قبلی

             │

             ▼

         Login مجدد


# یک پیاده‌سازی ساده با PDO

ساختار کلی بخش درخواست Reset می‌تواند به شکل زیر باشد:


<?php



session_start();



$email = trim($_POST['email'] ?? '');



if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {

    exit('ایمیل نامعتبر است.');

}



$stmt = $pdo->prepare(

    'SELECT id, email

     FROM users

     WHERE email = :email

     LIMIT 1'

);



$stmt->execute([

    'email' => $email

]);



$user = $stmt->fetch(PDO::FETCH_ASSOC);



if ($user) {



    // باطل کردن Tokenهای قبلی

    $stmt = $pdo->prepare(

        'UPDATE password_reset_tokens

         SET used_at = NOW()

         WHERE user_id = :user_id

         AND used_at IS NULL'

    );



    $stmt->execute([

        'user_id' => $user['id']

    ]);



    // تولید Token امن

    $token = bin2hex(random_bytes(32));



    // ذخیره Hash

    $tokenHash = hash('sha256', $token);



    // اعتبار 30 دقیقه

    $expiresAt = date(

        'Y-m-d H:i:s',

        time() + 1800

    );



    $stmt = $pdo->prepare(

        'INSERT INTO password_reset_tokens

        (user_id, token_hash, expires_at)

        VALUES

        (:user_id, :token_hash, :expires_at)'

    );



    $stmt->execute([

        'user_id' => $user['id'],

        'token_hash' => $tokenHash,

        'expires_at' => $expiresAt

    ]);



    $resetUrl =

        'https://example.com/reset-password?token='

        . urlencode($token);



    // ارسال ایمیل را اینجا انجام دهید.

}



echo 'اگر حسابی با این ایمیل وجود داشته باشد، لینک بازیابی برای شما ارسال خواهد شد.';


# بخش تغییر رمز عبور

حالا بخش دوم:


<?php



$token = $_POST['token'] ?? '';

$password = $_POST['password'] ?? '';

$confirmation = $_POST['password_confirmation'] ?? '';



if ($token === '') {

    exit('Token نامعتبر است.');

}



if ($password !== $confirmation) {

    exit('رمزهای عبور یکسان نیستند.');

}



if (strlen($password) < 12) {

    exit('رمز عبور باید حداقل 12 کاراکتر باشد.');

}



$tokenHash = hash('sha256', $token);



$stmt = $pdo->prepare(

    'SELECT id, user_id, token_hash, expires_at, used_at

     FROM password_reset_tokens

     WHERE token_hash = :token_hash

     LIMIT 1'

);



$stmt->execute([

    'token_hash' => $tokenHash

]);



$resetToken = $stmt->fetch(PDO::FETCH_ASSOC);



if (!$resetToken) {

    exit('Token نامعتبر است.');

}



if ($resetToken['used_at'] !== null) {

    exit('این لینک قبلاً استفاده شده است.');

}



if (strtotime($resetToken['expires_at']) < time()) {

    exit('این لینک منقضی شده است.');

}



if (!hash_equals(

    $resetToken['token_hash'],

    $tokenHash

)) {

    exit('Token نامعتبر است.');

}



$passwordHash = password_hash(

    $password,

    PASSWORD_DEFAULT

);



$stmt = $pdo->prepare(

    'UPDATE users

     SET password = :password

     WHERE id = :user_id'

);



$stmt->execute([

    'password' => $passwordHash,

    'user_id' => $resetToken['user_id']

]);



$stmt = $pdo->prepare(

    'UPDATE password_reset_tokens

     SET used_at = NOW()

     WHERE id = :id

     AND used_at IS NULL'

);



$stmt->execute([

    'id' => $resetToken['id']

]);



echo 'رمز عبور شما با موفقیت تغییر کرد.';

این کد یک نمونه آموزشی است و در پروژه واقعی باید CSRF، Transaction، Rate Limiting، مدیریت Sessionها و ارسال ایمیل واقعی نیز به آن اضافه شود.


# استفاده از Transaction

تغییر رمز عبور و مصرف Token بهتر است در یک Transaction انجام شود.

برای مثال:


$pdo->beginTransaction();



try {



    $passwordHash = password_hash(

        $password,

        PASSWORD_DEFAULT

    );



    $stmt = $pdo->prepare(

        'UPDATE users

         SET password = :password

         WHERE id = :user_id'

    );



    $stmt->execute([

        'password' => $passwordHash,

        'user_id' => $resetToken['user_id']

    ]);



    $stmt = $pdo->prepare(

        'UPDATE password_reset_tokens

         SET used_at = NOW()

         WHERE id = :id

         AND used_at IS NULL'

    );



    $stmt->execute([

        'id' => $resetToken['id']

    ]);



    $pdo->commit();



} catch (Throwable $e) {



    $pdo->rollBack();



    throw $e;

}

مزیت Transaction این است که اگر یکی از عملیات‌های مهم شکست بخورد، وضعیت سیستم ناقص باقی نمی‌ماند.


# Rate Limiting برای Password Reset

یک مشکل مهم دیگر، ارسال تعداد زیادی درخواست Reset است.

مهاجم می‌تواند:


1000 درخواست

      ↓

1000 ایمیل

      ↓

Spam

ایجاد کند.

بنابراین باید روی Password Reset محدودیت قرار دهیم.

مثلاً:


حداکثر 3 درخواست در 15 دقیقه

البته مقدار واقعی باید بر اساس نوع پروژه تعیین شود.

Rate Limit می‌تواند بر اساس مواردی مانند:


IP

Email

User ID

ترکیب IP + Email

انجام شود.


# آیا برای هر درخواست باید Token جدید بسازیم؟

بهتر است سیستم Tokenهای قبلی را مدیریت کند.

مثلاً:


درخواست اول

    ↓

Token A



درخواست دوم

    ↓

Token A باطل

    ↓

Token B فعال

در نتیجه فقط آخرین Token فعال باقی می‌ماند.


# اگر کاربر ایمیل را دریافت نکرد چه کنیم؟

چند دلیل رایج وجود دارد:

  • ایمیل وارد Spam شده است.
  • سرویس ایمیل مشکل دارد.
  • آدرس ایمیل اشتباه وارد شده است.
  • ارسال ایمیل با تأخیر انجام شده است.
  • محدودیت Rate Limit فعال شده است.

اما سیستم نباید به شکل ناامن وجود حساب را فاش کند.

می‌توانیم پیام عمومی زیر را نشان دهیم:


اگر حسابی با این ایمیل وجود داشته باشد،

ایمیل بازیابی ارسال خواهد شد.

در صورت عدم دریافت، پوشه Spam را بررسی کنید.


# Token چقدر باید معتبر باشد؟

مدت اعتبار Token به نوع سیستم بستگی دارد.

برای مثال:


15 دقیقه

30 دقیقه

60 دقیقه

مقادیر رایجی برای طراحی هستند.

هرچه زمان اعتبار طولانی‌تر باشد، پنجره زمانی سوءاستفاده نیز بیشتر می‌شود.

بنابراین بهتر است Token عمر محدودی داشته باشد.


# آیا می‌توان Token را در دیتابیس خام ذخیره کرد؟

از نظر طراحی امنیتی بهتر است:


Token خام

     ↓

Hash

     ↓

Database

داشته باشیم.

نه:


Token خام

     ↓

Database

به این ترتیب اگر اطلاعات دیتابیس افشا شود، Tokenهای بازیابی مستقیماً قابل استفاده نیستند.


# آیا Password Reset با JWT انجام شود؟

از نظر فنی می‌توان از JWT استفاده کرد، اما برای Password Reset معمولاً یک Token تصادفی و یک‌بارمصرف بسیار ساده‌تر است.

Password Reset معمولاً به این ویژگی‌ها نیاز دارد:


Random

Short-lived

Single-use

Revocable

Server-side validation

جدول Token اختصاصی این ویژگی‌ها را به شکل ساده‌ای فراهم می‌کند.


# مراقبت از URL بازیابی

Token در URL قرار می‌گیرد:


/reset-password?token=...

بنابراین باید توجه کنیم که این URL ممکن است در محیط‌هایی مانند Log، History یا Referrer دیده شود.

برای کاهش ریسک:

  • از HTTPS استفاده کنید.
  • Token را در Logهای برنامه ثبت نکنید.
  • روی صفحه Reset منابع شخص ثالث غیرضروری بارگذاری نکنید.
  • از سیاست مناسب Referrer-Policy استفاده کنید.
  • بعد از مصرف، Token را باطل کنید.
  • Token را برای مدت طولانی معتبر نگه ندارید.

برای مثال:


Referrer-Policy: no-referrer

می‌تواند برای چنین صفحه‌ای مورد استفاده قرار گیرد.


# چرا نباید Password را در URL قرار دهیم؟

این کار کاملاً اشتباه است:


/reset-password?email=user@example.com&password=123456

رمز عبور نباید در URL قرار بگیرد.

URL می‌تواند در:

  • Browser History
  • Proxy
  • Server Log
  • Analytics
  • Referrer

ثبت شود.

فقط Token موقت باید در URL قرار داشته باشد.


# CSRF را فراموش نکنید

دو فرم مهم داریم:


Forgot Password

Reset Password

هر دو باید در صورت مناسب بودن معماری برنامه، با CSRF Protection محافظت شوند.

برای مثال:


<input

    type="hidden"

    name="csrf_token"

    value="..."

>

و در PHP Token را بررسی می‌کنیم.

جزئیات CSRF در مقاله مستقل مربوط به امنیت CSRF بررسی شده است.


# HTTPS الزامی است

Password Reset بدون HTTPS طراحی مناسبی نیست.

لینک باید:


https://example.com/reset-password?token=...

باشد.

نه:


http://example.com/reset-password?token=...

زیرا Token در URL قرار دارد و نباید در مسیر شبکه به صورت ناامن منتقل شود.


# اشتباهات رایج در Password Reset

1. ذخیره Token خام

اشتباه:


$token = bin2hex(random_bytes(32));



// ذخیره مستقیم Token

بهتر:


$token = bin2hex(random_bytes(32));



$tokenHash = hash(

    'sha256',

    $token

);



// ذخیره $tokenHash


2. استفاده از rand()

اشتباه:


$token = rand(100000, 999999);

برای Token امنیتی مناسب نیست.


3. Token بدون تاریخ انقضا

Token نباید برای همیشه معتبر باشد.


4. امکان استفاده چندباره

Token باید بعد از موفقیت مصرف شود:


used_at = NOW()


5. اعلام وجود ایمیل

نباید به مهاجم بگوییم:


این ایمیل در سیستم وجود دارد.


6. تغییر مستقیم Password بدون Hash

اشتباه:


UPDATE users

SET password = :password

در صورتی که مقدار password خام باشد.

صحیح:


$passwordHash = password_hash(

    $password,

    PASSWORD_DEFAULT

);


7. عدم محدودیت درخواست‌ها

بدون Rate Limiting، مهاجم می‌تواند درخواست‌های زیادی ارسال کند.


8. ساخت لینک با Host ورودی کاربر

بهتر است:


$baseUrl = 'https://example.com';

از Configuration برنامه بیاید.

نه اینکه بدون کنترل:


$_SERVER['HTTP_HOST']

مبنای ساخت لینک حساس باشد.


# چک‌لیست Password Reset امن

قبل از انتشار سیستم Password Reset این موارد را بررسی کنید:

  • [ ] Token با random_bytes() ساخته می‌شود.
  • [ ] Token خام در دیتابیس ذخیره نمی‌شود.
  • [ ] Hash Token در دیتابیس ذخیره می‌شود.
  • [ ] Token تاریخ انقضا دارد.
  • [ ] Token یک‌بارمصرف است.
  • [ ] Tokenهای قبلی مدیریت می‌شوند.
  • [ ] وجود حساب کاربری افشا نمی‌شود.
  • [ ] Password با password_hash() ذخیره می‌شود.
  • [ ] Password در URL قرار نمی‌گیرد.
  • [ ] HTTPS فعال است.
  • [ ] فرم‌ها CSRF Protection دارند.
  • [ ] Rate Limiting وجود دارد.
  • [ ] Token در Log ذخیره نمی‌شود.
  • [ ] Sessionهای قبلی بعد از Reset مدیریت می‌شوند.
  • [ ] لینک Reset با Domain ثابت و قابل اعتماد ساخته می‌شود.
  • [ ] ارسال ایمیل در سیستم‌های بزرگ با Queue انجام می‌شود.
  • [ ] عملیات تغییر Password و مصرف Token با Transaction مدیریت می‌شود.

# تفاوت Password Reset با Password Change

این دو مفهوم را نباید با هم اشتباه گرفت.

Password Change

کاربر معمولاً وارد حساب خود شده و رمز فعلی را می‌داند:


Login

 ↓

Current Password

 ↓

New Password

Password Reset

کاربر رمز فعلی را فراموش کرده است:


Forgot Password

 ↓

Email

 ↓

Reset Token

 ↓

New Password

در Password Reset، مالکیت یا دسترسی به کانال بازیابی مانند ایمیل باید مبنای اعتبارسنجی قرار گیرد.


# آیا بعد از Reset باید رمز قبلی را بررسی کنیم؟

خیر.

هدف Password Reset این است که وقتی کاربر رمز قبلی را در اختیار ندارد، بتواند با استفاده از مکانیزم بازیابی معتبر رمز جدید تعیین کند.

بنابراین:


Current Password

در فرآیند Reset الزاماً در دسترس نیست.

اما در Password Change معمولاً باید رمز فعلی نیز بررسی شود.


# FAQ

آیا می‌توان رمز عبور قبلی را به کاربر ایمیل کرد؟

خیر.

رمز عبور نباید به صورت قابل بازیابی ذخیره شود. باید یک رمز جدید تعیین شود.

Token بازیابی چند دقیقه معتبر باشد؟

مدت اعتبار را می‌توان بر اساس نیاز سیستم تعیین کرد؛ برای بسیاری از سیستم‌ها بازه‌ای مانند 15 تا 60 دقیقه قابل بررسی است.

آیا Token خام را در دیتابیس ذخیره کنیم؟

بهتر است فقط Hash آن ذخیره شود.

آیا بعد از Reset کاربر را خودکار Login کنیم؟

ضرورتی ندارد و در بسیاری از سیستم‌ها بهتر است Sessionهای قبلی مدیریت/باطل شوند و کاربر دوباره Login کند.

آیا Password Reset نیاز به Rate Limit دارد؟

بله. هم درخواست‌های Reset و هم Login باید در برابر تلاش‌های بیش از حد محافظت شوند.

آیا JWT برای Password Reset لازم است؟

خیر. یک Token تصادفی، کوتاه‌عمر و یک‌بارمصرف معمولاً طراحی ساده و مناسبی برای این کاربرد است.


# جمع‌بندی

یک Password Reset امن فقط شامل یک فرم و یک لینک نیست.

یک پیاده‌سازی مناسب باید چند اصل مهم را هم‌زمان رعایت کند:


random_bytes()

       +

Token Hash

       +

Expiration

       +

Single-use

       +

HTTPS

       +

CSRF Protection

       +

Rate Limiting

       +

password_hash()

       +

Session Invalidation

       +

Generic Response

فرآیند اصلی نیز باید به شکل زیر باشد:


Forgot Password

       ↓

Email

       ↓

Secure Token

       ↓

Token Hash

       ↓

Database

       ↓

Email Link

       ↓

Token Validation

       ↓

New Password

       ↓

password_hash()

       ↓

Invalidate Token

       ↓

Invalidate Sessions

       ↓

Login

اگر این اصول رعایت شوند، Password Reset شما بسیار قابل اعتمادتر از پیاده‌سازی‌هایی خواهد بود که صرفاً یک Token ساده تولید می‌کنند و مستقیماً آن را در دیتابیس ذخیره می‌کنند.

در پروژه‌های بزرگ‌تر نیز بهتر است این منطق از Controller جدا شده و در یک Service مستقل قرار بگیرد تا مدیریت Token، ارسال ایمیل، Rate Limiting، Session Invalidation و ثبت رویدادهای امنیتی قابل توسعه‌تر باشد.

نظرات (0)

نظرات پس از تأیید مدیر نمایش داده می‌شوند

هنوز نظری ثبت نشده است؛ اولین نفر باشید!

مقالات مرتبط

ساخت CMS با Laravel 12؛ آموزش ساخت سیستم مدیریت محتوا از صفر

ساخت CMS با Laravel 12؛ آموزش ساخت سیستم مدیریت محتوا از صفر

۲۴ دقیقه
ساخت فروشگاه اینترنتی با Laravel 12؛ آموزش پروژه‌محور از صفر تا طراحی هسته فروشگاه

ساخت فروشگاه اینترنتی با Laravel 12؛ آموزش پروژه‌محور از صفر تا طراحی هسته فروشگاه

۲۲ دقیقه
ساخت Marketplace با Laravel؛ آموزش طراحی فروشگاه چندفروشندگی

ساخت Marketplace با Laravel؛ آموزش طراحی فروشگاه چندفروشندگی

۲۲ دقیقه
مدیریت Queueهای سنگین در Laravel؛ آموزش پردازش کارهای طولانی

مدیریت Queueهای سنگین در Laravel؛ آموزش پردازش کارهای طولانی

۲۲ دقیقه