详解 WordPress `check_ajax_referer()` 函数源码:在 AJAX 请求中验证 `Nonce` 的流程。

各位代码界的弄潮儿,大家好!我是你们的老朋友,今天咱们来聊聊WordPress里一个非常关键,但又经常被忽略的小可爱——check_ajax_referer()。这玩意儿,在保护你的AJAX请求免受CSRF攻击方面,那可是个顶梁柱。

想象一下,你辛辛苦苦搭建的网站,突然被人从背后捅了一刀,用户数据被篡改,甚至整个站点都被控制了,是不是想想都觉得可怕?check_ajax_referer()就像一个忠诚的门卫,帮你挡住那些心怀不轨的家伙。

咱们今天就来深入剖析一下它的源码,看看它到底是怎么工作的,以及如何在你的代码里把它用得溜溜的。

一、Nonce是个啥玩意儿?为啥要用它?

在深入check_ajax_referer()之前,咱们先来搞清楚Nonce是个什么东东。这玩意儿,其实就是一个一次性的、随机生成的字符串。它的主要作用是防止跨站请求伪造(CSRF)攻击。

简单来说,CSRF攻击就是攻击者诱骗用户在不知情的情况下,以用户的身份执行某些操作。比如,用户登录了银行网站,攻击者构造了一个恶意链接,用户点击后,可能就会在不知不觉中向攻击者的账户转账。

Nonce的出现,就相当于给每一个敏感操作加上了一个密码。这个密码是动态生成的,每次都不一样,而且只能使用一次。这样,即使攻击者构造了恶意链接,也无法获得正确的Nonce值,从而无法执行攻击。

举个例子,假设我们有一个删除文章的AJAX请求。如果没有Nonce保护,攻击者可以轻易地构造一个恶意链接,诱骗管理员点击,从而删除文章。但是,如果我们加上Nonce,情况就不一样了:

  1. 生成Nonce 在页面加载时,我们使用wp_create_nonce()函数生成一个Nonce值,并把它嵌入到HTML代码中。

    <?php $nonce = wp_create_nonce( 'delete_post_' . $post_id ); ?>
    <a href="#" class="delete-post" data-post-id="<?php echo $post_id; ?>" data-nonce="<?php echo $nonce; ?>">删除文章</a>
  2. 传递Nonce 当用户点击“删除文章”链接时,我们通过AJAX请求将Nonce值发送到服务器。

    jQuery(document).ready(function($) {
        $('.delete-post').click(function(e) {
            e.preventDefault();
            var postId = $(this).data('post-id');
            var nonce = $(this).data('nonce');
    
            $.ajax({
                url: ajaxurl, // WordPress定义的全局变量,指向admin-ajax.php
                type: 'POST',
                data: {
                    action: 'delete_post', // AJAX action
                    post_id: postId,
                    nonce: nonce
                },
                success: function(response) {
                    // 处理成功响应
                    console.log(response);
                }
            });
        });
    });
  3. 验证Nonce 在服务器端,我们使用check_ajax_referer()函数验证Nonce值是否正确。

    <?php
    add_action( 'wp_ajax_delete_post', 'my_delete_post' ); // 登录用户
    add_action( 'wp_ajax_nopriv_delete_post', 'my_delete_post' ); // 未登录用户
    
    function my_delete_post() {
        check_ajax_referer( 'delete_post_' . $_POST['post_id'], 'nonce' ); // 验证Nonce
    
        $post_id = intval( $_POST['post_id'] );
    
        // 只有管理员才能删除文章
        if ( ! current_user_can( 'delete_post', $post_id ) ) {
            wp_send_json_error( '权限不足' );
        }
    
        $result = wp_delete_post( $post_id, true ); // true表示强制删除
    
        if ( $result ) {
            wp_send_json_success( '文章已删除' );
        } else {
            wp_send_json_error( '删除失败' );
        }
    
        wp_die(); // 结束AJAX请求
    }

这样,即使攻击者构造了恶意链接,也无法获得正确的Nonce值,从而无法删除文章。

二、check_ajax_referer()源码剖析

现在,咱们终于要进入正题了,一起来看看check_ajax_referer()的源码:

/**
 * Verifies that a correct security nonce was used with time limit.
 *
 * The user is automatically logged out if the nonce is invalid.
 *
 * @since 2.0.3
 *
 * @param int|string $action  Action name. Should give the context to what is taking place and be the same when nonce was created.
 * @param string      $query_arg Optional. Key to check for the nonce in `$_REQUEST` (since 2.5).
 *                               If false, the function will not look in `$_REQUEST` for the nonce.
 *                               Default: `_wpnonce`.
 * @param bool        $die       Optional. Whether to die early when the nonce find fails.
 *                               Default: true.
 * @return bool True if the nonce is valid, false otherwise.
 */
function check_ajax_referer( $action = -1, $query_arg = false, $die = true ) {
    $result = wp_verify_nonce( $_REQUEST[ $query_arg ], $action );
    if ( false === $result ) {
        if ( $die ) {
            wp_nonce_ays( $action );
            exit;
        } else {
            return false;
        }
    }

    return true;
}

