跳转至

当客户端输入 mysql -h x.x.x.x -u root -p 时,服务端在做什么(下)

Conventions

  • 每一个代码块的顶部都有它所属的文件(相对)路径,如果代码块属于某个函数(方法),那么顶部会有函数(方法)的声明。
  • 代码块中的对象,如果很重要,会在行尾补充该对象的声明。
  • 我使用的MySQL 版本是 8.0.41。你看到这篇时,可能有了更新的版本,比如 8.0.42,区别不大的。

快速认证

sql/auth/sha2_password.cc static int caching_sha2_password_authenticate(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info)
  std::pair<bool, bool> fast_auth_result =
      g_caching_sha2_password->fast_authenticate(
          authorization_id, reinterpret_cast<unsigned char *>(scramble),
          SCRAMBLE_LENGTH, pkt,
          info->additional_auth_string_length ? true : false);

上篇,我们看到了服务端与客户端的握手,握手完成后又做了 TLS 握手,程序走到这里,服务端已经获取到了用户的用户名和密码。接下来,我们看看服务端如何验证密码。

MySQL 在这个版本使用的默认身份验证插件是 caching_sha2_password,从这个名字我们能猜出这个插件的 2 个特征,一个是缓存,因为它叫 caching,另一个是使用 sha2 散列算法。

Tip

sha2 散列算法是一个系列,包含多种散列算法,当前 MySQL 使用 sha256 散列算法,任意值经过 sha256 散列运算后会得到一个 256 比特(32 字节)长度的值。

我们看上面这段代码片段,这是 caching_sha2_password_authenticate 函数中的第 989 行,程序走到这里,调用 fast_authenticate 函数验证密码。为什么函数名字叫快速认证,那肯定是它用了缓存,如果缓存没有命中,那就得调用慢速认证了。不急,我们先看看快速认证的实现。

sql/auth/sha2_password.cc
/**
  Perform fast authentication

  1. Retrieve hash from cache
  2. Validate it against received scramble

  @param [in] authorization_id User information
  @param [in] random           Per session random number
  @param [in] random_length    Length of the random number
  @param [in] scramble         Scramble received from the client
  @param [in] check_second     Check secondary credentials

  @returns Outcome of scramble validation and whether
           second password was used or not.
*/

std::pair<bool, bool> Caching_sha2_password::fast_authenticate(
    const std::string &authorization_id, const unsigned char *random,
    unsigned int random_length, const unsigned char *scramble,
    bool check_second) {
  DBUG_TRACE;
  if (!scramble || !random) {
    DBUG_PRINT("info", ("For authorization id : %s,"
                        "Scramble is null - %s :"
                        "Random is null - %s :",
                        authorization_id.c_str(), !scramble ? "true" : "false",
                        !random ? "true" : "false"));
    return std::make_pair(true, false);
  }

  rwlock_scoped_lock rdlock(&m_cache_lock, false, __FILE__, __LINE__);
  sha2_cache_entry digest;

  if (m_cache.search(authorization_id, digest)) {
    DBUG_PRINT("info", ("Could not find entry for %s in cache.",
                        authorization_id.c_str()));
    return std::make_pair(true, false);
  }

  /* Entry found, so validate scramble against it */
  Validate_scramble validate_scramble_first(scramble, digest.digest_buffer[0],
                                            random, random_length);
  bool retval = validate_scramble_first.validate();
  bool second = false;
  if (retval && check_second) {
    second = true;
    Validate_scramble validate_scramble_second(
        scramble, digest.digest_buffer[1], random, random_length);
    retval = validate_scramble_second.validate();
  }
  return std::make_pair(retval, second);
}

先看快速认证函数的注释,它说第一步从缓存中获取散列值,第二步根据接收到的随机数(scramble)进行验证。

第 368 行就是从缓存中获取散列值,m_cache 内部维护了一个散列表,key 是用户名,value 是密码散列值。如果命中缓存,那么 digest 变量就被赋值为密码散列值;如果没有命中缓存,那么直接返回 std::pair<true, false> 布尔值对。

第 375-377 行根据接收到的随机数(scramble)进行验证。375 行声明并初始化 validate_scramble_first 变量,Validate_scramble 类的构造函数接收了 4 个变量,第一个是客户端发送给服务端的随机值 scramble,第二个是(缓存中取出的)密码散列值,第三个是握手阶段服务端握手包中的 salt,在这里叫 random,第四个是盐长度。377 行调用 validate 进行验证。

sql/auth/sha2_password_common.cc
/**
  Validate the scramble

  @note
    SHA2(known, rnd) => scramble_stage1
    XOR(scramble, scramble_stage1) => digest_stage1
    SHA2(digest_stage1) => digest_stage2
    m_known == digest_stage2

  @returns Result of validation process
    @retval false Successful validation
    @retval true Error
*/

