你真的了解如何登錄MySQL么?


   昨天同事碰到一個問題,在MySQL上創建了一個用戶,host設置為%,本地竟然無法登錄。創建一個host為localhost的同名用戶后,本地可以登錄。感腳很怪異,下面我們重新分析產生這個問題的原因。

1. 現場重現

*root本地登錄

Shell>./mysql -uroot
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 7
Server version: 5.5.17-debug-log Source distribution

Copyright (c) 2000, 2011, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

*切換到mysql庫下

mysql> use mysql;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

*創建用戶

mysql> create user 'u1'@'%' identified by '1111111';
Query OK, 0 rows affected (0.00 sec)

mysql> select user,host from user;
+------+-----------+
| user | host      |
+------+-----------+
| repl | %         |
| u1   | %         |
| root | 127.0.0.1 |
| root | ::1       |
|      | Ubuntu    |
| root | Ubuntu    |
|      | localhost |
| repl | localhost |
| root | localhost |
+------+-----------+
9 rows in set (0.00 sec)

*本地登錄——使用新用戶+密碼

Shell>./mysql -uu1 --protocol=tcp --port=13000 -p1111111
ERROR 1045 (28000): Access denied for user 'u1'@'localhost' (using password: YES)

*本地登錄——使用新用戶+空密碼登錄

Shell>./mysql -uu1 --protocol=tcp --port=13000
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 11
Server version: 5.5.17-debug-log Source distribution

Copyright (c) 2000, 2011, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> select current_user;
+--------------+
| current_user |
+--------------+
| @localhost   |
+--------------+
1 row in set (0.00 sec)

mysql> select user();
+--------------+
| user()       |
+--------------+
| u1@localhost |
+--------------+
1 row in set (0.00 sec)

*遠程登錄——使用新用戶+密碼

AAA@-ThinkPad:~/mysql-bin/bin$ ./mysql -uu1 --port=13000 -h192.168.1.103 -p1111111
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 13
Server version: 5.5.17-debug-log Source distribution

Copyright (c) 2000, 2011, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> select current_user();
+----------------+
| current_user() |
+----------------+
| u1@%           |
+----------------+
1 row in set (0.01 sec)

*遠程登錄——使用新用戶+空密碼

AAA@-ThinkPad:~/mysql-bin/bin$ ./mysql -uu1 --port=13000 -h192.168.1.103
ERROR 1045 (28000): Access denied for user 'u1'@'-ThinkPad' (using password: NO)

tips:

user與current_user的區別:
user:client提供給server的用戶名和密碼
current_user:連接到server上的真正的用戶名和密碼

2. 現場分析

  上面進行了本地登錄和遠程登錄的實驗,遠程登錄與預期一致,本地登錄出現了問題。

  本地登錄使用密碼登錄時竟然失敗,不使用時竟然成功,即使成功但是current_user竟然是‘@localhost’,也就是說根本不是u1用戶登錄成功,而是‘@localhost’登錄成功。

  我們在上面的user表的信息可以看到確實存在一個user為空,host為localhost的用戶。這是個新庫創建時自帶的用戶。

  下面就有兩個疑問了:

        為嘛本地登錄使用密碼不能成功?為嘛不用密碼登錄成功但登錄用戶卻是‘@localhost’呢?結合代碼來看看真正的認證過程吧。

 

3. 代碼分析

  關於用戶認證的代碼基本都在sql/sql_acl.cc文件中,acl即access control list(訪問控制列表),MySQL在系統啟動時會調用acl_load,將mysql.user表中的信息全部加載到系統中,這里不涉及加載,只為尋找真相。為了方便用戶自行跟蹤,給一個身份驗證的堆棧。

(gdb) bt
#0  compare_hostname (host=0x1d80580, hostname=0xb45e52 "localhost", ip=0x1dc8430 "127.0.0.1")
    at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_acl.cc:2041
