剖析 WordPress `wp_signon()` 函数的源码:登录认证的核心流程,包括密码验证和 Cookie 设置。

大家好!今天咱们来聊聊 WordPress 登录认证的灵魂人物—— wp_signon() 函数。这哥们儿,可以说是 WordPress 安全防线的第一道关卡,负责验证用户身份,发放“通行证”(Cookie),让用户顺利进入后台。咱们要做的,就是把他扒个精光,看看他到底是怎么干活的。

(一) 前戏:调用与参数准备

首先,wp_signon() 函数并不是直接被用户调用的,通常是由 WordPress 登录表单提交后,经过一系列处理后才轮到它出场。它的基本用法是这样的:

$credentials = array();
$credentials['user_login'] = $_POST['log']; // 用户名
$credentials['user_password'] = $_POST['pwd']; // 密码
$credentials['remember'] = isset($_POST['rememberme']); // 是否记住登录状态

$user = wp_signon( $credentials, false ); // 调用 wp_signon()

这里,$credentials 是一个非常关键的数组,它包含了登录所需的信息:

键名 描述
user_login 用户名或邮箱。WordPress 允许使用用户名或邮箱登录。
user_password 用户密码,明文的!后面会进行加密验证。
remember 一个布尔值,指示是否记住登录状态。如果为 true,WordPress 会设置一个较长时间的 Cookie,让用户下次访问时自动登录。
force_auth_recheck (可选) 一个布尔值,默认为 false。如果为 true,会强制重新检查用户权限,即使缓存中已经存在用户信息。 这个参数在一些权限相关的插件中可能会用到,一般情况用不到。

第二个参数 $secure_cookie,指定 Cookie 是否应该通过 HTTPS 发送。如果你的网站启用了 HTTPS,通常设置为 true。如果未指定,WordPress 会自动检测当前连接是否安全。

返回值:

wp_signon() 函数会返回一个 WP_User 对象,如果登录成功。如果登录失败,它会返回一个 WP_Error 对象,里面包含了错误信息。

(二) 源码剖析:一步一步揭开神秘面纱

好了,现在让我们深入 wp-includes/user.php 文件,开始解剖 wp_signon() 函数的源码。为了方便理解,我将源码拆解成几个关键步骤,并加上详细的注释。

function wp_signon( $credentials = array(), $secure_cookie = '' ) {
  // 1. 初始化
  $defaults = array( 'user_login' => '', 'user_password' => '', 'remember' => false );
  $credentials = wp_parse_args( $credentials, $defaults );

  /**
   * Fires before the user is authenticated.
   *
   * @since 2.9.0
   *
   * @param array $credentials An array of user login credentials.
   */
  do_action( 'wp_authenticate', $credentials['user_login'], $credentials['user_password'] );

  // 2. 过滤用户名
  $credentials['user_login'] = sanitize_user( $credentials['user_login'] );

  // 3. 插件介入点:允许插件进行自定义认证
  $user = apply_filters( 'authenticate', null, $credentials['user_login'], $credentials['user_password'] );

  // 如果插件已经认证成功,直接返回
  if ( $user instanceof WP_User ) {
    return $user;
  }

  if ( $user instanceof WP_Error ) {
    return $user;
  }

  // 4. 根据用户名获取用户信息
  if ( ! empty( $credentials['user_login'] ) ) {
    // 尝试通过用户名获取用户
    $user = get_user_by( 'login', $credentials['user_login'] );
    if ( ! $user ) {
      // 如果没找到,尝试通过邮箱获取用户
      $user = get_user_by( 'email', $credentials['user_login'] );
    }
  }

  // 5. 用户不存在的情况
  if ( ! $user ) {
    do_action( 'wp_login_failed', $credentials['user_login'] ); // 触发登录失败的钩子
    $errors = new WP_Error();
    $errors->add( 'invalid_username', __( 'Invalid username or email address.' ) );
    return $errors;
  }

  // 6. 检查用户是否被禁止登录
  if ( ! $user->exists() ) {
    do_action( 'wp_login_failed', $credentials['user_login'] );
    $errors = new WP_Error();
    $errors->add( 'invalid_username', __( 'Invalid username or email address.' ) );
    return $errors;
  }

  // 7. 检查用户是否被禁用
  if ( ! $user->has_cap( 'read' ) ) {
    do_action( 'wp_login_failed', $credentials['user_login'] );
    $errors = new WP_Error();
    $errors->add( 'incorrect_password', __( 'The password you entered for the username <strong>%1$s</strong> is incorrect.', 'wordpress' ), $credentials['user_login'] );
    return $errors;
  }

  // 8. 密码验证
  $is_valid_password = wp_check_password( $credentials['user_password'], $user->user_pass, $user->ID );

  if ( ! $is_valid_password ) {
    do_action( 'wp_login_failed', $credentials['user_login'] );
    $errors = new WP_Error();
    $errors->add( 'incorrect_password', __( 'The password you entered for the username <strong>%1$s</strong> is incorrect.', 'wordpress' ), $credentials['user_login'] );
    return $errors;
  }

  // 9. 验证成功,执行登录操作
  wp_set_auth_cookie( $user->ID, $credentials['remember'], $secure_cookie );
  do_action( 'wp_login', $user->user_login, $user );

  return $user;
}