bool Validate_scramble::validate() {
  DBUG_TRACE;
  unsigned char *digest_stage1 = nullptr;
  unsigned char *digest_stage2 = nullptr;
  unsigned char *scramble_stage1 = nullptr;

  switch (m_digest_type) {
    case Digest_info::SHA256_DIGEST: {
      digest_stage1 = (unsigned char *)alloca(m_digest_length);
      digest_stage2 = (unsigned char *)alloca(m_digest_length);
      scramble_stage1 = (unsigned char *)alloca(m_digest_length);
      break;
    }
    default: {
      assert(false);
      return true;
    }
  }

  /* SHA2(known, m_rnd) => scramble_stage1 */
  if (m_digest_generator->update_digest(m_known, m_digest_length) ||
      m_digest_generator->update_digest(m_rnd, m_rnd_length) ||
      m_digest_generator->retrieve_digest(scramble_stage1, m_digest_length)) {
    DBUG_PRINT("info",
               ("Failed to generate scramble_stage1: SHA2(known, m_rnd)"));
    return true;
  }

  /* XOR(scramble, scramble_stage1) => digest_stage1 */
  for (unsigned int i = 0; i < m_digest_length; ++i)
    digest_stage1[i] = (m_scramble[i] ^ scramble_stage1[i]);

  /* SHA2(digest_stage1) => digest_stage2 */
  m_digest_generator->scrub();
  if (m_digest_generator->update_digest(digest_stage1, m_digest_length) ||
      m_digest_generator->retrieve_digest(digest_stage2, m_digest_length)) {
    DBUG_PRINT("info",
               ("Failed to generate digest_stage2: SHA2(digest_stage1)"));
    return true;
  }

  /* m_known == digest_stage2 */
  if (memcmp(m_known, digest_stage2, m_digest_length) == 0) return false;

  return true;
}

validate 函数的注释写得很清楚,它分四步来验证。

在讲这四步之前,我们先了解一下客户端发送给服务端的 scramble 到底是什么。它由 XOR(SHA2(password), SHA2(SHA2(SHA2(password)), salt)) 计算得到,拆开看会看得更清楚,

第一部分 SHA2(password),这就是对用户密码进行了一次散列。

第二部分 SHA2(SHA2(SHA2(password)), salt),对用户密码进行了二次散列,并将结果与盐拼接后再次散列。

最后将第一部分计算的结果与第二部分计算的结果进行异或运算,得到 scamble。在上述计算过程中,密码来自用户的输入,盐来自服务端的握手包。

知道了 scramble 是什么后,再看 validate 函数就好懂了。它通过对 scrambleSHA2(SHA2(SHA2(password)), salt) 进行异或运算,得到了 SHA2(password),然后求得 SHA2(SHA2(password)),将它与缓存中的值比较,相同就验证通过。

也就是说缓存中存放的不是密码明文,而是对密码明文的二次散列值,即 SHA2(SHA2(password)),盐又是服务端每一次握手时随机生成的,所以服务端可以计算 SHA2(SHA2(SHA2(password)), salt),将它与 scramble 异或后,就能还原出对密码的一次散列值,即 SHA2(password)

服务端通过计算从客户端发来的 scramble 得到了 SHA2(password),服务端缓存中有 SHA2(SHA2(password)),自然可以验证用户的密码是否正确。

现在,我们来看 validate 函数注释中的四个步骤:

  1. SHA2(known, rnd) => scramble_stage1

    known 就是缓存中的值,是明文密码的二次散列值;rnd 是盐。对应代码 337-339 行。

  2. XOR(scramble, scramble_stage1) => digest_stage1

    scramble 是客户端发来的随机数,scramble_stage1 是第一步的结果。对应代码 346-347 行。

  3. SHA2(digest_stage1) => digest_stage2

    digest_stage1 是第二步的结果。对应代码 351-352 行。

  4. m_known == digest_stage2

    m_known 是缓存中的值,和第一步中的 known 是同一个;digest_stage2 是第三步的结果。对应代码 359 行。

慢速认证

如果没有命中缓存,该怎么办?

那只能让客户端发明文密码过来了,当然我们已经完成了 TLS 握手,所以发明文密码没毛病。

sql/auth/sha2_password.cc
/**
  Perform slow authentication.

  1. Disect serialized_string and retrieve
    a. Salt
    b. Hash iteration count
    c. Expected hash
  2. Use plaintext password, salt and hash iteration count to generate
     hash.
  3. Validate generated hash against expected hash.

  In case of successful authentication, update password cache.

  @param [in] authorization_id   User information
  @param [in] serialized_string        Information retrieved from
                                 mysql.authentication_string column
  @param [in] plaintext_password Password as received from client

  @returns Outcome of comparison against expected hash and whether
           second password was used or not.
*/