#1  0x00000000005990dc in find_mpvio_user (mpvio=0x7fffe8165530) at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_acl.cc:8215
#2  0x000000000059a651 in parse_client_handshake_packet (mpvio=0x7fffe8165530, buff=0x7fffe8165418, pkt_len=58)
    at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_acl.cc:8746
#3  0x000000000059adba in server_mpvio_read_packet (param=0x7fffe8165530, buf=0x7fffe8165418)
    at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_acl.cc:8970
#4  0x000000000059c338 in native_password_authenticate (vio=0x7fffe8165530, info=0x7fffe8165548)
    at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_acl.cc:9547
#5  0x000000000059b350 in do_auth_once (thd=0x1d5e470, auth_plugin_name=0xfd1280, mpvio=0x7fffe8165530)
    at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_acl.cc:9133
#6  0x000000000059b7fe in acl_authenticate (thd=0x1d5e470, connect_errors=0, com_change_user_pkt_len=0)
    at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_acl.cc:9269
#7  0x00000000006d4ed9 in check_connection (thd=0x1d5e470) at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_connect.cc:524
#8  0x00000000006d5034 in login_connection (thd=0x1d5e470) at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_connect.cc:582
#9  0x00000000006d5500 in thd_prepare_connection (thd=0x1d5e470) at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_connect.cc:716
#10 0x00000000006d599a in do_handle_one_connection (thd_arg=0x1d5e470)
    at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_connect.cc:782
#11 0x00000000006d54c9 in handle_one_connection (arg=0x1d5e470) at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_connect.cc:708
#12 0x00007ffff6ee3efc in start_thread () from /lib/x86_64-linux-gnu/libpthread.so.0
#13 0x00007ffff6c1e59d in clone () from /lib/x86_64-linux-gnu/libc.so.6
#14 0x0000000000000000 in ?? ()

  堆棧上的#4 native_password_authenticate便是真正的身份認證函數,感興趣的同學可以自己仔細看,我們具體看下find_mpvio_user函數:

