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

咳咳,各位观众老爷们,晚上好!今天咱们来聊聊 WordPress 里一个挺重要的安全机制——check_ajax_referer(),看看它是怎么帮我们验证 AJAX 请求中的“身份证明”的。

咱们先热热身:什么是 Nonce?

Nonce,这词儿听起来挺高大上,其实就是 "Number used once" 的缩写,翻译过来就是“一次性使用的数字”。 在 WordPress 里,它是一个随机生成的字符串,用来防止 CSRF(Cross-Site Request Forgery,跨站请求伪造)攻击。 简单来说,CSRF 攻击就是坏人冒充你偷偷地执行一些操作,比如偷偷发个帖子、偷偷改个密码啥的。

WordPress 使用 Nonce 来确保发起的请求确实来自你的网站,而不是其他地方伪造的。 想象一下,你的网站就像一个城堡,每个允许进入城堡的人都要有一张独特的通行证 (Nonce)。这张通行证只能用一次,用完就作废,下次来还得重新领一张。

check_ajax_referer() 的作用:守门大爷的职责

check_ajax_referer() 函数就像是城堡的守门大爷,专门负责检查 AJAX 请求里有没有携带正确的通行证 (Nonce)。 如果通行证不对,或者根本没有通行证,守门大爷就会毫不客气地把请求拒之门外。

check_ajax_referer() 的用法:怎么给 AJAX 请求加上通行证?

要使用 check_ajax_referer(),通常需要两个步骤:

  1. 生成 Nonce: 在你的 PHP 代码里,用 wp_create_nonce() 函数生成一个 Nonce。 这个函数会返回一个随机字符串,你可以把它嵌入到你的 HTML 表单或者 JavaScript 代码里。

    <?php
    $nonce = wp_create_nonce( 'my_ajax_action' ); // 'my_ajax_action' 是一个唯一的 action 名称
    ?>

    这里的 'my_ajax_action' 就像是通行证的类型,不同的操作应该使用不同的 action 名称,这样才能更安全。

  2. 在 AJAX 请求中发送 Nonce: 把生成的 Nonce 放到 AJAX 请求的数据里,一起发送到服务器。

    jQuery(document).ready(function($) {
        $('#my_button').click(function() {
            $.ajax({
                url: ajaxurl, // WordPress 定义的 AJAX URL
                type: 'POST',
                data: {
                    action: 'my_ajax_action', // PHP 函数处理 AJAX 请求的 action 名称
                    nonce: '<?php echo $nonce; ?>', // 传递生成的 Nonce
                    some_data: '一些数据'
                },
                success: function(response) {
                    console.log(response);
                }
            });
        });
    });
  3. 验证 Nonce: 在你的 PHP 代码里,用 check_ajax_referer() 函数验证 AJAX 请求里携带的 Nonce 是否正确。

    <?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_ajax_action', 'nonce' ); // 'my_ajax_action' 是 action 名称,'nonce' 是 AJAX 请求中 Nonce 的字段名
    
        // 如果 Nonce 验证通过,就可以执行你的逻辑了
        $data = $_POST['some_data'];
        $response = '你发送的数据是:' . $data;
    
        wp_send_json_success( $response ); // 返回 JSON 格式的成功响应
    
        wp_die(); // 结束 AJAX 请求
    }
    ?>

    这里的 'my_ajax_action' 必须和生成 Nonce 时使用的 action 名称一致。'nonce' 是 AJAX 请求中 Nonce 字段的名称,通常是 nonce,但你也可以自定义。

深入 check_ajax_referer() 的源码:看看守门大爷是怎么工作的

check_ajax_referer() 函数实际上是对 wp_verify_nonce() 函数的一个封装,专门用于 AJAX 请求的 Nonce 验证。 让我们来扒一扒它的源码,看看守门大爷是怎么工作的:

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_die(
                __( 'Are you sure you want to do this?' ),
                __( 'Cheatin’ huh?' ),
                array( 'response' => 403 )
            );
        }

        return false;
    }

    return $result;
}

