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

同学们,各位靓仔靓女,晚上好!今天咱们来聊聊 WordPress 中一个非常重要的安全机制:check_ajax_referer() 函数,特别是它如何验证 AJAX 请求中的 Nonce。这东西就像 AJAX 请求的通行证,没它,别想进门!

一、什么是 Nonce?为什么要用它?

首先,咱们得搞清楚 Nonce 是个啥玩意儿。Nonce,英文全称是 "Number used Once",顾名思义,就是“一次性使用的数字”。 在 WordPress 里,Nonce 可不是简单的数字,而是一个加密的字符串,用来防止 CSRF (Cross-Site Request Forgery,跨站请求伪造) 攻击。

CSRF 攻击是啥呢?简单来说,就是攻击者伪造你的身份,在未经你授权的情况下,执行某些操作。 举个例子,假设你登录了银行网站,正在浏览你的账户信息。 这时候,攻击者通过某种方式(比如邮件里的恶意链接),诱使你点击了一个链接,这个链接指向银行网站,并且包含了转账的请求。 如果银行网站没有采取 CSRF 防护措施,那么这个请求就有可能被执行,你的钱就被转走了!

Nonce 的作用就是,给每个请求加上一个只有你知道的“暗号”,服务器验证这个暗号,确认请求确实来自你的页面,而不是攻击者伪造的。

二、check_ajax_referer() 函数登场!

在 WordPress 的 AJAX 请求中,check_ajax_referer() 函数就是负责验证这个 Nonce 的大管家。 它的作用是:

  1. 接收 Nonce 值: 从 AJAX 请求中获取 Nonce 值。
  2. 验证 Nonce 值: 使用 WordPress 的内部函数,验证 Nonce 值是否有效。
  3. 如果验证失败: 优雅地拒绝请求,并可能返回错误信息。

三、check_ajax_referer() 的源码剖析

咱们来看看 check_ajax_referer() 的源码,看看它到底是怎么工作的。这个函数位于 wp-includes/pluggable.php 文件中(不同 WordPress 版本可能会略有差异,但核心逻辑不变)。

function check_ajax_referer( $action = -1, $query_arg = false, $die = true ) {
    if ( -1 == $action ) {
        _doing_it_wrong( __FUNCTION__, __( 'You should specify a nonce action to be verified.' ), '3.2' );
        return false;
    }

    $query_arg = $query_arg ? $query_arg : '_wpnonce';

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

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

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

    return $result;
}

让我们来逐行解读一下:

  1. function check_ajax_referer( $action = -1, $query_arg = false, $die = true )

    • 定义了 check_ajax_referer() 函数,它接收三个参数:
      • $action:一个字符串,用于生成 Nonce。 这个参数非常重要,它决定了 Nonce 的唯一性。
      • $query_arg:可选参数,指定 Nonce 在请求中的参数名。 默认是 '_wpnonce'
      • $die:可选参数,指定验证失败时是否立即终止脚本执行。 默认是 true
  2. if ( -1 == $action ) { ... }

    • 检查是否指定了 $action。 如果没有指定,会触发一个 _doing_it_wrong() 函数,提示开发者需要指定一个 Nonce action。
  3. $query_arg = $query_arg ? $query_arg : '_wpnonce';

    • 如果 $query_arg 没有指定,则默认使用 '_wpnonce' 作为参数名。
  4. if ( isset( $_REQUEST[ $query_arg ] ) ) { ... } else { ... }

    • 尝试从 $_REQUEST 超全局变量中获取 Nonce 值。 $_REQUEST 包含了 $_GET$_POST$_COOKIE 的数据。
    • 如果找到了 Nonce,就赋值给 $nonce 变量; 否则,将 $nonce 设置为空字符串。
  5. $result = wp_verify_nonce( $nonce, $action );

    • 调用 wp_verify_nonce() 函数来验证 Nonce 的有效性。 这是整个验证过程的核心! wp_verify_nonce() 函数会返回 1(有效)、2(过期)或 false(无效)。
  6. if ( false === $result && $die ) { ... }

    • 如果验证失败($resultfalse),并且 $die 参数为 true(默认值),则调用 wp_nonce_ays() 函数显示一个确认页面,并终止脚本执行。
  7. return $result;

    • 返回验证结果。

四、wp_verify_nonce() 函数:Nonce 验证的灵魂

现在,咱们把目光聚焦到 wp_verify_nonce() 函数,这个函数才是 Nonce 验证的灵魂所在。 它位于 wp-includes/pluggable.php 文件中。

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

    $i = wp_nonce_tick();

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

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

    return false;
}

咱们来一步步解析:

  1. function wp_verify_nonce( $nonce, $action = -1 )

    • 定义了 wp_verify_nonce() 函数,它接收两个参数:
      • $nonce:要验证的 Nonce 值。
      • $action:用于生成 Nonce 的 Action 值。
  2. $nonce = (string) $nonce;

    • $nonce 强制转换为字符串类型。
  3. $i = wp_nonce_tick();

    • 调用 wp_nonce_tick() 函数获取一个时间戳,这个时间戳会随着时间变化。
  4. $expected = wp_hash( $i . $action . get_current_user_id(), 'nonce' );

    • 使用 wp_hash() 函数,根据当前时间戳 $i、Action 值 $action 和当前用户 ID 生成一个预期 Nonce 值。
    • wp_hash() 函数实际上是 hash_hmac() 函数的封装,它使用 WordPress 的盐 (salt) 来增加安全性。
  5. if ( hash_equals( $expected, $nonce ) ) { ... }

    • 使用 hash_equals() 函数比较预期 Nonce 值 $expected 和实际接收到的 Nonce 值 $nonce
    • hash_equals() 函数可以防止时序攻击,即使攻击者知道 Nonce 的生成方式,也无法轻易伪造 Nonce。
    • 如果 Nonce 值匹配,则返回 1,表示 Nonce 有效。
  6. $expected = wp_hash( ( $i - 1 ) . $action . get_current_user_id(), 'nonce' );

    • 如果第一次验证失败,则尝试使用前一个时间戳 $i - 1 重新生成预期 Nonce 值,并进行比较。
    • 这是为了处理 Nonce 在时间间隔的边界情况下失效的问题。
  7. if ( hash_equals( $expected, $nonce ) ) { ... }

    • 如果 Nonce 值匹配,则返回 2,表示 Nonce 有效,但已经过期。
  8. return false;

    • 如果以上两次验证都失败,则返回 false,表示 Nonce 无效。

