剖析 WordPress `check_ajax_referer()` 函数的源码:如何验证 AJAX 请求中的 `Nonce`。

各位程序猿朋友们,大家好!今天咱们来聊聊WordPress里一个看似不起眼,实则非常重要的函数——check_ajax_referer()。 这家伙的主要任务是验证AJAX请求中的Nonce,确保你接收到的数据不是来自什么妖魔鬼怪,而是来自你的网站。 咱们今天就来扒一扒它的底裤,看看它到底是怎么玩的。

开场白:Nonce 是个啥?

在正式开始之前,先简单回顾一下Nonce的概念。 Nonce (Number used Once) 是一种安全令牌,就像一次性的密码,用来防止CSRF(跨站请求伪造)攻击。 简单来说,就是当你发起一个请求时,服务器给你一个随机数,你下次发起请求时要把这个数带上,服务器验证这个数是否正确,以此来判断请求是否来自你的网站。

check_ajax_referer() 函数的真面目

check_ajax_referer() 函数位于 wp-includes/functions.php 文件中。 它的基本用法如下:

check_ajax_referer( $action = -1, $query_arg = false, $die = true );
  • $action (string|int) (Optional) Action nonce. Default -1. 这是你创建Nonce时使用的action,稍后会详细讲解。
  • $query_arg (string|bool) (Optional) Key to check for the nonce in the $_REQUEST array. If false, will look for ‘_wpnonce’. Default false. 指定从哪个 $_REQUEST 变量中获取Nonce, 默认是 _wpnonce
  • $die (bool) (Optional) Whether to die early when the nonce is invalid. Default true. 如果验证失败,是否立即停止脚本执行,默认是 true

源码深度剖析:一层一层扒开它的心

现在,让我们深入到源码中,看看 check_ajax_referer() 到底做了些什么。

function check_ajax_referer( $action = -1, $query_arg = false, $die = true ) {
    if ( false === $query_arg ) {
        $query_arg = '_wpnonce';
    }

    $nonce = '';
    if ( isset( $_REQUEST[ $query_arg ] ) ) {
        $nonce = $_REQUEST[ $query_arg ];
    }

    $result = wp_verify_nonce( $nonce, $action );

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

    return $result;
}

让我们分解一下这段代码:

  1. 确定Nonce的来源:

    if ( false === $query_arg ) {
        $query_arg = '_wpnonce';
    }

    这段代码检查 $query_arg 是否为 false。 如果是,则将其设置为 '_wpnonce'。 这意味着,如果你没有明确指定从哪个 $_REQUEST 变量中获取Nonce,函数默认会从 $_REQUEST['_wpnonce'] 中获取。

  2. 获取Nonce值:

    $nonce = '';
    if ( isset( $_REQUEST[ $query_arg ] ) ) {
        $nonce = $_REQUEST[ $query_arg ];
    }

    这段代码从 $_REQUEST 数组中获取Nonce值。 首先,它检查 $query_arg 指定的键是否存在于 $_REQUEST 中。 如果存在,则将对应的值赋给 $nonce 变量。 如果不存在,$nonce 变量将保持为空字符串。

  3. 验证Nonce:

    $result = wp_verify_nonce( $nonce, $action );

    这是最关键的一步。 wp_verify_nonce() 函数负责验证Nonce的有效性。 它接收两个参数: $nonce (从请求中获取的Nonce值) 和 $action (用于生成Nonce的action)。 wp_verify_nonce() 函数会返回 1 (如果Nonce有效且在12小时内生成), 2 (如果Nonce有效但超过12小时,但在24小时内生成), 或者 false (如果Nonce无效或已过期)。

  4. 处理验证失败:

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

    如果 wp_verify_nonce() 返回 false, 并且 $die 参数为 true (默认值),则会调用 wp_nonce_ays() 函数并 exitwp_nonce_ays() 函数会显示一个确认页面,告诉用户请求被拒绝,因为Nonce无效。 这是一个防止CSRF攻击的重要措施。

  5. 返回验证结果:

    return $result;

    最后,函数返回 wp_verify_nonce() 的结果。 你可以根据这个结果来决定如何处理请求。

wp_verify_nonce() 函数的内部运作

check_ajax_referer() 依赖于 wp_verify_nonce() 来进行实际的验证。 让我们也简单看看 wp_verify_nonce() 的实现 (位于 wp-includes/pluggable.php)。

function wp_verify_nonce( $nonce, $action = -1 ) {
    $nonce = (string) $nonce;
    $action = (string) $action;

    $i = wp_nonce_tick();

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

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

    return false;
}