让我们一行一行地解读一下:

  1. $result = wp_verify_nonce( $_REQUEST[ $query_arg ], $action );:这行代码是核心。 它调用了 wp_verify_nonce() 函数来验证 Nonce。$_REQUEST[ $query_arg ]$_REQUEST 数组中获取 Nonce 的值,$action 是 action 名称。 $_REQUEST 包含了 $_GET$_POST$_COOKIE 的内容。$query_arg 指的是 AJAX 请求中传递 Nonce 的参数名,默认为 _wpnonce,但是在 check_ajax_referer 函数中,你可以指定这个参数名。

  2. if ( false === $result ) { ... }:如果 wp_verify_nonce() 返回 false,说明 Nonce 验证失败。

  3. if ( $die ) { ... }:如果 $die 参数为 true(默认值),wp_die() 函数会立即停止脚本的执行,并显示一个错误信息。 wp_die() 就像一个急刹车,防止恶意请求继续执行。

  4. wp_die( __( 'Are you sure you want to do this?' ), __( 'Cheatin’ huh?' ), array( 'response' => 403 ) );wp_die() 函数会显示一个错误信息,告诉用户:“你确定你要这么做吗?” 错误信息的标题是 “Cheatin’ huh?” (作弊,是吧?)。 array( 'response' => 403 ) 设置 HTTP 响应状态码为 403 (Forbidden,禁止访问)。

  5. return false;:如果 $die 参数为 falsecheck_ajax_referer() 函数会返回 false,而不是直接停止脚本的执行。 这样,你就可以在你的代码里自定义错误处理逻辑。

  6. return $result;:如果 Nonce 验证通过,check_ajax_referer() 函数会返回 wp_verify_nonce() 的返回值。 wp_verify_nonce() 的返回值是 Nonce 的生成时间戳,可以用来判断 Nonce 是否过期。

wp_verify_nonce() 的源码:Nonce 验证的幕后英雄

wp_verify_nonce() 函数才是真正干活的,让我们继续深入,看看它是怎么验证 Nonce 的:

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

    $i = wp_nonce_tick();

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

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

    /**
     * Fires when a nonce verification fails.
     *
     * @since 4.4.0
     *
     * @param string     $nonce  The nonce that was used.
     * @param string|int $action The action that was used.
     */
    do_action( 'wp_nonce_failed', $nonce, $action );

    return false;
}

再来一行一行地解读一下:

  1. $nonce = (string) $nonce;:把 Nonce 转换成字符串类型,防止类型不匹配导致验证失败。

  2. $i = wp_nonce_tick();:调用 wp_nonce_tick() 函数获取一个时间戳“刻度”。 这个刻度会根据时间变化,用于生成不同的 Nonce。

  3. $expected = substr( wp_hash( $i . $action . get_current_user_id(), 'nonce' ), -12, 10 );:这行代码生成一个“期望的” Nonce。 它把时间戳刻度 $i、action 名称 $action 和当前用户 ID 连接起来,然后用 wp_hash() 函数进行哈希运算,最后取哈希值的后 10 位作为“期望的” Nonce。

  4. if ( hash_equals( $expected, $nonce ) ) { return 1; }:这行代码比较“期望的” Nonce 和 AJAX 请求里携带的 Nonce 是否相等。 hash_equals() 函数是一个安全比较函数,可以防止时序攻击。 如果 Nonce 相等,说明验证通过,返回 1

  5. $expected = substr( wp_hash( ( $i - 1 ) . $action . get_current_user_id(), 'nonce' ), -12, 10 );:这行代码生成一个“旧的” Nonce。 它使用上一个时间戳刻度 ( $i - 1 ) 来生成 Nonce。 这是为了防止 Nonce 过期太快,允许请求有一定的延迟。

  6. if ( hash_equals( $expected, $nonce ) ) { return 2; }:这行代码比较“旧的” Nonce 和 AJAX 请求里携带的 Nonce 是否相等。 如果 Nonce 相等,说明验证通过,返回 2

  7. do_action( 'wp_nonce_failed', $nonce, $action );:如果 Nonce 验证失败,触发 wp_nonce_failed action,允许开发者执行一些自定义的逻辑,比如记录日志、发送邮件等等。

  8. return false;:如果 Nonce 验证失败,返回 false

wp_nonce_tick() 的源码:时间戳刻度的秘密

wp_nonce_tick() 函数负责生成时间戳刻度,让我们看看它的源码:

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

    return ceil( time() / ( $lifespan ) );
}
  1. $lifespan = apply_filters( 'nonce_life', DAY_IN_SECONDS / 2 );:这行代码获取 Nonce 的有效期。 默认情况下,Nonce 的有效期是半天 (12 小时)。 apply_filters() 函数允许开发者通过 nonce_life filter 来修改 Nonce 的有效期。

  2. return ceil( time() / ( $lifespan ) );:这行代码计算时间戳刻度。 它把当前时间戳除以 Nonce 的有效期,然后向上取整。 这样,每隔一段时间,时间戳刻度就会变化一次,从而生成新的 Nonce。