接下来,我们详细分析每个步骤:

1. 初始化:

$defaults = array( 'user_login' => '', 'user_password' => '', 'remember' => false );
$credentials = wp_parse_args( $credentials, $defaults );

这段代码使用 wp_parse_args() 函数将传入的 $credentials 数组与默认值合并。这意味着,即使你只传入了用户名和密码,wp_signon() 也会自动补全 remember 字段为 false

2. 触发 wp_authenticate 动作:

do_action( 'wp_authenticate', $credentials['user_login'], $credentials['user_password'] );

这是一个钩子(Hook),允许插件在用户认证之前执行一些操作,例如记录登录尝试、执行额外的安全检查等。

3. 插件介入:authenticate 过滤器

$user = apply_filters( 'authenticate', null, $credentials['user_login'], $credentials['user_password'] );

// 如果插件已经认证成功,直接返回
if ( $user instanceof WP_User ) {
  return $user;
}

if ( $user instanceof WP_Error ) {
  return $user;
}

authenticate 是一个过滤器(Filter),允许插件完全接管用户认证过程。插件可以根据自己的逻辑验证用户身份,并返回一个 WP_User 对象(如果认证成功)或一个 WP_Error 对象(如果认证失败)。如果插件返回了 WP_User 对象,wp_signon() 函数会直接返回,不再执行后续的认证步骤。这为开发者提供了极大的灵活性,可以实现各种自定义的认证方式,例如基于 OAuth、LDAP 或其他外部服务的认证。

4. 根据用户名或邮箱获取用户信息:

if ( ! empty( $credentials['user_login'] ) ) {
  // 尝试通过用户名获取用户
  $user = get_user_by( 'login', $credentials['user_login'] );
  if ( ! $user ) {
    // 如果没找到,尝试通过邮箱获取用户
    $user = get_user_by( 'email', $credentials['user_login'] );
  }
}

这段代码首先检查用户名是否为空。如果不为空,它会先尝试使用 get_user_by( 'login', $credentials['user_login'] ) 函数根据用户名获取用户信息。如果找不到用户,它会继续尝试使用 get_user_by( 'email', $credentials['user_login'] ) 函数根据邮箱获取用户信息。WordPress 允许用户使用用户名或邮箱登录,这提高了用户体验。

5. 用户不存在的情况:

if ( ! $user ) {
  do_action( 'wp_login_failed', $credentials['user_login'] ); // 触发登录失败的钩子
  $errors = new WP_Error();
  $errors->add( 'invalid_username', __( 'Invalid username or email address.' ) );
  return $errors;
}

如果根据用户名和邮箱都找不到用户,说明用户不存在。这时,wp_signon() 会触发 wp_login_failed 动作,并返回一个包含错误信息的 WP_Error 对象。wp_login_failed 动作可以被插件用来记录登录失败的尝试,或者执行其他的安全措施。

6. 检查用户是否存在 ($user->exists()):

if ( ! $user->exists() ) {
  do_action( 'wp_login_failed', $credentials['user_login'] );
  $errors = new WP_Error();
  $errors->add( 'invalid_username', __( 'Invalid username or email address.' ) );
  return $errors;
}

$user->exists() 方法检查用户对象是否有效。 即使$user变量不为null,也可能因为某些原因(例如用户被删除但对象未被清理)导致用户对象无效。 这个检查确保后续操作基于一个真实存在的用户。

7. 检查用户是否被禁用:

if ( ! $user->has_cap( 'read' ) ) {
  do_action( 'wp_login_failed', $credentials['user_login'] );
  $errors = new WP_Error();
  $errors->add( 'incorrect_password', __( 'The password you entered for the username <strong>%1$s</strong> is incorrect.', 'wordpress' ), $credentials['user_login'] );
  return $errors;
}

$user->has_cap( 'read' ) 方法检查用户是否具有 read 权限。在 WordPress 中,read 权限是访问网站的基本权限。如果用户没有 read 权限,说明该用户可能已被禁用。注意,这里返回的错误信息是“密码错误”,这是一种安全措施,避免暴露用户是否存在的信息。

8. 密码验证:

$is_valid_password = wp_check_password( $credentials['user_password'], $user->user_pass, $user->ID );

if ( ! $is_valid_password ) {
  do_action( 'wp_login_failed', $credentials['user_login'] );
  $errors = new WP_Error();
  $errors->add( 'incorrect_password', __( 'The password you entered for the username <strong>%1$s</strong> is incorrect.', 'wordpress' ), $credentials['user_login'] );
  return $errors;
}

这是整个认证过程中最关键的一步。wp_check_password() 函数负责验证用户输入的密码是否与数据库中存储的密码哈希匹配。

我们来看一下 wp_check_password() 函数的内部实现(简化版):