这段代码的核心逻辑是:

  1. 计算预期的Nonce值:

    $i = wp_nonce_tick();
    $expected = wp_hash( $i . $action, 'nonce' );

    wp_nonce_tick() 函数返回一个基于时间的整数,它每12小时更新一次。 wp_hash() 函数使用这个时间戳、 $action 和 ‘nonce’ 作为盐值来生成一个哈希值。 这个哈希值就是我们期望从请求中收到的Nonce值。

  2. 比较预期的Nonce值和实际收到的Nonce值:

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

    hash_equals() 函数用于比较两个哈希值,以防止时序攻击。 如果预期的Nonce值和实际收到的Nonce值匹配,则返回 1

  3. 检查旧的Nonce值:

    $expected = wp_hash( ( $i - 1 ) . $action, 'nonce' );
    if ( hash_equals( $expected, $nonce ) ) {
        return 2;
    }

    为了处理客户端和服务器之间的时间差异,函数还会检查12小时前的Nonce值是否有效。 如果有效,则返回 2

  4. 验证失败:

    return false;

    如果以上所有检查都失败,则说明Nonce无效,函数返回 false

wp_nonce_tick() 函数:时间才是关键

wp_nonce_tick() 函数在Nonce的生成和验证过程中扮演着关键角色。 让我们看看它的实现 (位于 wp-includes/pluggable.php)。

function wp_nonce_tick() {
    /**
     * Filters the lifespan of nonces.
     *
     * @since 4.7.0
     *
     * @param int $lifespan Nonce lifespan in seconds. Default is 12 hours.
     */
    $lifespan = apply_filters( 'nonce_life', DAY_IN_SECONDS / 2 );

    return ceil( time() / ( $lifespan ) );
}

这段代码很简单:

  1. 获取Nonce的有效期:

    $lifespan = apply_filters( 'nonce_life', DAY_IN_SECONDS / 2 );

    apply_filters( 'nonce_life', DAY_IN_SECONDS / 2 ) 允许你通过 nonce_life 过滤器来修改Nonce的有效期。 默认情况下,Nonce的有效期是半天 (12小时)。 DAY_IN_SECONDS 是一个常量,表示一天有多少秒 (86400)。

  2. 计算时间戳:

    return ceil( time() / ( $lifespan ) );

    time() 函数返回当前的时间戳 (从Unix纪元开始的秒数)。 将时间戳除以Nonce的有效期,然后向上取整,得到一个整数。 这个整数就是 wp_nonce_tick() 函数的返回值。 这个值每12小时 (默认情况下) 都会改变, 从而保证了Nonce的安全性。

生成 Nonce:wp_create_nonce() 函数

既然我们已经了解了如何验证Nonce,那么也需要知道如何生成Nonce。 WordPress 提供了 wp_create_nonce() 函数来生成Nonce。

$nonce = wp_create_nonce( $action );

$action 参数是一个字符串,用于标识Nonce的目的。 例如,你可以使用 'delete_post_' . $post_id 作为删除文章的Nonce的action。 wp_create_nonce() 函数会返回一个唯一的Nonce值。

让我们看看 wp_create_nonce() 的源码 (位于 wp-includes/pluggable.php)。

function wp_create_nonce( $action = -1 ) {
    $action = (string) $action;
    $i = wp_nonce_tick();

    return wp_hash( $i . $action, 'nonce' );
}

这段代码非常简单:

  1. 获取时间戳:

    $i = wp_nonce_tick();

    调用 wp_nonce_tick() 函数获取当前的时间戳。

  2. 生成哈希值:

    return wp_hash( $i . $action, 'nonce' );

    使用时间戳、 $action 和 ‘nonce’ 作为盐值来生成一个哈希值。 这个哈希值就是生成的Nonce值。

把它们串起来:一个完整的 AJAX 示例

现在,让我们通过一个完整的示例来演示如何使用 check_ajax_referer() 函数。

1. PHP 端 (处理 AJAX 请求):

<?php
add_action( 'wp_ajax_my_ajax_action', 'my_ajax_callback' );
add_action( 'wp_ajax_nopriv_my_ajax_action', 'my_ajax_callback' ); // 如果需要未登录用户也能使用

function my_ajax_callback() {
    // 验证 Nonce
    check_ajax_referer( 'my_nonce_action', 'security' );

    // 获取传递过来的数据
    $data = $_POST['data'];

    // 处理数据...
    $response = array(
        'success' => true,
        'message' => '数据已成功处理!',
        'data' => $data,
    );

    // 返回 JSON 格式的响应
    wp_send_json( $response );

    // Always die in functions echoing AJAX results
    die();
}