static bool find_mpvio_user(MPVIO_EXT *mpvio)
{
  DBUG_ENTER("find_mpvio_user");
  DBUG_PRINT("info", ("entry: %s", mpvio->auth_info.user_name));
  DBUG_ASSERT(mpvio->acl_user == 0);
  mysql_mutex_lock(&acl_cache->lock);
  for (uint i=0; i < acl_users.elements; i++)
  {
    ACL_USER *acl_user_tmp= dynamic_element(&acl_users, i, ACL_USER*);
    if ((!acl_user_tmp->user || 
         !strcmp(mpvio->auth_info.user_name, acl_user_tmp->user)) &&
        compare_hostname(&acl_user_tmp->host, mpvio->host, mpvio->ip))
    {
      mpvio->acl_user= acl_user_tmp->copy(mpvio->mem_root);
      if (acl_user_tmp->plugin.str == native_password_plugin_name.str ||
          acl_user_tmp->plugin.str == old_password_plugin_name.str)
        mpvio->acl_user_plugin= acl_user_tmp->plugin;
      else
        make_lex_string_root(mpvio->mem_root, 
                             &mpvio->acl_user_plugin, 
                             acl_user_tmp->plugin.str, 
                             acl_user_tmp->plugin.length, 0);
      break;
    }
 
....
}

  acl_users即為緩存mysql.user表中數據的動態數組。函數的基本邏輯是逐個遍歷acl_users中的每個user,首先判斷acl_user_tmp的用戶名為空或者用戶名和登錄的用戶名相同,然后比較host的值,由於‘@localhost’在列表中比‘u1@%’靠前,而且我們是本地登錄,這就導致acl_user_tmp為空且host比較成功,就返回了‘@localhost’。這就是為什么current_user為‘@localhost’的原因。

  那么為嘛加了密碼就不行了呢?這是由於在獲得了內存中的user后,會進行密碼的驗證。

static int native_password_authenticate(MYSQL_PLUGIN_VIO *vio,
                                        MYSQL_SERVER_AUTH_INFO *info)
{
 
....
  if (pkt_len == 0) /* no password */
    DBUG_RETURN(mpvio->acl_user->salt_len != 0 ? CR_ERROR : CR_OK);

  info->password_used= PASSWORD_USED_YES;
  if (pkt_len == SCRAMBLE_LENGTH)
  {
    if (!mpvio->acl_user->salt_len)
      DBUG_RETURN(CR_ERROR);

    DBUG_RETURN(check_scramble(pkt, mpvio->scramble, mpvio->acl_user->salt) ?
                CR_ERROR : CR_OK);
  }

....
}

  如果輸入了密碼,那么就會進入pkt_len==SCRAMBLE_LEN的分支,而此時的用戶為系統默認的用戶'@localhost',所以mpvio->acl_user->salt_len必然為0,
故返回ERROR。

  為嘛‘@localhost’在列表中比‘u1@%’靠前,這就涉及到acl_users的排序問題了,先給出一個堆棧:

#0  get_sort (count=1) at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_acl.cc:1266
#1  0x0000000000580427 in acl_load (thd=0x1db0c50, tables=0x7fffffffc970)
    at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_acl.cc:872
#2  0x00000000005815bf in acl_reload (thd=0x1db0c50) at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_acl.cc:1174
#3  0x000000000057f8b6 in acl_init (dont_read_acl_tables=false) at /home/loushuai/src/mysql-server/mysql-5.5/sql/sql_acl.cc:644
#4  0x000000000055973b in mysqld_main (argc=11, argv=0x12d6f38) at /home/loushuai/src/mysql-server/mysql-5.5/sql/mysqld.cc:4551
#5  0x0000000000552154 in main (argc=2, argv=0x7fffffffe0c8) at /home/loushuai/src/mysql-server/mysql-5.5/sql/main.cc:25

#1 現在執行到

acl_user.sort=get_sort(2,acl_user.host.hostname,acl_user.user);

用於計算當前acl_user的sort值,用於后面進行重新排序。
我們看下具體的排序函數:

static ulong get_sort(uint count,...)
{
  va_list args;
  va_start(args,count);
  ulong sort=0;

  /* Should not use this function with more than 4 arguments for compare. */
  DBUG_ASSERT(count <= 4);

  while (count--)
  {
    char *start, *str= va_arg(args,char*);
    uint chars= 0;
    uint wild_pos= 0;           /* first wildcard position */

    if ((start= str))
    {
      for (; *str ; str++)
      {
        if (*str == wild_prefix && str[1])
          str++;
    //如果碰到%或者_,則記錄wild_pos
        else if (*str == wild_many || *str == wild_one) 
        {
          wild_pos= (uint) (str - start) + 1;
          break;
        }
        chars= 128;                             // Marker that chars existed
      }
    }
    sort= (sort << 8) + (wild_pos ? min(wild_pos, 127) : chars);
  }
  va_end(args);
  return sort;
}

從上面可以看出,根據host和user連個字段的值進行排序(host作為高位區分(sort<<8)),當存在%時,使用%的位置作為sort,否則就用128.
下面我們就具體看下一下三個用戶的排序:

no    host             user
1.    localhost     u1
2.    localhost      
3.    %          u1

  首先比較host,顯然1,2 大於3,因為%用的是wild_pos,而1,2走chars=128.
然后比較1,2,顯然1的優先級高,因為2為空。

  故在acl_users中的順序為1 2 3,也就是說,如果你創建了一個u1@localhost,那么使用u1就可以登錄成功,注意,這時候就需要創建時的密碼了。

  終於找到原因了,往往細微的地方,我們往往拿捏不住,不斷的發覺這些細微現象的真相,才能有所提高。

  真相只有一個:-)

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM