در وردپرس، JWT (JSON Web Token) یک مکانیزم احراز هویت بدون نشست است که برای اپ موبایل، SPA و سرویس‌های خارجی طراحی شده و جایگزین Cookie و Session می‌شود. بدون HTTPS، بدون انقضای کوتاه و بدون Refresh Token، هر توکن JWT به یک ریسک امنیتی جدی تبدیل می‌شود و در صورت لو رفتن، تا زمان انقضا معتبر باقی می‌ماند. ساختار Header، Payload و Signature در JWT باید دقیق پیاده‌سازی شود و اشتباه در انتخاب Algorithm یا Secret، امنیت توکن را به‌طور کامل از بین می‌برد. لغو توکن در JWT یکی از چالش‌های اصلی است چون توکن بدون حالت (Stateless) ذخیره می‌شود و برای لغو، نیازمند Blacklist یا Rotation Strategy هستید. تست JWT در سه سطح ساختار توکن، اعتبارسنجی امضا و لغو انجام می‌شود و بدون آن، انتشار به تولید ریسک بالایی دارد. در این راهنما از ساختار پایه تا استقرار تولیدی JWT در وردپرس را با نگاه مهندسی و کد عملی پوشش می‌دهیم.

در پروژه‌های واقعی، اولین بار که برای یک اپ موبایل JWT پیاده می‌کنم، همیشه با این سؤال مواجه می‌شوم: چگونه توکن را لغو کنیم؟ این راهنما از همان نقطه‌ای شروع می‌کند که تجربه می‌گوید بیشترین ارزش را دارد.

JWT چیست و چه تفاوتی با Session دارد؟

JWT یک استاندارد باز (RFC 7519) برای انتقال امن اطلاعات بین دو طرف است. این توکن شامل سه بخش است و با یک Secret یا Private Key امضا می‌شود.

تفاوت اصلی JWT با Session: JWT Stateless است و در سرور ذخیره نمی‌شود. Session Stateful است و در سرور نگهداری می‌شود. JWT برای اپ موبایل و API مقیاس‌پذیر مناسب‌تر است، اما لغو آن پیچیده‌تر است.

پیش از ادامه، راهنمای REST API Authentication و احراز هویت را مطالعه کنید تا تصویر کامل‌تری از گزینه‌های موجود داشته باشید.

مقایسه JWT و Application Password

معیارJWTApplication Password
Statelessبلهخیر
انقضا خودکاربلهخیر
Refresh Tokenبلهخیر
پیچیدگیبالاکم

ساختار Header، Payload و Signature

JWT از سه بخش تشکیل شده که با نقطه جدا می‌شوند:

header.payload.signature

# Header (Base64URL)
{"alg":"HS256","typ":"JWT"}

# Payload (Base64URL)
{
  "iss": "https://example.com",
  "sub": 5,
  "iat": 1728389094,
  "exp": 1728392694,
  "role": "administrator"
}

# Signature
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

پارامترهای استاندارد در Payload شامل iss (صادرکننده)، sub (موضوع)، aud (مخاطب)، exp (انقضا)، nbf (قبل از این زمان معتبر نیست) و iat (زمان صدور) است.

Claimهای سفارشی

می‌توانید Claimهای سفارشی مثل role، capabilities یا tenant اضافه کنید. اما توصیه می‌کنم داده حساس را در توکن قرار ندهید چون Payload قابل رمزگشایی است.

انتخاب Algorithm و Secret

سه خانواده Algorithm در JWT وجود دارد:

  1. HS256/HS384/HS512: HMAC با Secret متقارن.
  2. RS256/RS384/RS512: RSA با کلید نامتقارن.
  3. ES256/ES384/ES512: ECDSA با کلید نامتقارن.

برای پروژه‌های کوچک و متوسط، HS256 کافی است. برای پروژه‌های بزرگ با چند سرویس، RS256 انتخاب بهتری است چون کلید عمومی را می‌توان به اشتراک گذاشت.

مدیریت Secret در وردپرس

// در wp-config.php
define( "WPK_JWT_SECRET", "your-random-64-char-secret" );

هرگز Secret را در دیتابیس یا فایل‌های قالب ذخیره نکنید. برای پروژه‌های حساس، از Environment Variable استفاده کنید. راهنمای امن‌سازی API Key و Secret را ببینید.

انقضا و Refresh Token

انقضای Access Token باید کوتاه باشد: ۱۵ دقیقه تا ۱ ساعت. این بازه، پنجره حمله در صورت لو رفتن توکن را محدود می‌کند.

Access Token:  15 دقیقه تا 1 ساعت
Refresh Token: 7 تا 30 روز
Refresh Rotation: هر بار Refresh، Refresh Token جدید صادر شود.

Refresh Token باید در دیتابیس ذخیره شود تا بتوان آن را لغو کرد. Access Token به‌صورت Stateless اعتبارسنجی می‌شود.

Refresh Rotation و جلوگیری از Replay

در هر بار استفاده از Refresh Token، باید Refresh Token قدیمی باطل و نسخه جدید صادر شود. اگر Refresh Token قدیمی دوباره استفاده شد، نشانه حمله است و همه توکن‌های کاربر باید باطل شوند.

صدور JWT در وردپرس

برای صدور JWT، یک Endpoint اختصاصی در REST API ایجاد می‌کنیم.

<?php
add_action( "rest_api_init", function() {
    register_rest_route( "wpk/v1", "/auth/login", array(
        "methods"             => "POST",
        "callback"            => "wpk_jwt_login",
        "permission_callback" => "__return_true",
        "args"                => array(
            "username" => array( "required" => true, "type" => "string" ),
            "password" => array( "required" => true, "type" => "string" ),
        ),
    ) );
} );

function wpk_jwt_login( WP_REST_Request $request ) {
    $username = sanitize_user( $request->get_param( "username" ) );
    $password = $request->get_param( "password" );

    $user = wp_authenticate( $username, $password );
    if ( is_wp_error( $user ) ) {
        return new WP_Error( "wpk_invalid_credentials", "نام کاربری یا رمز عبور نادرست است.", array( "status" => 401 ) );
    }

    $access_token  = wpk_jwt_sign( $user->ID, 15 * MINUTE_IN_SECONDS );
    $refresh_token = wpk_jwt_create_refresh_token( $user->ID );

    return new WP_REST_Response( array(
        "access_token"  => $access_token,
        "refresh_token" => $refresh_token,
        "expires_in"    => 900,
        "token_type"    => "Bearer",
    ), 200 );
}

تابع امضای JWT

<?php
function wpk_jwt_sign( $user_id, $ttl ) {
    $header = wpk_base64url_encode( wp_json_encode( array( "alg" => "HS256", "typ" => "JWT" ) ) );

    $payload = wpk_base64url_encode( wp_json_encode( array(
        "iss" => home_url(),
        "sub" => $user_id,
        "iat" => time(),
        "exp" => time() + $ttl,
        "jti" => wp_generate_uuid4(),
    ) ) );

    $signature = wpk_base64url_encode(
        hash_hmac( "sha256", $header . "." . $payload, WPK_JWT_SECRET, true )
    );

    return $header . "." . $payload . "." . $signature;
}

function wpk_base64url_encode( $data ) {
    return rtrim( strtr( base64_encode( $data ), "+/", "-_" ), "=" );
}

function wpk_base64url_decode( $data ) {
    return base64_decode( strtr( $data, "-_", "+/" ) );
}

اعتبارسنجی JWT در Endpoint

برای اعتبارسنجی، فیلتر determine_current_user را گسترش می‌دهیم تا کاربر را از توکن استخراج کند.

<?php
add_filter( "determine_current_user", function( $user_id ) {
    if ( $user_id ) {
        return $user_id;
    }

    $auth = isset( $_SERVER["HTTP_AUTHORIZATION"] ) ? $_SERVER["HTTP_AUTHORIZATION"] : "";
    if ( 0 !== stripos( $auth, "Bearer " ) ) {
        return $user_id;
    }

    $token = trim( substr( $auth, 7 ) );
    return wpk_jwt_validate( $token );
} );

function wpk_jwt_validate( $token ) {
    $parts = explode( ".", $token );
    if ( 3 !== count( $parts ) ) {
        return false;
    }

    list( $header, $payload, $signature ) = $parts;

    $expected = wpk_base64url_encode(
        hash_hmac( "sha256", $header . "." . $payload, WPK_JWT_SECRET, true )
    );

    if ( ! hash_equals( $expected, $signature ) ) {
        return false;
    }

    $data = json_decode( wpk_base64url_decode( $payload ), true );
    if ( ! is_array( $data ) ) {
        return false;
    }

    if ( ! isset( $data["exp"] ) || $data["exp"] < time() ) {
        return false;
    }

    if ( isset( $data["jti"] ) && get_transient( "wpk_jwt_blacklist_" . $data["jti"] ) ) {
        return false;
    }

    return isset( $data["sub"] ) ? (int) $data["sub"] : false;
}

دو نکته حیاتی: استفاده از hash_equals() برای جلوگیری از Timing Attack و بررسی Blacklist برای توکن‌های لغوشده.

اعتبارسنجی Algorithm

همیشه Algorithm را در سمت سرور بررسی کنید. هرگز به Header توکن اعتماد نکنید و Algorithm را از Secret خودتان تعیین کنید.

// اشتباه: استفاده از alg از Header توکن
$algorithm = $decoded_header["alg"];

// درست: تعیین Algorithm در سمت سرور
$algorithm = "HS256";

لغو توکن و Blacklist

در JWT، لغو توکن نیازمند Blacklist است. هر JWT یک jti (JWT ID) دارد که می‌توان آن را در Blacklist قرار داد.

<?php
function wpk_jwt_revoke( $token ) {
    $parts = explode( ".", $token );
    if ( 3 !== count( $parts ) ) {
        return false;
    }

    $data = json_decode( wpk_base64url_decode( $parts[1] ), true );
    if ( ! isset( $data["jti"], $data["exp"] ) ) {
        return false;
    }

    $ttl = max( 1, $data["exp"] - time() );
    set_transient( "wpk_jwt_blacklist_" . $data["jti"], 1, $ttl );
    return true;
}

TTL Blacklist باید معادل زمان باقی‌مانده توکن باشد. پس از انقضا، نیازی به نگه‌داشتن Blacklist نیست.

لغو همه توکن‌های یک کاربر

<?php
function wpk_jwt_revoke_all( $user_id ) {
    update_user_meta( $user_id, "wpk_jwt_not_before", time() );
}

// در validate
if ( isset( $data["iat"] ) ) {
    $not_before = (int) get_user_meta( $data["sub"], "wpk_jwt_not_before", true );
    if ( $not_before && $data["iat"] < $not_before ) {
        return false;
    }
}

Refresh Token حرفه‌ای

Refresh Token باید در دیتابیس ذخیره شود تا بتوان آن را لغو کرد.

<?php
function wpk_jwt_create_refresh_token( $user_id ) {
    global $wpdb;
    $token  = bin2hex( random_bytes( 32 ) );
    $hash   = hash( "sha256", $token );
    $expiry = time() + ( 30 * DAY_IN_SECONDS );

    $wpdb->insert(
        $wpdb->prefix . "wpk_jwt_refresh",
        array(
            "user_id"    => $user_id,
            "token_hash" => $hash,
            "expires_at" => gmdate( "Y-m-d H:i:s", $expiry ),
            "created_at" => current_time( "mysql" ),
        ),
        array( "%d", "%s", "%s", "%s" )
    );

    return $token;
}

توکن خام فقط یک بار به کلاینت ارسال می‌شود و در دیتابیس فقط Hash ذخیره می‌شود. این الگو، همان رویکرد Password Hash است.

Refresh Rotation

<?php
function wpk_jwt_rotate_refresh( $old_token ) {
    global $wpdb;
    $hash = hash( "sha256", $old_token );

    $row = $wpdb->get_row(
        $wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}wpk_jwt_refresh WHERE token_hash = %s AND expires_at > NOW()",
            $hash
        )
    );

    if ( ! $row ) {
        return false;
    }

    $wpdb->delete(
        $wpdb->prefix . "wpk_jwt_refresh",
        array( "id" => $row->id ),
        array( "%d" )
    );

    return wpk_jwt_create_refresh_token( $row->user_id );
}

تست و دیباگ JWT