2. JavaScript 端 (发起 AJAX 请求):

jQuery(document).ready(function($) {
    $('#my-button').click(function() {
        var data = {
            'action': 'my_ajax_action',
            'security': my_ajax_object.ajax_nonce, // 从 WordPress 传递过来的 Nonce
            'data': 'Hello, world!',
        };

        $.post(my_ajax_object.ajax_url, data, function(response) {
            if (response.success) {
                alert(response.message);
            } else {
                alert('请求失败!');
            }
        });
    });
});

3. 在 WordPress 中传递 Nonce 和 AJAX URL:

<?php
add_action( 'wp_enqueue_scripts', 'enqueue_my_ajax_script' );

function enqueue_my_ajax_script() {
    wp_enqueue_script( 'my-ajax-script', get_template_directory_uri() . '/js/my-ajax-script.js', array( 'jquery' ), '1.0', true );

    wp_localize_script( 'my-ajax-script', 'my_ajax_object', array(
        'ajax_url' => admin_url( 'admin-ajax.php' ),
        'ajax_nonce' => wp_create_nonce( 'my_nonce_action' ),
    ) );
}

代码解释:

  • PHP 端:

    • add_action() 函数用于注册 AJAX actions。 wp_ajax_my_ajax_action 用于已登录用户, wp_ajax_nopriv_my_ajax_action 用于未登录用户。
    • check_ajax_referer( 'my_nonce_action', 'security' ) 验证 Nonce。 'my_nonce_action' 是 action, 'security'$_POST 数组中包含Nonce的键。
    • wp_send_json() 函数用于返回 JSON 格式的响应。
    • die() 函数是必须的,用于停止脚本执行。
  • JavaScript 端:

    • 使用 jQuery 的 $.post() 函数发起 AJAX 请求。
    • my_ajax_object.ajax_urlmy_ajax_object.ajax_nonce 是从 WordPress 传递过来的 AJAX URL 和 Nonce。
  • WordPress 端:

    • wp_enqueue_script() 函数用于加载 JavaScript 文件。
    • wp_localize_script() 函数用于将 PHP 变量传递给 JavaScript。 在这里,我们将 AJAX URL 和 Nonce 传递给 JavaScript。
    • wp_create_nonce( 'my_nonce_action' ) 函数生成 Nonce。 'my_nonce_action' 是 action。

重点总结:

为了更好地理解,我们用表格来总结一下 check_ajax_referer() 的关键点:

概念 描述 作用
Nonce 一次性使用的随机数,用于防止 CSRF 攻击。 验证请求的来源,确保请求来自你的网站。
$action 用于生成和验证 Nonce 的字符串。 必须在生成和验证时使用相同的值。 区分不同的 AJAX 请求,提高安全性。
wp_create_nonce() 用于生成 Nonce 的函数。 创建 Nonce 值。
check_ajax_referer() 用于验证 AJAX 请求中的 Nonce 的函数。 验证 Nonce 的有效性,防止 CSRF 攻击。
wp_verify_nonce() 实际验证 Nonce 的函数。 比较预期的 Nonce 值和实际收到的 Nonce 值,判断 Nonce 是否有效。
wp_nonce_tick() 返回一个基于时间的整数,用于 Nonce 的生成和验证。 这个值每 12 小时更新一次 (默认情况下)。 保证 Nonce 的有效期,防止重放攻击。
$_REQUEST 用于存储 HTTP 请求参数的数组。 Nonce 通常通过 $_REQUEST['_wpnonce'] 传递。 从请求中获取 Nonce 值。

实战建议和安全最佳实践:

  • 永远不要在客户端生成 Nonce。 Nonce 应该始终在服务器端生成,并传递给客户端。
  • 使用唯一的 action。 为每个 AJAX 请求使用不同的 action,以提高安全性。 例如,可以使用 'delete_post_' . $post_id 作为删除文章的Nonce的action。
  • 缩短 Nonce 的有效期。 如果你的应用程序对安全性要求很高,可以考虑缩短 Nonce 的有效期。 可以通过 nonce_life 过滤器来修改 Nonce 的有效期。
  • 始终验证 Nonce。 在处理 AJAX 请求之前,始终验证 Nonce。
  • 使用 HTTPS。 使用 HTTPS 可以加密客户端和服务器之间的通信,防止中间人攻击。

总结:

check_ajax_referer() 函数是 WordPress 中一个重要的安全函数,用于防止 CSRF 攻击。 通过理解其源码和使用方法,可以更好地保护你的 WordPress 网站。 希望今天的讲解能帮助大家更深入地理解 WordPress 的安全机制。 下次再见!

发表回复

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