总结:Nonce 验证的流程

现在,我们来总结一下 Nonce 验证的整个流程:

  1. 生成 Nonce: 使用 wp_create_nonce() 函数生成一个 Nonce。 这个函数会调用 wp_nonce_tick() 函数获取时间戳刻度,然后把时间戳刻度、action 名称和当前用户 ID 连接起来,进行哈希运算,最后取哈希值的后 10 位作为 Nonce。

  2. 在 AJAX 请求中发送 Nonce: 把生成的 Nonce 放到 AJAX 请求的数据里,一起发送到服务器。

  3. 验证 Nonce: 使用 check_ajax_referer() 函数验证 AJAX 请求里携带的 Nonce 是否正确。 这个函数会调用 wp_verify_nonce() 函数来验证 Nonce。 wp_verify_nonce() 函数会根据当前时间戳刻度和上一个时间戳刻度生成两个“期望的” Nonce,然后分别和 AJAX 请求里携带的 Nonce 进行比较。 如果 Nonce 匹配,说明验证通过;否则,验证失败。

表格总结

函数名 作用 参数 返回值
wp_create_nonce() 生成一个 Nonce $action (string):action 名称,用于区分不同的操作 Nonce (string):随机生成的字符串
check_ajax_referer() 验证 AJAX 请求中的 Nonce $action (string):action 名称,必须和生成 Nonce 时使用的 action 名称一致;$query_arg (string):AJAX 请求中 Nonce 字段的名称,默认为 _wpnonce$die (bool):是否在验证失败时停止脚本的执行,默认为 true 如果验证通过,返回 wp_verify_nonce() 的返回值 (1 或 2);如果验证失败,且 $dietrue,则停止脚本的执行并显示错误信息;如果验证失败,且 $diefalse,则返回 false
wp_verify_nonce() 验证 Nonce 是否有效 $nonce (string):要验证的 Nonce;$action (string):action 名称,必须和生成 Nonce 时使用的 action 名称一致 如果验证通过,且 Nonce 是当前时间戳生成的,返回 1;如果验证通过,且 Nonce 是上一个时间戳生成的,返回 2;如果验证失败,返回 false
wp_nonce_tick() 获取时间戳刻度,用于生成 Nonce 时间戳刻度 (int):根据当前时间戳和 Nonce 有效期计算出来的整数

一些需要注意的地方:

  • Action 名称的唯一性: 不同的操作应该使用不同的 action 名称,这样可以防止 Nonce 被滥用。
  • Nonce 的有效期: Nonce 的有效期默认是 12 小时,你可以通过 nonce_life filter 来修改。 Nonce 的有效期不宜过长,否则容易被破解;也不宜过短,否则会影响用户体验。
  • HTTPS: 如果你的网站使用了 HTTPS,可以防止中间人攻击,从而提高 Nonce 的安全性。
  • 安全比较: 使用 hash_equals() 函数进行 Nonce 的比较,可以防止时序攻击。

举个栗子:评论功能的 Nonce 验证

假设你要给 WordPress 的评论功能添加 Nonce 验证,可以这样做:

  1. 生成 Nonce:comment_form() 函数里,添加一个隐藏字段,用于存储 Nonce。

    <?php
    function my_comment_form_defaults( $defaults ) {
        $defaults['fields']['nonce'] = '<p class="comment-form-nonce"><input type="hidden" name="comment_nonce" value="' . wp_create_nonce( 'comment_nonce' ) . '" /></p>';
        return $defaults;
    }
    add_filter( 'comment_form_defaults', 'my_comment_form_defaults' );
    ?>
  2. 验证 Nonce:wp_insert_comment() 函数执行之前,验证 Nonce 是否正确。

    <?php
    function my_pre_comment_on_post( $commentdata ) {
        if ( ! isset( $_POST['comment_nonce'] ) || ! wp_verify_nonce( $_POST['comment_nonce'], 'comment_nonce' ) ) {
            wp_die( 'Cheatin’ huh?' );
        }
        return $commentdata;
    }
    add_filter( 'preprocess_comment', 'my_pre_comment_on_post' );
    ?>

总结

check_ajax_referer() 函数是 WordPress 里一个非常重要的安全机制,它可以帮助我们防止 CSRF 攻击,保护我们的网站免受恶意请求的侵害。 理解它的工作原理,可以让我们更好地利用它,提高我们网站的安全性。

好了,今天的讲座就到这里,希望大家有所收获! 下次有机会再和大家分享其他的 WordPress 技术干货。 拜拜!

发表回复

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