核心函数:`wp_verify_password`的密码哈希算法与安全性,以及如何实现自定义密码策略?

WordPress 密码哈希与安全:深入 wp_verify_password 及自定义策略

大家好,今天我们要深入探讨 WordPress 密码安全的核心:wp_verify_password 函数,以及如何在此基础上构建更强大的自定义密码策略。 密码安全是网站安全的基础,理解 WordPress 的默认实现以及如何扩展它至关重要。

WordPress 密码哈希:wp_hash_passwordwp_check_password 的演变

在深入 wp_verify_password 之前,我们先回顾一下 WordPress 密码处理的历史。 早期,WordPress 使用 MD5 哈希,但这在安全性上存在严重缺陷,容易受到彩虹表攻击。

后来,WordPress 引入了 Portable PHP password hashing framework,提供了 wp_hash_passwordwp_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;
}

工作原理:

  1. 空密码检查: 首先,检查密码是否为空。 如果为空,则直接返回 false

  2. MD5 哈希兼容性: 检查哈希值的前四个字符是否为 $P$B,这表明哈希值仍然是 MD5。 如果是,则使用 hash_equals 函数进行安全比较。 如果匹配,则返回 true。 注意,这里的 hash_equals 函数用于防止定时攻击。

  3. 加载 PasswordHash 类: 如果哈希不是 MD5,则加载 PasswordHash 类(如果尚未加载)。 这是 Portable PHP password hashing framework 的核心类。

  4. 密码校验: 使用 $wp_hasher->CheckPassword() 方法来验证密码是否与哈希匹配。 CheckPassword() 方法内部使用 bcrypt 算法进行比较。

  5. 触发 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 钩子和过滤器来实现。

可以自定义的密码策略包括:

  • 密码长度要求: 强制密码至少包含特定数量的字符。
  • 复杂性要求: 要求密码包含大小写字母、数字和特殊字符。
  • 密码过期: 定期强制用户更改密码。
  • 密码重用限制: 防止用户重复使用以前的密码。
  • 黑名单密码: 禁止使用常见的、容易被破解的密码。

实现自定义密码策略的步骤:

  1. 创建插件或在主题的 functions.php 文件中编写代码。 建议使用插件,因为它更易于维护和更新。

  2. 使用 WordPress 钩子和过滤器来修改密码处理过程。 常用的钩子包括:

    • validate_password_reset: 在密码重置过程中验证新密码。
    • user_profile_update_errors: 在用户更新个人资料时验证密码。
    • check_password: 用于替换默认的密码验证逻辑(不推荐,除非你完全理解其影响)。
    • wp_authenticate_user: 在用户身份验证期间执行自定义检查。
  3. 编写自定义验证逻辑来强制执行密码策略。

  4. 向用户显示清晰的错误消息,指导他们创建符合策略的密码。

代码示例:强制密码长度和复杂性

<?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_headcustom_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 网站的密码安全性。 掌握密码安全策略,并持续关注新的安全威胁和技术,才能更好地保护我们的网站和用户数据。

发表回复

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