function wp_check_password( $password, $hash, $user_id = '' ) {
  // 1. 检查是否需要重新哈希密码
  if ( password_needs_rehash( $hash, PASSWORD_DEFAULT ) ) {
    // 2. 如果需要,重新哈希密码
    $new_hash = password_hash( $password, PASSWORD_DEFAULT );

    // 3. 更新数据库中的密码哈希
    wp_set_password( $password, $user_id );

    return password_verify( $password, $new_hash );
  } else {
    // 4. 否则,直接验证密码
    return password_verify( $password, $hash );
  }
}

wp_check_password() 函数首先检查密码哈希是否需要更新。如果需要,它会使用 password_hash() 函数生成一个新的哈希,并使用 wp_set_password() 函数更新数据库中的密码哈希。然后,它使用 password_verify() 函数验证用户输入的密码与新的哈希是否匹配。如果不需要更新哈希,它会直接使用 password_verify() 函数验证密码。

password_verify() 函数是 PHP 内置的函数,它使用 bcrypt 算法安全地比较密码和哈希。bcrypt 算法是一种自适应的哈希算法,可以根据计算能力自动调整计算复杂度,防止暴力破解。

回到 wp_signon() 函数,如果密码验证失败,它会触发 wp_login_failed 动作,并返回一个包含错误信息的 WP_Error 对象。

9. 验证成功,设置 Cookie 并触发 wp_login 动作:

wp_set_auth_cookie( $user->ID, $credentials['remember'], $secure_cookie );
do_action( 'wp_login', $user->user_login, $user );

return $user;

如果密码验证成功,说明用户身份已验证。这时,wp_signon() 函数会调用 wp_set_auth_cookie() 函数设置认证 Cookie,并将用户重定向到后台。

我们来看一下 wp_set_auth_cookie() 函数的实现:

function wp_set_auth_cookie( $user_id, $remember = false, $secure = '' ) {
  if ( '' === $secure ) {
    $secure = is_ssl();
  }

  $expiration = time() + apply_filters( 'auth_cookie_expiration', ( $remember ? 14 * DAY_IN_SECONDS : 2 * DAY_IN_SECONDS ), $user_id, $remember );

  $cookie = wp_generate_auth_cookie( $user_id, $expiration, 'auth', $secure );

  setcookie( AUTH_COOKIE, $cookie, $expiration, COOKIEPATH, COOKIE_DOMAIN, $secure, true );

  if ( $remember ) {
    wp_set_cookie( 'wp-saving-post', did_action( 'wp_saving_post' ) ? 'yes' : 'no', time() + YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, $secure, true );
  }

  /**
   * Fires immediately after a user is authenticated.
   *
   * @since 2.5.0
   *
   * @param int   $user_id  User ID.
   * @param bool  $remember Whether to remember the user. Default false.
   * @param mixed $secure   Whether the auth cookie should be secure.
   */
  do_action( 'set_auth_cookie', $cookie, $user_id, $remember, $secure );
}

wp_set_auth_cookie() 函数首先确定 Cookie 是否应该通过 HTTPS 发送。然后,它根据 $remember 参数设置 Cookie 的过期时间。如果 $remembertrue,Cookie 的过期时间为 14 天,否则为 2 天。接下来,它调用 wp_generate_auth_cookie() 函数生成认证 Cookie 的值,并使用 setcookie() 函数设置 Cookie。

最后,wp_signon() 函数触发 wp_login 动作,并返回 $user 对象。wp_login 动作可以被插件用来执行一些登录后的操作,例如记录登录时间、更新用户会话等。

(三) 安全考量:wp_signon() 的安全防护

wp_signon() 函数虽然强大,但也有一些安全隐患需要注意:

  • 密码存储: WordPress 使用 bcrypt 算法对密码进行哈希存储,这是一种相对安全的做法。但是,如果你的服务器存在安全漏洞,攻击者仍然可能获取到密码哈希,并通过暴力破解或其他手段破解密码。因此,你需要确保你的服务器安全,并定期更新 WordPress 和插件。
  • Cookie 安全: WordPress 使用 Cookie 来跟踪用户的登录状态。如果 Cookie 被盗,攻击者就可以冒充用户登录网站。为了防止 Cookie 被盗,你需要启用 HTTPS,并设置 Cookie 的 secure 标志为 true,确保 Cookie 只能通过 HTTPS 连接发送。
  • 防止暴力破解: 攻击者可以通过尝试不同的用户名和密码来破解用户账户。为了防止暴力破解,你可以使用插件来限制登录尝试的次数,并实施账户锁定策略。
  • 双因素认证: 为了提高安全性,你可以启用双因素认证。双因素认证需要在登录时输入密码和一次性验证码,即使密码被盗,攻击者也无法登录网站。

(四) 总结:wp_signon() 是 WordPress 登录认证的核心

wp_signon() 函数是 WordPress 登录认证的核心。它负责验证用户身份,设置认证 Cookie,并触发一系列的动作和过滤器,允许插件自定义认证过程和执行登录后的操作。理解 wp_signon() 函数的实现原理,可以帮助你更好地理解 WordPress 的安全机制,并开发出更安全、更可靠的 WordPress 插件和主题。

希望今天的讲解对大家有所帮助! 咱们下次再见!

发表回复

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