是不是感觉代码很简单? 别被它的外表迷惑了,咱们来一行一行地解读它:

  1. 参数:

    • $action:这个参数是用来生成Nonce时的action名称。它应该与生成Nonce时使用的action名称保持一致。
    • $query_arg:这个参数指定了在$_REQUEST数组中查找Nonce值的键名。默认情况下,它是_wpnonce。也就是说,check_ajax_referer()函数会默认从$_REQUEST['_wpnonce']中获取Nonce值。
    • $die:这个参数指定了当Nonce验证失败时,是否立即终止脚本的执行。默认情况下,它是true。如果设置为false,则函数会返回false,你可以自己处理错误。
  2. wp_verify_nonce()

    $result = wp_verify_nonce( $_REQUEST[ $query_arg ], $action );

    这行代码是check_ajax_referer()函数的核心。它调用了wp_verify_nonce()函数来验证Nonce值是否正确。wp_verify_nonce()函数会检查Nonce值是否过期,以及是否与指定的action名称匹配。如果Nonce值无效,wp_verify_nonce()函数会返回false

  3. 错误处理:

    if ( false === $result ) {
        if ( $die ) {
            wp_nonce_ays( $action );
            exit;
        } else {
            return false;
        }
    }

    这段代码处理了Nonce验证失败的情况。如果$die参数为true(默认值),则会调用wp_nonce_ays()函数来显示一个友好的错误提示信息,并终止脚本的执行。如果$die参数为false,则函数会返回false,你可以自己处理错误。

  4. 返回结果:

    return true;

    如果Nonce验证成功,函数会返回true

三、wp_verify_nonce()源码探索

既然check_ajax_referer()的核心是wp_verify_nonce(),那咱们就继续深入,看看wp_verify_nonce()到底做了些什么:

/**
 * Verifies that a nonce is valid.
 *
 * The user is automatically logged out if the nonce is invalid.
 *
 * @since 2.0.3
 *
 * @param string|int $nonce  Nonce that was used in the form to verify
 *                            that the form request was valid.
 * @param string|int $action Optional. Action name. Should give the context
 *                            to what is taking place and be the same when
 *                            nonce was created.
 * @return false|int False if the nonce is invalid, 1 if the nonce is valid and generated between
 *                   0-12 hours ago, 2 if the nonce is valid and generated between 12-24 hours ago.
 */
function wp_verify_nonce( $nonce, $action = -1 ) {
    $nonce = (string) $nonce;
    $uid = (int) get_current_user_id();
    $token = wp_get_session_token();

    if ( empty( $nonce ) ) {
        return false;
    }

    $i = wp_nonce_tick();

    // Nonce generated 0-12 hours ago
    $expected = substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 1;
    }

    // Nonce generated 12-24 hours ago
    $expected = substr( wp_hash( ( $i - 1 ) . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 2;
    }

    /**
     * Fires when a nonce verification fails.
     *
     * @since 4.5.0
     *
     * @param string|int $nonce  Nonce that was used in the form to verify
     *                            that the form request was valid.
     * @param string|int $action Optional. Action name. Should give the context
     *                            to what is taking place and be the same when
     *                            nonce was created.
     */
    do_action( 'wp_verify_nonce_failed', $nonce, $action );

    return false;
}

