深入理解 `wp_verify_nonce()` 函数的源码,它是如何通过重新生成 `Nonce` 并进行对比来验证其有效性的?

各位好,今天咱们来聊聊WordPress里一个看似简单,实则暗藏玄机的函数:wp_verify_nonce()。别看它名字里有个“verify”(验证),实际上它背后的逻辑可比表面功夫复杂多了。咱们的目标是:把这玩意儿扒个精光,让你以后再看到它,就像看到老朋友一样亲切。

开场白:Nonce是个啥?为啥要验证?

在深入源码之前,咱们先搞清楚Nonce是啥。Nonce,全称Number used once,顾名思义,就是“一次性使用的数字”。在WordPress里,它被用来防止CSRF(Cross-Site Request Forgery,跨站请求伪造)攻击。

想象一下,你在银行网站登录了,然后坏人发给你一个链接,点进去后,悄悄地以你的名义转账。如果没有Nonce,银行服务器就没法区分这个请求是不是你亲自发起的。Nonce就像一个暗号,只有你和服务器知道,每次请求都要带上这个暗号,服务器才能确认这个请求是你本人发起的。

wp_verify_nonce() 函数的作用,就是验证这个“暗号”是不是有效。如果有效,说明请求很可能是合法的;如果无效,那就很有可能是CSRF攻击,果断拒绝!

源码解剖:一步一步走进wp_verify_nonce()

好了,理论知识铺垫完毕,现在咱们深入到wp_verify_nonce()的源码里,看看它到底是怎么工作的。

首先,我们找到这个函数的定义(通常在wp-includes/pluggable.php文件中):

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

    $nonce_tick = wp_nonce_tick();

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

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

    return false;
}

别被这一堆代码吓到,咱们一步一步来分析。

  1. 参数转换:
$nonce = (string) $nonce;
$action = (string) $action;

这两行代码很简单,就是把传入的$nonce$action参数强制转换为字符串类型。这样做是为了防止类型不一致导致的问题。

  1. 获取Nonce的“时间片”:
$nonce_tick = wp_nonce_tick();

wp_nonce_tick()函数是Nonce验证的关键。它会返回一个基于时间的数值,这个数值会影响Nonce的生成。我们来看看wp_nonce_tick()的源码:

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

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

这个函数的核心是time() / ( $nonce_life )time()返回当前时间的Unix时间戳(从1970年1月1日到现在的秒数)。$nonce_life默认是半天(12小时,DAY_IN_SECONDS / 2)。所以,time() / ( $nonce_life )的结果就是当前时间距离1970年1月1日有多少个12小时。ceil()函数则对结果向上取整。

简单来说,wp_nonce_tick()返回的是当前时间的12小时时间片。这意味着,在同一个12小时内生成的Nonce,wp_nonce_tick()的返回值是相同的。

  1. 生成期望的Nonce:
$expected = substr( wp_hash( $nonce_tick . '|' . $action . '|' . get_current_user_id() . '|' . wp_get_session_token(), 'nonce' ), -12, 10 );

这行代码是生成期望的Nonce的关键。我们来分解一下:

  • $nonce_tick . '|' . $action . '|' . get_current_user_id() . '|' . wp_get_session_token(): 这部分是将$nonce_tick(时间片)、$action(动作名称)、get_current_user_id()(当前用户ID)和wp_get_session_token()(会话令牌)拼接成一个字符串,用|分隔。get_current_user_id()返回当前登录用户的ID,如果用户未登录,则返回0。wp_get_session_token()返回当前用户的会话令牌,这个令牌在用户登录时生成,用于识别用户。
  • wp_hash(..., 'nonce'): wp_hash() 函数使用WordPress的哈希算法对拼接的字符串进行哈希。第二个参数 'nonce' 是一个“算法名称”,它会影响哈希算法的选择。WordPress会根据这个名称选择合适的哈希算法。
  • substr(..., -12, 10): substr() 函数从哈希后的字符串中截取最后12位,然后取前10位作为最终的$expected值。这样做是为了缩短Nonce的长度,同时增加破解的难度。
  1. 对比Nonce:
if ( hash_equals( $expected, $nonce ) ) {
    return 1;
}

这行代码使用 hash_equals() 函数来比较期望的Nonce $expected 和传入的Nonce $nonce 是否相等。hash_equals() 函数是一个安全字符串比较函数,可以防止时序攻击。如果两个Nonce相等,说明验证通过,函数返回1。

  1. 检查前一个时间片:
$expected = substr( wp_hash( ( $nonce_tick - 1 ) . '|' . $action . '|' . get_current_user_id() . '|' . wp_get_session_token(), 'nonce' ), -12, 10 );
if ( hash_equals( $expected, $nonce ) ) {
    return 2;
}