std::pair<bool, bool> Caching_sha2_password::authenticate(
    const std::string &authorization_id, const std::string *serialized_string,
    const std::string &plaintext_password) {
  DBUG_TRACE;

  /* Don't process the password if it is longer than maximum limit. */
  if (plaintext_password.length() > CACHING_SHA2_PASSWORD_MAX_PASSWORD_LENGTH)
    return std::make_pair(true, false);

  /* Empty authentication string. */
  if (!serialized_string[0].length())
    return std::make_pair(plaintext_password.length() ? true : false, false);

  bool second = false;
  for (unsigned int i = 0;
       i < MAX_PASSWORDS && serialized_string[i].length() > 0; ++i) {
    second = i > 0;
    std::string random;
    std::string digest;
    std::string generated_digest;
    Digest_info digest_type;
    size_t iterations;

    /*
      Get digest type, iterations, salt and digest
      from the authentication string.
    */
    if (deserialize(serialized_string[i], digest_type, random, digest,
                    iterations)) {
      if (m_plugin_info)
        LogPluginErr(ERROR_LEVEL, ER_SHA_PWD_FAILED_TO_PARSE_AUTH_STRING,
                     authorization_id.c_str());
      return std::make_pair(true, second);
    }

    /*
      Generate multiple rounds of sha2 hash using plaintext password
      and salt retrieved from the authentication string.
    */

    if (this->generate_sha2_multi_hash(plaintext_password, random,
                                       &generated_digest, iterations)) {
      if (m_plugin_info)
        LogPluginErr(ERROR_LEVEL,
                     ER_SHA_PWD_FAILED_TO_GENERATE_MULTI_ROUND_HASH,
                     authorization_id.c_str());
      return std::make_pair(true, second);
    }

    /*
      Generated digest should match with stored digest
      for successful authentication.
    */
    if (memcmp(digest.c_str(), generated_digest.c_str(),
               STORED_SHA256_DIGEST_LENGTH) == 0) {
      /*
        If authentication is successful, we would want to make
        entry in cache for fast authentication. Subsequent
        authentication attempts would use the fast authentication
        to speed up the process.
      */
      sha2_cache_entry fast_digest;
      memset(&fast_digest, 0, sizeof(fast_digest));

      if (generate_fast_digest(plaintext_password, fast_digest, i)) {
        DBUG_PRINT("info", ("Failed to generate multi-round hash for %s. "
                            "Fast authentication won't be possible.",
                            authorization_id.c_str()));
        return std::make_pair(false, second);
      }

      rwlock_scoped_lock wrlock(&m_cache_lock, true, __FILE__, __LINE__);
      if (m_cache.add(authorization_id, fast_digest)) {
        sha2_cache_entry stored_digest;
        m_cache.search(authorization_id, stored_digest);

        /* Same digest is already added, so just return. */
        if (memcmp(fast_digest.digest_buffer[i], stored_digest.digest_buffer[i],
                   sizeof(fast_digest.digest_buffer[i])) == 0)
          return std::make_pair(false, second);

        /* Update the digest. */
        uint retain_index = i ? 0 : 1;
        memcpy(fast_digest.digest_buffer[retain_index],
               stored_digest.digest_buffer[retain_index],
               sizeof(fast_digest.digest_buffer[retain_index]));
        m_cache.remove(authorization_id);
        m_cache.add(authorization_id, fast_digest);
        DBUG_PRINT("info", ("An old digest for %s was recorded in cache. "
                            "It has been replaced with the latest digest.",
                            authorization_id.c_str()));
        return std::make_pair(false, second);
      }
      return std::make_pair(false, second);
    }
  }
  return std::make_pair(true, second);
}

慢速认证可不是我说的,你看上面这个函数的注释,是 MySQL 开发自己写的。

不知道你有没有看过系统数据库 mysql 中的用户表 user 中的 authentication_string 字段,该字段存放着用户密码的散列值和盐。做过后端开发的小伙伴肯定很熟悉,因为大家都是这么做的,存明文密码是不可能存的。

第 263 行 deserialize 函数就是解析 authentication_string 字段。这个字段的格式是 DELIMITER[digest_type]DELIMITER[iterations]DELIMITER[salt][digest],其中 DELIMITER 分隔符是美元符,[digest_type] 表示所使用的散列算法,[iterations] 表示做了几次散列运算,[salt] 是盐,[digest] 是散列计算的结果。

第 276 行根据用户的明文密码、盐、散列运算次数,计算出散列值。

第 289 行比较 276 行计算得到的散列值与数据库中存储的是否一样。

第 300 行根据明文密码计算 2 次散列值。

第 308 行将 2 次散列值加入缓存。

小结

快速认证的关键在于服务端缓存中保存了用户密码的二次散列值,慢速认证的关键在于服务端使用用户明文密码与数据库中保存的盐值进行散列计算后,再与数据库中保存的散列值进行比较,成功后会计算二次散列值放入缓存中,之后就能享受到快速认证。

慢速认证并不会真的从数据库中查询 mysql.user 表,该表在服务端启动阶段就已经缓存起来,所以慢速认证并不会慢很多。

下一篇,连接阶段总结篇。