各位代码界的弄潮儿,大家好!我是你们的老朋友,今天咱们来聊聊WordPress里一个非常关键,但又经常被忽略的小可爱——check_ajax_referer()
。这玩意儿,在保护你的AJAX请求免受CSRF攻击方面,那可是个顶梁柱。
想象一下,你辛辛苦苦搭建的网站,突然被人从背后捅了一刀,用户数据被篡改,甚至整个站点都被控制了,是不是想想都觉得可怕?check_ajax_referer()
就像一个忠诚的门卫,帮你挡住那些心怀不轨的家伙。
咱们今天就来深入剖析一下它的源码,看看它到底是怎么工作的,以及如何在你的代码里把它用得溜溜的。
一、Nonce
是个啥玩意儿?为啥要用它?
在深入check_ajax_referer()
之前,咱们先来搞清楚Nonce
是个什么东东。这玩意儿,其实就是一个一次性的、随机生成的字符串。它的主要作用是防止跨站请求伪造(CSRF)攻击。
简单来说,CSRF攻击就是攻击者诱骗用户在不知情的情况下,以用户的身份执行某些操作。比如,用户登录了银行网站,攻击者构造了一个恶意链接,用户点击后,可能就会在不知不觉中向攻击者的账户转账。
Nonce
的出现,就相当于给每一个敏感操作加上了一个密码。这个密码是动态生成的,每次都不一样,而且只能使用一次。这样,即使攻击者构造了恶意链接,也无法获得正确的Nonce
值,从而无法执行攻击。
举个例子,假设我们有一个删除文章的AJAX请求。如果没有Nonce
保护,攻击者可以轻易地构造一个恶意链接,诱骗管理员点击,从而删除文章。但是,如果我们加上Nonce
,情况就不一样了:
-
生成
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>
-
传递
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); } }); }); });
-
验证
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;
}
是不是感觉代码很简单? 别被它的外表迷惑了,咱们来一行一行地解读它:
-
参数:
$action
:这个参数是用来生成Nonce
时的action
名称。它应该与生成Nonce
时使用的action
名称保持一致。$query_arg
:这个参数指定了在$_REQUEST
数组中查找Nonce
值的键名。默认情况下,它是_wpnonce
。也就是说,check_ajax_referer()
函数会默认从$_REQUEST['_wpnonce']
中获取Nonce
值。$die
:这个参数指定了当Nonce
验证失败时,是否立即终止脚本的执行。默认情况下,它是true
。如果设置为false
,则函数会返回false
,你可以自己处理错误。
-
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
。 -
错误处理:
if ( false === $result ) { if ( $die ) { wp_nonce_ays( $action ); exit; } else { return false; } }
这段代码处理了
Nonce
验证失败的情况。如果$die
参数为true
(默认值),则会调用wp_nonce_ays()
函数来显示一个友好的错误提示信息,并终止脚本的执行。如果$die
参数为false
,则函数会返回false
,你可以自己处理错误。 -
返回结果:
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;
}
这个函数稍微复杂一些,咱们一步一步来:
-
参数:
$nonce
:要验证的Nonce
值。$action
:生成Nonce
时使用的action
名称。
-
获取用户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。 -
wp_nonce_tick()
:$i = wp_nonce_tick();
这行代码调用了
wp_nonce_tick()
函数来获取一个时间戳。这个时间戳用于计算Nonce
的有效期。wp_nonce_tick()
函数返回一个整数,表示当前时间距离某个固定时间点的秒数,并且每隔12小时会更新一次。 -
计算期望的
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 中传递。 -
验证
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()
会比较两个字符串的所有字符,无论它们是否相等,因此可以防止时序攻击。 -
处理
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
值。 -
错误处理:
do_action( 'wp_verify_nonce_failed', $nonce, $action ); return false;
如果
Nonce
验证失败,则会触发wp_verify_nonce_failed
这个action,你可以通过hook这个action来记录日志或者执行其他操作。 -
返回结果:
false
:Nonce
验证失败。1
:Nonce
验证成功,且是在0-12小时之前生成的。2
:Nonce
验证成功,且是在12-24小时之前生成的。
四、如何正确使用check_ajax_referer()
现在,咱们已经了解了check_ajax_referer()
和wp_verify_nonce()
的源码,接下来咱们来聊聊如何在你的代码里正确使用check_ajax_referer()
。
-
选择合适的
action
名称:action
名称应该能够清晰地描述当前的操作。例如,如果我们要删除文章,可以使用delete_post
作为action
名称。为了增加安全性,可以将action
名称与文章ID结合起来,例如delete_post_123
。 -
确保
action
名称的一致性:在生成
Nonce
和验证Nonce
时,必须使用相同的action
名称。否则,Nonce
验证会失败。 -
选择合适的
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; ?>">
-
处理
Nonce
验证失败的情况:如果
Nonce
验证失败,check_ajax_referer()
函数会默认终止脚本的执行。你可以通过将$die
参数设置为false
来禁用这个行为,并自己处理错误。if ( ! check_ajax_referer( 'delete_post', 'nonce', false ) ) { wp_send_json_error( 'Nonce验证失败' ); }
-
使用
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
验证失败的情况。
希望今天的讲解对你有所帮助。记住,安全无小事,时刻保持警惕,才能让你的网站更加安全可靠。 下次再见!