这部分代码是为了处理Nonce过期的情况。由于$nonce_tick是基于12小时的时间片,如果用户在某个时间片内生成了Nonce,但是提交请求的时间跨越了两个时间片,那么第一次验证就会失败。为了解决这个问题,wp_verify_nonce()还会检查前一个时间片生成的Nonce是否有效。如果前一个时间片的Nonce有效,函数返回2。

  1. 验证失败:
return false;

如果以上所有验证都失败了,说明Nonce无效,函数返回false

总结:wp_verify_nonce()的验证流程

为了更清晰地理解wp_verify_nonce()的验证流程,我们可以用一张表格来总结:

步骤 描述 涉及的变量
1 获取当前时间的12小时时间片。 $nonce_tick
2 使用当前时间片、动作名称、用户ID和会话令牌生成期望的Nonce。 $expected, $nonce_tick, $action, get_current_user_id(), wp_get_session_token()
3 比较期望的Nonce和传入的Nonce是否相等。 $expected, $nonce
4 如果相等,验证通过,返回1。
5 如果不相等,使用前一个时间片、动作名称、用户ID和会话令牌生成期望的Nonce。 $expected, $nonce_tick, $action, get_current_user_id(), wp_get_session_token()
6 比较期望的Nonce和传入的Nonce是否相等。 $expected, $nonce
7 如果相等,验证通过,返回2。
8 如果以上所有验证都失败,验证失败,返回false

实战演练:如何正确使用wp_verify_nonce()

光说不练假把式,现在咱们来演示一下如何正确使用wp_verify_nonce()

首先,在你的表单中添加一个Nonce字段:

<form method="post" action="">
    <?php wp_nonce_field( 'my_action', 'my_nonce' ); ?>
    <input type="text" name="my_field">
    <input type="submit" value="提交">
</form>

wp_nonce_field() 函数会自动生成一个隐藏的Nonce字段。它的第一个参数是$action,也就是动作名称,第二个参数是Nonce字段的名称。

然后,在你的处理表单的PHP代码中,验证Nonce:

if ( isset( $_POST['my_nonce'] ) ) {
    if ( wp_verify_nonce( $_POST['my_nonce'], 'my_action' ) ) {
        // Nonce验证通过,处理表单数据
        $my_field = sanitize_text_field( $_POST['my_field'] );
        echo '你输入的内容是:' . $my_field;
    } else {
        // Nonce验证失败,拒绝处理
        echo '非法请求!';
    }
}

这段代码首先检查$_POST数组中是否存在名为my_nonce的字段。如果存在,就调用wp_verify_nonce()函数来验证Nonce。如果验证通过,就处理表单数据;如果验证失败,就拒绝处理。

注意事项:

  • $action参数必须和生成Nonce时使用的$action参数一致。
  • Nonce字段的名称必须和表单中的名称一致。
  • 在处理表单数据之前,一定要先验证Nonce。
  • wp_verify_nonce() 函数只能验证一次。如果同一个Nonce被多次使用,第二次验证将会失败。

安全性考量:

虽然Nonce可以有效地防止CSRF攻击,但是它并不是万能的。以下是一些需要注意的安全问题:

  • Nonce的泄露: 如果Nonce被泄露,攻击者就可以利用这个Nonce来发起CSRF攻击。因此,Nonce应该被保密,不要在URL中传递Nonce。
  • 时间窗口: Nonce的有效期是有限的(默认是12小时)。如果用户在Nonce过期后提交请求,验证将会失败。因此,应该尽量缩短Nonce的有效期。
  • 服务器时间同步: wp_nonce_tick() 函数依赖于服务器的时间。如果服务器的时间不准确,可能会导致Nonce验证失败。因此,应该确保服务器的时间是准确的。

高级技巧:自定义Nonce的有效期

如果你觉得12小时的Nonce有效期太长或太短,你可以使用nonce_life 过滤器来自定义Nonce的有效期。例如,将Nonce的有效期设置为1小时:

add_filter( 'nonce_life', 'my_custom_nonce_life' );
function my_custom_nonce_life() {
    return HOUR_IN_SECONDS; // 1小时
}

总结:

通过今天的讲解,相信大家对wp_verify_nonce()函数有了更深入的理解。它不仅仅是一个简单的验证函数,而是WordPress安全体系中一个重要的组成部分。掌握它的原理和使用方法,可以帮助你编写更安全、更可靠的WordPress代码。记住,安全无小事,多一份防范,少一份风险。

希望今天的分享对你有所帮助!下次有机会,咱们再聊聊WordPress的其他有趣的技术细节。

发表回复

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