WordPress Nonce 机制深度解析:wp_verify_nonce()
函数源码大冒险
各位观众老爷们,大家好!欢迎来到今天的 WordPress 技术讲座。我是你们的老朋友,人称“代码界的段子手”——老码农。今天咱们要聊的,是 WordPress 安全机制中一个相当重要,但又常常被新手忽略的家伙:Nonce。更具体地说,我们要扒开 wp_verify_nonce()
函数的源码,看看它是如何像个老侦探一样,通过时间戳和用户 ID,来验证 Nonce 的有效性,揪出那些想浑水摸鱼的坏蛋。
准备好了吗?系好安全带,我们这就开始这场源码大冒险!
什么是 Nonce?为什么要用它?
在开始深入源码之前,我们先来回顾一下 Nonce 的基本概念。Nonce,是 "Number used ONCE" 的缩写,顾名思义,就是一个只能用一次的随机数。在 WordPress 中,Nonce 主要用于防止 CSRF (Cross-Site Request Forgery,跨站请求伪造) 攻击。
想象一下,如果没有 Nonce,一个坏蛋可以在你的网站上伪造一个请求,比如删除你的文章,然后通过各种手段诱骗你点击这个链接。一旦你点击了,你的 WordPress 就会执行这个恶意请求,因为你已经登录了,浏览器会自动带上你的身份信息。
Nonce 的作用,就像给每个敏感操作加上一个只有你知道的密码。坏蛋不知道这个密码,就无法伪造有效的请求。
wp_verify_nonce()
:Nonce 验证的核心
wp_verify_nonce()
函数是 WordPress 中验证 Nonce 是否有效的关键函数。它接受两个参数:
$nonce
: 要验证的 Nonce 值。$action
: 生成 Nonce 时使用的 Action 值,用于区分不同的操作。
这个函数会返回以下值:
1
: Nonce 有效,且在有效期内。2
: Nonce 有效,但已经过期(在有效期的一半之前)。false
: Nonce 无效。
现在,让我们打开 WordPress 的源码,找到 wp-includes/pluggable.php
文件,开始深入 wp_verify_nonce()
函数的内部。
wp_verify_nonce()
源码剖析
function wp_verify_nonce( $nonce, $action = -1 ) {
$nonce = (string) $nonce;
$action = (string) $action;
$nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );
$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 the nonce verification fails.
*
* @since 4.5.0
*
* @param string|int $nonce The nonce that was rejected.
* @param string|int $action The action hook the nonce was used for.
*/
do_action( 'wp_nonce_failed', $nonce, $action );
return false;
}
代码看起来不多,但信息量很大。让我们一行一行地解读:
-
参数类型转换:
$nonce = (string) $nonce; $action = (string) $action;
首先,将传入的
$nonce
和$action
强制转换为字符串类型,确保后续操作的类型一致性。这是一种良好的编程习惯,可以避免一些潜在的类型错误。 -
获取 Nonce 的有效期:
$nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );
这里通过
apply_filters()
函数,允许开发者通过nonce_life
过滤器来修改 Nonce 的有效期。默认情况下,Nonce 的有效期是DAY_IN_SECONDS
,也就是一天 (24 小时)。这给了开发者很大的灵活性,可以根据实际需求调整 Nonce 的有效期。 -
获取时间戳 "Tick":
$i = wp_nonce_tick();
wp_nonce_tick()
函数返回一个基于时间戳的整数,用于计算 Nonce 的哈希值。这个 "Tick" 的作用是,将 Nonce 的有效期分成多个时间段,使得 Nonce 在每个时间段内都是唯一的。我们稍后会详细分析
wp_nonce_tick()
函数。 -
计算预期的 Nonce 值 (当前时间段):
$expected = substr( wp_hash( $i . $action . get_current_user_id(), 'nonce' ), -12, 10 );
这行代码是关键。它做了以下几件事:
- 将当前时间段的 "Tick" (
$i
)、Action ($action
) 和当前用户 ID (get_current_user_id()
) 拼接成一个字符串。 - 使用
wp_hash()
函数对这个字符串进行哈希运算,生成一个哈希值。wp_hash()
默认使用md5
算法,但可以通过wp_hash_algorithm
filter 修改。 - 截取哈希值的最后 10 位字符,作为预期的 Nonce 值。之所以截取 10 位,是为了缩短 Nonce 的长度,提高安全性,同时避免过长的 Nonce 影响性能。
- 使用
hash_equals()
函数进行安全比较,防止时序攻击。
- 将当前时间段的 "Tick" (
-
比较 Nonce 值:
if ( hash_equals( $expected, $nonce ) ) { return 1; }
使用
hash_equals()
函数,安全地比较计算出的预期 Nonce 值和传入的$nonce
值。如果两者相等,说明 Nonce 有效,返回1
。 -
计算预期的 Nonce 值 (上一个时间段):
$expected = substr( wp_hash( ( $i - 1 ) . $action . get_current_user_id(), 'nonce' ), -12, 10 ); if ( hash_equals( $expected, $nonce ) ) { return 2; }
如果当前的 Nonce 值无效,则尝试使用上一个时间段的 "Tick" 值 (
$i - 1
) 重新计算预期的 Nonce 值,并进行比较。如果两者相等,说明 Nonce 虽然有效,但已经过期 (在有效期的一半之前),返回2
。这样做是为了允许 Nonce 在一定时间内仍然有效,避免由于服务器时钟不同步或用户操作延迟导致 Nonce 验证失败。
-
Nonce 验证失败钩子:
do_action( 'wp_nonce_failed', $nonce, $action );
如果 Nonce 验证失败,则触发
wp_nonce_failed
钩子,允许开发者执行一些自定义的操作,例如记录日志或显示错误信息。 -
返回 False:
return false;
如果 Nonce 验证失败,且不在过期时间内,则返回
false
,表示 Nonce 无效。
wp_nonce_tick()
函数:时间戳的秘密
现在,让我们深入 wp_nonce_tick()
函数,看看它是如何计算时间戳 "Tick" 的。
function wp_nonce_tick() {
/**
* Filters the lifespan of the nonce.
*
* @since 4.5.0
*
* @param int $lifespan The amount of time to use as the lifespan of a nonce.
* Default is DAY_IN_SECONDS.
*/
$nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );
return ceil( time() / ( $nonce_life / 2 ) );
}
这段代码也很简单:
-
获取 Nonce 的有效期:
$nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );
同样,通过
apply_filters()
函数获取 Nonce 的有效期。 -
计算时间戳 "Tick":
return ceil( time() / ( $nonce_life / 2 ) );
这行代码的核心在于计算时间戳 "Tick" 的逻辑。它做了以下几件事:
- 将当前时间戳 (
time()
) 除以 Nonce 有效期的一半 ($nonce_life / 2
)。 - 使用
ceil()
函数向上取整,得到一个整数值,作为时间戳 "Tick"。
这样做的目的是,将 Nonce 的有效期分成两个时间段。例如,如果 Nonce 的有效期是 24 小时,那么每个时间段就是 12 小时。在每个时间段内,
wp_nonce_tick()
函数返回的值都是相同的,从而保证 Nonce 在每个时间段内都是唯一的。 - 将当前时间戳 (
为什么 Nonce 要基于用户 ID?
你可能注意到,在计算 Nonce 的哈希值时,wp_verify_nonce()
函数使用了 get_current_user_id()
函数来获取当前用户 ID。为什么要这样做呢?
原因很简单:为了防止用户之间的 Nonce 冲突。
如果没有用户 ID,那么所有用户的 Nonce 值都会相同,这会导致一个用户生成的 Nonce 可以被其他用户使用,从而破坏了 Nonce 的唯一性。
通过将用户 ID 包含在 Nonce 的哈希值中,可以保证每个用户的 Nonce 值都是唯一的,从而提高安全性。
总结:wp_verify_nonce()
的工作原理
现在,让我们总结一下 wp_verify_nonce()
函数的工作原理:
- 获取 Nonce 的有效期和时间戳 "Tick"。
- 将时间戳 "Tick"、Action 和用户 ID 拼接成一个字符串。
- 对这个字符串进行哈希运算,并截取哈希值的最后 10 位字符,作为预期的 Nonce 值。
- 比较预期的 Nonce 值和传入的 Nonce 值。
- 如果两者相等,则返回
1
,表示 Nonce 有效。 - 如果 Nonce 值不在当前时间段,则尝试使用上一个时间段的时间戳 "Tick" 重新计算预期的 Nonce 值,并进行比较。
- 如果两者相等,则返回
2
,表示 Nonce 有效,但已经过期。 - 如果 Nonce 验证失败,则触发
wp_nonce_failed
钩子,并返回false
,表示 Nonce 无效。
用一张表格来总结一下:
步骤 | 描述 |
---|---|
1 | 获取 Nonce 有效期 (nonce_life ),默认 24 小时 (DAY_IN_SECONDS)。 |
2 | 获取时间戳 "Tick" ($i ),通过 wp_nonce_tick() 计算,将有效期分成两个时间段。 |
3 | 拼接字符串:$i . $action . get_current_user_id() 。 |
4 | 使用 wp_hash() 对字符串进行哈希运算。 |
5 | 截取哈希值的最后 10 位字符,作为预期 Nonce 值。 |
6 | 使用 hash_equals() 安全地比较预期 Nonce 值和传入的 Nonce 值。 |
7 | 如果相等,返回 1 (有效)。 |
8 | 尝试使用上一个时间段的 Tick ($i - 1 ) 重新计算,如果相等,返回 2 (过期)。 |
9 | 如果验证失败,触发 wp_nonce_failed 钩子,返回 false (无效)。 |
如何正确使用 wp_verify_nonce()
?
现在我们已经深入了解了 wp_verify_nonce()
函数的源码,那么如何正确地使用它呢?
以下是一些建议:
- 在生成 Nonce 时,使用唯一的 Action 值。 Action 值用于区分不同的操作,可以防止 Nonce 被用于错误的操作。
- 在验证 Nonce 时,使用与生成 Nonce 时相同的 Action 值。 否则,Nonce 验证将会失败。
- 不要将 Nonce 暴露给客户端。 Nonce 应该保存在服务器端,例如 Session 或数据库中。
- 定期更新 Nonce。 Nonce 的有效期是有限的,过期后就不能再使用。
- 使用
wp_nonce_field()
函数生成隐藏的 Nonce 字段。 这个函数会自动生成包含 Nonce 值的隐藏表单字段,方便在表单提交时传递 Nonce 值。
总结
通过这次源码大冒险,我们深入了解了 WordPress wp_verify_nonce()
函数的工作原理,以及 Nonce 机制在 WordPress 安全中的重要作用。希望这次讲座能够帮助你更好地理解 WordPress 安全机制,写出更安全、更可靠的 WordPress 代码。
记住,代码世界充满了乐趣,只要你敢于探索,就能发现更多的秘密!下次再见!