WordPress 密码哈希与安全:深入 wp_verify_password
及自定义策略
大家好,今天我们要深入探讨 WordPress 密码安全的核心:wp_verify_password
函数,以及如何在此基础上构建更强大的自定义密码策略。 密码安全是网站安全的基础,理解 WordPress 的默认实现以及如何扩展它至关重要。
WordPress 密码哈希:wp_hash_password
与 wp_check_password
的演变
在深入 wp_verify_password
之前,我们先回顾一下 WordPress 密码处理的历史。 早期,WordPress 使用 MD5 哈希,但这在安全性上存在严重缺陷,容易受到彩虹表攻击。
后来,WordPress 引入了 Portable PHP password hashing framework,提供了 wp_hash_password
和 wp_check_password
函数。 这些函数使用 bcrypt 算法,一种自适应的密钥派生函数,能有效抵抗暴力破解和彩虹表攻击。
wp_hash_password( $password )
: 对给定的密码进行哈希处理,返回哈希后的字符串。 使用 bcrypt 算法并添加随机 salt。wp_check_password( $password, $hash, $user_id = '' )
: 验证给定的密码是否与存储的哈希值匹配。 如果密码匹配,则返回true
,否则返回false
。
bcrypt 的优势:
- Salt: 每个密码都使用唯一的随机 salt,防止相同的密码生成相同的哈希值。
- 自适应性: bcrypt 的计算成本可以调整,随着计算能力的提升,可以增加迭代次数来保持安全性。
- 成熟度: bcrypt 是一种广泛使用和经过充分测试的哈希算法。
wp_verify_password
: wp_check_password
的增强版
wp_verify_password
函数是对 wp_check_password
的改进和增强,它不仅检查密码的有效性,还处理密码哈希的升级。
函数签名:
function wp_verify_password( $password, $hash ) {
global $wp_hasher;
if ( empty( $password ) ) {
return false;
}
// If the hash is still using MD5, re-hash it, otherwise, check if the password matches the hash.
if ( substr( $hash, 0, 4 ) == '$P$B' ) {
$check = hash_equals( $hash, md5( $password ) );
if ( $check ) {
return true;
}
}
if ( empty( $wp_hasher ) ) {
require_once ABSPATH . WPINC . '/class-phpass.php';
$wp_hasher = new PasswordHash( 8, true );
}
$check = $wp_hasher->CheckPassword( $password, $hash );
/**
* Fires when a user's password is verified.
*
* @since 4.4.0
*
* @param WP_User|false $user WP_User object if the user was found, false if not.
* @param string $password The user's password in plain text.
* @param string $hash The user's password hash.
*/
do_action( 'wp_verify_password', get_user_by( 'login', wp_get_current_user()->user_login ), $password, $hash );
return $check;
}
工作原理:
-
空密码检查: 首先,检查密码是否为空。 如果为空,则直接返回
false
。 -
MD5 哈希兼容性: 检查哈希值的前四个字符是否为
$P$B
,这表明哈希值仍然是 MD5。 如果是,则使用hash_equals
函数进行安全比较。 如果匹配,则返回true
。 注意,这里的hash_equals
函数用于防止定时攻击。 -
加载 PasswordHash 类: 如果哈希不是 MD5,则加载
PasswordHash
类(如果尚未加载)。 这是 Portable PHP password hashing framework 的核心类。 -
密码校验: 使用
$wp_hasher->CheckPassword()
方法来验证密码是否与哈希匹配。CheckPassword()
方法内部使用 bcrypt 算法进行比较。 -
触发
wp_verify_password
动作: 触发wp_verify_password
动作,允许插件和主题在密码验证后执行自定义操作。 传递的参数包括用户对象(如果存在)、密码和哈希值。
为什么要用 hash_equals
比较 MD5 哈希?
hash_equals
函数用于防止定时攻击。 定时攻击通过测量比较两个字符串所需的时间来推断它们是否相等。 传统的字符串比较函数在发现第一个不匹配的字符时会立即停止,这会泄露有关字符串结构的敏感信息。 hash_equals
函数始终比较整个字符串,无论何时发现不匹配的字符,从而消除了定时攻击的可能性。
自动密码哈希升级:
虽然 wp_verify_password
负责验证密码,但实际的密码哈希升级发生在用户登录或更新密码时。 WordPress 会检查存储的密码哈希是否使用了过时的算法(例如 MD5 或较弱的 bcrypt 配置)。 如果发现过时的哈希,WordPress 会自动使用最新的 bcrypt 配置重新哈希密码。 这个过程对用户来说是透明的,但对于保持密码安全至关重要。
PasswordHash 类的配置:
PasswordHash
类在初始化时接受两个参数:
$iteration_count_log2
: bcrypt 迭代次数的以 2 为底的对数。 较高的值会增加计算成本,提高安全性,但也会增加服务器的负载。 默认值为 8。$portable_hashes
: 一个布尔值,指示是否生成与较旧的 PHP 版本兼容的哈希。 默认为true
。
代码示例:手动哈希和验证密码
// 哈希密码
$password = 'mysecretpassword';
$hashed_password = wp_hash_password( $password );
echo "Hashed password: " . $hashed_password . "n";
// 验证密码
$password_to_check = 'mysecretpassword';
$is_valid = wp_verify_password( $password_to_check, $hashed_password );
if ( $is_valid ) {
echo "Password is valid.n";
} else {
echo "Password is invalid.n";
}
自定义密码策略:超越默认设置
WordPress 默认的密码策略相对简单。 要实现更强大的密码安全,我们需要自定义密码策略。 这可以通过使用 WordPress 钩子和过滤器来实现。
可以自定义的密码策略包括:
- 密码长度要求: 强制密码至少包含特定数量的字符。
- 复杂性要求: 要求密码包含大小写字母、数字和特殊字符。
- 密码过期: 定期强制用户更改密码。
- 密码重用限制: 防止用户重复使用以前的密码。
- 黑名单密码: 禁止使用常见的、容易被破解的密码。
实现自定义密码策略的步骤:
-
创建插件或在主题的
functions.php
文件中编写代码。 建议使用插件,因为它更易于维护和更新。 -
使用 WordPress 钩子和过滤器来修改密码处理过程。 常用的钩子包括:
validate_password_reset
: 在密码重置过程中验证新密码。user_profile_update_errors
: 在用户更新个人资料时验证密码。check_password
: 用于替换默认的密码验证逻辑(不推荐,除非你完全理解其影响)。wp_authenticate_user
: 在用户身份验证期间执行自定义检查。
-
编写自定义验证逻辑来强制执行密码策略。
-
向用户显示清晰的错误消息,指导他们创建符合策略的密码。
代码示例:强制密码长度和复杂性
<?php
/**
* Plugin Name: Custom Password Policy
* Description: Enforces a custom password policy for WordPress users.
* Version: 1.0.0
* Author: Your Name
*/
// Minimum password length
define( 'MIN_PASSWORD_LENGTH', 8 );
// Function to check password complexity
function check_password_complexity( $password ) {
$errors = array();
if ( strlen( $password ) < MIN_PASSWORD_LENGTH ) {
$errors[] = sprintf(
__( 'Password must be at least %d characters long.', 'custom-password-policy' ),
MIN_PASSWORD_LENGTH
);
}
if ( ! preg_match( '/[A-Z]/', $password ) ) {
$errors[] = __( 'Password must contain at least one uppercase letter.', 'custom-password-policy' );
}
if ( ! preg_match( '/[a-z]/', $password ) ) {
$errors[] = __( 'Password must contain at least one lowercase letter.', 'custom-password-policy' );
}
if ( ! preg_match( '/[0-9]/', $password ) ) {
$errors[] = __( 'Password must contain at least one number.', 'custom-password-policy' );
}
if ( ! preg_match( '/[^a-zA-Z0-9s]/', $password ) ) {
$errors[] = __( 'Password must contain at least one special character.', 'custom-password-policy' );
}
return $errors;
}
// Validate password during password reset
add_filter( 'validate_password_reset', 'custom_validate_password_reset', 10, 2 );
function custom_validate_password_reset( $errors, $new_password ) {
$password_errors = check_password_complexity( $new_password );
if ( ! empty( $password_errors ) ) {
foreach ( $password_errors as $error ) {
$errors->add( 'password_error', $error );
}
}
return $errors;
}
// Validate password during user profile update
add_action( 'user_profile_update_errors', 'custom_user_profile_update_errors', 10, 3 );
function custom_user_profile_update_errors( $errors, $update, $user ) {
if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) {
$password = $_POST['pass1'];
$password_errors = check_password_complexity( $password );
if ( ! empty( $password_errors ) ) {
foreach ( $password_errors as $error ) {
$errors->add( 'password_error', $error );
}
}
}
}
// Add custom error messages
add_action( 'login_head', 'custom_login_head' );
function custom_login_head() {
if ( isset( $_GET['password_error'] ) ) {
$error_message = urldecode( $_GET['password_error'] );
echo '<style type="text/css">
#login_error, .message {
border-left-color: #dc3232;
}
</style>';
echo '<div id="login_error" class="error"><p>' . esc_html( $error_message ) . '</p></div>';
}
}
// Add custom error messages to user profile page
add_action( 'admin_notices', 'custom_admin_notices' );
function custom_admin_notices() {
global $pagenow;
if ( $pagenow == 'profile.php' || $pagenow == 'user-edit.php' ) {
if ( isset( $_GET['password_error'] ) ) {
$error_message = urldecode( $_GET['password_error'] );
echo '<div class="notice notice-error is-dismissible">
<p>' . esc_html( $error_message ) . '</p>
<button type="button" class="notice-dismiss">
<span class="screen-reader-text">' . __( 'Dismiss this notice.', 'custom-password-policy' ) . '</span>
</button>
</div>';
}
}
}
代码解释:
MIN_PASSWORD_LENGTH
: 定义了密码的最小长度。check_password_complexity
: 检查密码是否满足复杂性要求。 使用了正则表达式来验证密码是否包含大小写字母、数字和特殊字符。custom_validate_password_reset
: 在密码重置过程中使用validate_password_reset
过滤器验证新密码。custom_user_profile_update_errors
: 在用户更新个人资料时使用user_profile_update_errors
动作验证密码。custom_login_head
和custom_admin_notices
: 向用户显示清晰的错误消息。
密码过期策略:
要实现密码过期策略,可以使用 WordPress 计划任务 (WP-Cron) 定期检查用户的密码修改日期。 如果密码超过了设定的过期时间,则强制用户重置密码。
代码示例:实现密码过期策略
<?php
/**
* Plugin Name: Password Expiration Policy
* Description: Implements a password expiration policy for WordPress users.
* Version: 1.0.0
* Author: Your Name
*/
// Password expiration time (in days)
define( 'PASSWORD_EXPIRATION_DAYS', 90 );
// Function to check if password has expired
function check_password_expiration( $user_id ) {
$last_changed = get_user_meta( $user_id, 'last_password_change', true );
if ( empty( $last_changed ) ) {
return false; // Password never changed, consider it expired
}
$expiration_date = strtotime( '+' . PASSWORD_EXPIRATION_DAYS . ' days', $last_changed );
$now = time();
return $now > $expiration_date;
}
// Schedule a daily event to check password expiration
add_action( 'wp', 'schedule_password_expiration_check' );
function schedule_password_expiration_check() {
if ( ! wp_next_scheduled( 'password_expiration_check_event' ) ) {
wp_schedule_event( time(), 'daily', 'password_expiration_check_event' );
}
}
// Action to check password expiration for all users
add_action( 'password_expiration_check_event', 'password_expiration_check' );
function password_expiration_check() {
$users = get_users();
foreach ( $users as $user ) {
if ( check_password_expiration( $user->ID ) ) {
// Force user to reset password
update_user_meta( $user->ID, 'password_expired', true );
// Send email notification to user
send_password_expiration_notification( $user->ID );
}
}
}
// Function to send password expiration notification
function send_password_expiration_notification( $user_id ) {
$user = get_userdata( $user_id );
$user_login = stripslashes( $user->user_login );
$user_email = stripslashes( $user->user_email );
$message = __( 'Your password has expired. Please reset your password.', 'password-expiration-policy' ) . "rnrn";
$message .= wp_login_url() . "?action=lostpassword";
wp_mail( $user_email, __( 'Password Expired', 'password-expiration-policy' ), $message );
}
// Redirect user to password reset page if password has expired
add_action( 'template_redirect', 'redirect_expired_password' );
function redirect_expired_password() {
if ( is_user_logged_in() ) {
$user_id = get_current_user_id();
$password_expired = get_user_meta( $user_id, 'password_expired', true );
if ( $password_expired ) {
// Redirect to password reset page
wp_redirect( wp_login_url() . "?action=lostpassword&password_expired=1" );
exit;
}
}
}
// Update last password change timestamp when password is changed
add_action( 'profile_update', 'update_last_password_change', 10, 2 );
function update_last_password_change( $user_id, $old_user_data ) {
$new_user_data = get_userdata( $user_id );
if ( $new_user_data->user_pass != $old_user_data->user_pass ) {
update_user_meta( $user_id, 'last_password_change', time() );
delete_user_meta( $user_id, 'password_expired' ); // Reset password_expired flag
}
}
// Display a message on the password reset page if password expired
add_action( 'login_form_lostpassword', 'display_password_expired_message' );
function display_password_expired_message() {
if ( isset( $_GET['password_expired'] ) && $_GET['password_expired'] == 1 ) {
echo '<p class="message">' . __( 'Your password has expired. Please create a new password.', 'password-expiration-policy' ) . '</p>';
}
}
代码解释:
PASSWORD_EXPIRATION_DAYS
: 定义了密码过期时间(以天为单位)。check_password_expiration
: 检查密码是否已过期。schedule_password_expiration_check
: 安排一个每日事件来检查密码过期情况。password_expiration_check
: 检查所有用户的密码是否已过期,并发送电子邮件通知。send_password_expiration_notification
: 发送密码过期通知电子邮件。redirect_expired_password
: 如果密码已过期,则将用户重定向到密码重置页面。update_last_password_change
: 在密码更改时更新last_password_change
用户元数据。display_password_expired_message
: 在密码重置页面上显示密码过期消息。
表: WordPress 密码安全相关的钩子和过滤器
钩子/过滤器 | 描述 |
---|---|
validate_password_reset |
在密码重置过程中验证新密码。 可以用于强制执行密码策略。 |
user_profile_update_errors |
在用户更新个人资料时验证密码。 可以用于强制执行密码策略。 |
check_password |
用于替换默认的密码验证逻辑。 使用时要非常小心,因为它会影响所有密码验证。 |
wp_authenticate_user |
在用户身份验证期间执行自定义检查。 可以用于实现双因素身份验证或其他安全措施。 |
wp_verify_password |
在密码验证后触发。 可以用于记录密码验证事件或执行其他自定义操作。 |
auth_cookie_expiration |
修改身份验证 cookie 的过期时间。 可以用于控制用户会话的持续时间。 |
password_hint |
修改密码提示文本。 可以用于指导用户创建符合密码策略的密码。 |
lostpassword_url |
修改“忘记密码”链接的 URL。 可以用于自定义密码重置过程。 |
安全最佳实践
- 始终使用 HTTPS: 确保您的网站使用 HTTPS 来加密所有通信,包括密码传输。
- 保持 WordPress 和所有插件更新: 及时更新 WordPress 和所有插件,以修复安全漏洞。
- 使用强密码: 鼓励用户使用强密码,并实施自定义密码策略来强制执行密码复杂性。
- 限制登录尝试: 使用插件或自定义代码来限制登录尝试次数,以防止暴力破解攻击。
- 实施双因素身份验证 (2FA): 考虑实施双因素身份验证,以增加额外的安全层。
- 定期备份数据库: 定期备份数据库,以防止数据丢失。
- 监控安全日志: 监控安全日志,以检测可疑活动。
结论
wp_verify_password
函数是 WordPress 密码安全的核心,它使用了 bcrypt 算法并提供了自动密码哈希升级功能。 通过使用 WordPress 钩子和过滤器,我们可以自定义密码策略,以满足特定的安全需求。 遵循安全最佳实践,可以进一步提高 WordPress 网站的密码安全性。 掌握密码安全策略,并持续关注新的安全威胁和技术,才能更好地保护我们的网站和用户数据。