五、wp_nonce_tick() 函数:时间戳的秘密

wp_nonce_tick() 函数负责生成一个随时间变化的时间戳,它位于 wp-includes/pluggable.php 文件中。

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

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

咱们来看一下:

  1. function wp_nonce_tick() { ... }

    • 定义了 wp_nonce_tick() 函数。
  2. $nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS / 2 );

    • 计算 Nonce 的有效期。 默认情况下,Nonce 的有效期是半天(12 小时)。
    • DAY_IN_SECONDS 是 WordPress 定义的一个常量,表示一天有多少秒 (60 * 60 * 24)。
    • apply_filters( 'nonce_life', ... ) 允许开发者通过 nonce_life 过滤器修改 Nonce 的有效期。
  3. return ceil( time() / ( $nonce_life ) );

    • 返回当前时间戳除以 Nonce 有效期,然后向上取整的结果。
    • 这个结果会随着时间的推移而变化,从而保证 Nonce 的唯一性。

六、生成 Nonce:wp_create_nonce() 函数

既然咱们了解了如何验证 Nonce,那么如何生成 Nonce 呢? WordPress 提供了 wp_create_nonce() 函数,它位于 wp-includes/pluggable.php 文件中。

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

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

可以看到,wp_create_nonce() 函数的实现非常简单:

  1. function wp_create_nonce( $action = -1 ) { ... }

    • 定义了 wp_create_nonce() 函数,它接收一个参数:
      • $action:用于生成 Nonce 的 Action 值。
  2. $i = wp_nonce_tick();

    • 调用 wp_nonce_tick() 函数获取时间戳。
  3. return wp_hash( $i . $action . get_current_user_id(), 'nonce' );

    • 使用 wp_hash() 函数,根据时间戳 $i、Action 值 $action 和当前用户 ID 生成 Nonce 值。

七、实战演练:一个简单的 AJAX 请求

现在,咱们来用一个简单的例子来演示如何使用 Nonce 来保护 AJAX 请求。

1. 在 PHP 文件中生成 Nonce:

<?php
// 在 functions.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() {
  check_ajax_referer( 'my_nonce_action' ); // 验证 Nonce

  // 处理 AJAX 请求的逻辑
  $data = $_POST['data'];
  $response = '接收到的数据:' . $data;

  wp_send_json_success( $response );
}

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

  // 将 AJAX URL 和 Nonce 传递给 JavaScript
  wp_localize_script( 'my-ajax-script', 'my_ajax_object', array(
    'ajax_url' => admin_url( 'admin-ajax.php' ),
    'nonce' => wp_create_nonce( 'my_nonce_action' )
  ) );
}
add_action( 'wp_enqueue_scripts', 'enqueue_my_scripts' );

2. 在 JavaScript 文件中发送 AJAX 请求:

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

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

3. 在 HTML 文件中添加按钮:

<button id="my-button">发送 AJAX 请求</button>

在这个例子中:

  • 'my_nonce_action' 是 Nonce 的 Action 值。
  • wp_create_nonce( 'my_nonce_action' ) 生成 Nonce。
  • wp_localize_script() 函数将 AJAX URL 和 Nonce 传递给 JavaScript。
  • JavaScript 将 Nonce 作为 _wpnonce 参数发送到服务器。
  • check_ajax_referer( 'my_nonce_action' ) 验证 Nonce。

八、总结与最佳实践

check_ajax_referer() 函数是 WordPress 中一个非常重要的安全机制,它可以有效地防止 CSRF 攻击。 在使用 AJAX 请求时,务必使用 Nonce 来保护你的数据。

以下是一些最佳实践:

  • 为每个 AJAX 请求使用不同的 Action 值。 这样可以增加 Nonce 的唯一性,提高安全性。
  • 定期更新 Nonce。 虽然 Nonce 有一定的有效期,但定期更新 Nonce 可以进一步提高安全性。
  • 不要在客户端生成 Nonce。 客户端生成的 Nonce 容易被攻击者获取。
  • 始终验证 Nonce。 即使你认为你的 AJAX 请求不重要,也应该验证 Nonce。

表格总结:

函数 作用 参数 返回值
check_ajax_referer() 验证 AJAX 请求中的 Nonce $action (Action 值), $query_arg (Nonce 参数名), $die (验证失败是否终止脚本) 1 (有效), 2 (过期), false (无效)
wp_verify_nonce() 验证 Nonce 的有效性 $nonce (Nonce 值), $action (Action 值) 1 (有效), 2 (过期), false (无效)
wp_nonce_tick() 生成随时间变化的时间戳 时间戳
wp_create_nonce() 生成 Nonce $action (Action 值) Nonce 值
wp_localize_script() 将数据传递给 JavaScript $handle (脚本句柄), $object_name (JavaScript 对象名), $l10n (数据数组)

好了,今天的讲座就到这里。 希望大家能够理解 check_ajax_referer() 函数的原理和使用方法,并在实际开发中加以应用, 保护你的 WordPress 站点安全! 感谢大家的聆听!下次再见!

发表回复

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