这个函数稍微复杂一些,咱们一步一步来:

  1. 参数:

    • $nonce:要验证的Nonce值。
    • $action:生成Nonce时使用的action名称。
  2. 获取用户ID和会话Token:

    $uid = (int) get_current_user_id();
    $token = wp_get_session_token();

    这两行代码获取当前用户的ID和会话Token。用户ID用于确保Nonce只对当前用户有效,会话Token则提供额外的安全性。 wp_get_session_token() 函数用来获取用户的 session token。 如果用户没有登录,它会返回一个随机的 token。

  3. wp_nonce_tick()

    $i = wp_nonce_tick();

    这行代码调用了wp_nonce_tick()函数来获取一个时间戳。这个时间戳用于计算Nonce的有效期。wp_nonce_tick()函数返回一个整数,表示当前时间距离某个固定时间点的秒数,并且每隔12小时会更新一次。

  4. 计算期望的Nonce值:

    $expected = substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );

    这行代码使用wp_hash()函数计算期望的Nonce值。wp_hash()函数使用MD5算法对字符串进行哈希运算。这里,我们将时间戳、action名称、用户ID和会话Token拼接在一起,然后进行哈希运算,得到一个唯一的哈希值。然后,我们使用substr()函数截取哈希值的最后10位作为期望的Nonce值。 这里截取了 10 位,而不是全部的 hash 值,是为了减小 nonce 的长度,方便在 URL 中传递。

  5. 验证Nonce值:

    if ( hash_equals( $expected, $nonce ) ) {
        return 1;
    }

    这行代码使用hash_equals()函数比较实际的Nonce值和期望的Nonce值是否相等。hash_equals()函数可以防止时序攻击。如果Nonce值相等,则表示Nonce验证成功。

    hash_equals()函数是 PHP 5.6 中引入的一个函数,用于比较两个字符串是否相等。与直接使用 == 相比,hash_equals() 可以防止时序攻击。 时序攻击是指攻击者通过测量比较操作的时间来推断字符串的内容。 因为 == 在比较字符串时,如果发现两个字符串从一开始就不相等,它会立即返回 false。 攻击者可以通过测量比较操作的时间来判断两个字符串在前几个字符是否相等。 而 hash_equals() 会比较两个字符串的所有字符,无论它们是否相等,因此可以防止时序攻击。

  6. 处理Nonce过期的情况:

    $expected = substr( wp_hash( ( $i - 1 ) . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 2;
    }

    这段代码处理了Nonce过期的情况。由于wp_nonce_tick()函数每隔12小时更新一次,所以Nonce的有效期为24小时。如果Nonce是在12-24小时之前生成的,则我们需要使用前一个时间戳来计算期望的Nonce值。

  7. 错误处理:

    do_action( 'wp_verify_nonce_failed', $nonce, $action );
    return false;

    如果Nonce验证失败,则会触发wp_verify_nonce_failed这个action,你可以通过hook这个action来记录日志或者执行其他操作。

  8. 返回结果:

    • falseNonce验证失败。
    • 1Nonce验证成功,且是在0-12小时之前生成的。
    • 2Nonce验证成功,且是在12-24小时之前生成的。

四、如何正确使用check_ajax_referer()

现在,咱们已经了解了check_ajax_referer()wp_verify_nonce()的源码,接下来咱们来聊聊如何在你的代码里正确使用check_ajax_referer()

  1. 选择合适的action名称:

    action名称应该能够清晰地描述当前的操作。例如,如果我们要删除文章,可以使用delete_post作为action名称。为了增加安全性,可以将action名称与文章ID结合起来,例如delete_post_123

  2. 确保action名称的一致性:

    在生成Nonce和验证Nonce时,必须使用相同的action名称。否则,Nonce验证会失败。

  3. 选择合适的query_arg

    query_arg参数指定了在$_REQUEST数组中查找Nonce值的键名。默认情况下,它是_wpnonce。你可以根据自己的需要修改这个参数。例如,如果你想使用my_nonce作为键名,可以这样写:

    check_ajax_referer( 'delete_post', 'my_nonce' );

    确保在生成Nonce时,也使用相同的键名:

    <?php $nonce = wp_create_nonce( 'delete_post' ); ?>
    <input type="hidden" name="my_nonce" value="<?php echo $nonce; ?>">
  4. 处理Nonce验证失败的情况:

    如果Nonce验证失败,check_ajax_referer()函数会默认终止脚本的执行。你可以通过将$die参数设置为false来禁用这个行为,并自己处理错误。

    if ( ! check_ajax_referer( 'delete_post', 'nonce', false ) ) {
        wp_send_json_error( 'Nonce验证失败' );
    }
  5. 使用 wp_localize_script() 传递 nonce

    在 WordPress 中,推荐使用 wp_localize_script() 函数将 PHP 变量传递给 JavaScript。 这样做可以避免直接在 HTML 中嵌入 PHP 代码,提高代码的可维护性和安全性。

    例如:

    // 在 WordPress 后台 enqueue 你的 JavaScript 文件
    function my_enqueue_scripts() {
       wp_enqueue_script( 'my-ajax-script', get_template_directory_uri() . '/js/my-ajax-script.js', array( 'jquery' ), '1.0', true );
    
       // 将 nonce 传递给 JavaScript
       wp_localize_script( 'my-ajax-script', 'my_ajax_object', array(
           'ajax_url' => admin_url( 'admin-ajax.php' ),
           'delete_post_nonce' => wp_create_nonce( 'delete_post_' . get_the_ID() )
       ));
    }
    add_action( 'wp_enqueue_scripts', 'my_enqueue_scripts' );

    然后在你的 JavaScript 文件中,你可以这样使用 nonce:

    jQuery(document).ready(function($) {
       $('.delete-post').click(function(e) {
           e.preventDefault();
           var postId = $(this).data('post-id');
    
           $.ajax({
               url: my_ajax_object.ajax_url, // 使用 wp_localize_script 传递的 ajax_url
               type: 'POST',
               data: {
                   action: 'delete_post',
                   post_id: postId,
                   nonce: my_ajax_object.delete_post_nonce // 使用 wp_localize_script 传递的 nonce
               },
               success: function(response) {
                   // 处理成功响应
                   console.log(response);
               },
               error: function( jqXHR, textStatus, errorThrown ){
                   console.log( 'Error: ' + errorThrown );
               }
           });
       });
    });

五、总结

check_ajax_referer()是WordPress中一个非常重要的安全函数。它可以帮助你保护你的AJAX请求免受CSRF攻击。在使用check_ajax_referer()时,一定要注意选择合适的action名称,确保action名称的一致性,选择合适的query_arg,并处理Nonce验证失败的情况。

希望今天的讲解对你有所帮助。记住,安全无小事,时刻保持警惕,才能让你的网站更加安全可靠。 下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注