تست JWT در سه سطح انجام می‌شود: سطح ساختار توکن، سطح اعتبارسنجی امضا و سطح لغو.

# تست صدور توکن
curl -X POST https://example.com/wp-json/wpk/v1/auth/login 
  -H "Content-Type: application/json" 
  -d '{"username":"admin","password":"xxx"}'

# تست دسترسی با توکن
curl https://example.com/wp-json/wp/v2/posts 
  -H "Authorization: Bearer eyJhbGciOi..."

# تست توکن منقضی
# مقدار exp را در jwt.io دستی تغییر دهید و بررسی کنید 401 برگردد.

برای تست‌های خودکار، راهنمای تست E2E وردپرس با Playwright را ببینید.

اشتباهات رایج در تست

اشتباه اول، نبود تست با توکن منقضی. اشتباه دوم، نبود تست با امضای اشتباه. اشتباه سوم، نبود تست با alg تغییر یافته. اشتباه چهارم، نبود تست با توکن لغوشده. اشتباه پنجم، نبود تست با کاربر حذف‌شده.

اشتباهات امنیتی رایج

چهار اشتباه رایج که در پروژه‌های واقعی دیده‌ام:

  1. اجرای JWT روی HTTP بدون HTTPS.
  2. اعتماد به alg از Header توکن (Algorithm Confusion Attack).
  3. انقضای طولانی برای Access Token.
  4. ذخیره توکن در LocalStorage بدون محافظت XSS.
// اشتباه
$algorithm = $header["alg"];

// درست
$algorithm = "HS256";
if ( ! in_array( $algorithm, array( "HS256", "RS256" ), true ) ) {
    return false;
}

برای مطالعه بیشتر، راهنمای Escape کردن خروجی برای جلوگیری از XSS را ببینید. همچنین مفهوم JSON Web Token را در ویکی‌پدیا مرور کنید.

ذخیره امن توکن در کلاینت

در اپ موبایل، توکن را در Keychain (iOS) یا Keystore (Android) ذخیره کنید. در SPA، از HttpOnly Cookie استفاده کنید. هرگز از LocalStorage برای توکن حساس استفاده نکنید.

پرسش‌های پرتکرار درباره JWT

آیا JWT جایگزین Session می‌شود؟

برای اپ موبایل و API، بله. برای وب‌سایت معمولی، Session همچنان ساده‌تر و امن‌تر است.

آیا JWT امن است؟

بله، اگر HTTPS، انقضای کوتاه، Refresh Rotation و Blacklist پیاده شود.

چگونه توکن را لغو کنم؟

با Blacklist بر اساس jti یا با تغییر wpk_jwt_not_before برای لغو همه توکن‌های یک کاربر.

آیا می‌توان JWT را در کوکی ذخیره کرد؟

بله، اما توصیه می‌کنم در Header Authorization استفاده کنید تا CSRF را حذف کنید.

آیا JWT از داده رمزنگاری‌شده پشتیبانی می‌کند؟

بله، با JWE (JSON Web Encryption). اما در وردپرس معمولاً از JWS (امضا) استفاده می‌شود.

آیا Application Password ساده‌تر از JWT نیست؟

بله، اگر نیاز به Refresh Token و Stateless ندارید، Application Password کافی است. راهنمای REST API Authentication را ببینید.

نتیجه و مسیر ادامه

JWT مکانیزم احراز هویت بدون نشست برای اپ موبایل و SPA است. کلید موفقیت، ساختار درست توکن، انتخاب Algorithm مناسب، انقضای کوتاه، Refresh Rotation، Blacklist برای لغو و HTTPS اجباری است. اگر این لایه‌ها با دقت طراحی شوند، اپلیکیشن در برابر حملات احراز هویت پایدار می‌ماند.

پیشنهاد می‌کنم مسیر یادگیری را با REST API Authentication و احراز هویت ادامه دهید و سپس Custom Table و ساختار داده سفارشی را به‌عنوان مکمل مطالعه کنید.

اگر روی پروژه واقعی خود JWT پیاده کرده‌اید، برایم جالب است بدانید کدام بخش — Refresh Rotation یا Blacklist — بیشترین چالش را ایجاد کرده است. تجربه خودتان را در دیدگاه‌ها بنویسید.