1. 程式人生 > >mysql 賬戶登入的處理流程

mysql 賬戶登入的處理流程

現場環境mysql賬戶登入錯誤,使用者名稱和密碼都確認過沒有問題的。報錯資訊如下:

ERROR 1045 (28000): Access denied for user 'test'@'localhost' (using password: YES)

檢視1045對應的錯誤資訊:

perror 1045:MySQL error code 1045 (ER_ACCESS_DENIED_ERROR): Access denied for user '%-.48s'@'%-.64s' (using password: %s), 顯然使用者名稱和密碼訪問被拒絕了,從原始碼跟蹤客戶端認證的流程。

 sql_connection.cc:check_connection   客戶端認證的入口函式,主要工作:

vio_peer_addr函式:根據vio獲取對端的ip地址

ip_to_hostname函式:根據ip解析對應的host_name,在沒有配置skip_name_resolve的場景下工作

acl_check_host函式:匹配客戶端機器是否滿足mysql.user表中的host列,在程式啟動的時候會將mysql.user表中的host分為兩個hash結構儲存:acl_check_hosts和acl_wild_hosts,該函式檢查host或者ip是否在這兩個結構中包含

acl_authenticate:進行客戶端認證的主要函式

 

acl_authenticate 該函式主要功能如下圖,其中scrample是生成的一串隨機數值,用於認證使用;do_auth_once負責後續的認證工作

 else
  {
    /* mark the thd as having no scramble yet */
    mpvio.scramble[SCRAMBLE_LENGTH]= 1;
    
    /*
     perform the first authentication attempt, with the default plugin.
     This sends the server handshake packet, reads the client reply
     with a user name, and performs the authentication if everyone has used
     the correct plugin.
    */

    res= do_auth_once(thd, auth_plugin_name, &mpvio);  該函式後續也呼叫了一次,當非預設的plugin處理登入驗證的時候使用。預設的plugin是mysql_native_password,後續也是以該儲存引擎的驗證作為示例
  }

 

do_auth_once函式:

  if (auth_plugin_name.str == native_password_plugin_name.str)
    plugin= native_password_plugin;     採用預設外掛
#ifndef EMBEDDED_LIBRARY
  else
  {
    if ((plugin= my_plugin_lock_by_name(thd, auth_plugin_name,
                                        MYSQL_AUTHENTICATION_PLUGIN)))   根據外掛名稱獲取對應的外掛,外掛名稱在mysql.user表中plugin列
      unlock_plugin= true;
  }
#endif /* EMBEDDED_LIBRARY */

    
  mpvio->plugin= plugin;
  old_status= mpvio->status;
  
  if (plugin)
  {
    st_mysql_auth *auth= (st_mysql_auth *) plugin_decl(plugin)->info;
    res= auth->authenticate_user(mpvio, &mpvio->auth_info);  關鍵認證引數

    if (unlock_plugin)
      plugin_unlock(thd, plugin);
  }

authenticate_user函式,這是一個函式指標不同的plugin對應不同的處理函式,native_password_plugin外掛對應的處理函式為native_password_authenticate

 

native_password_authenticate函式:

static int native_password_authenticate(MYSQL_PLUGIN_VIO *vio,
                                        MYSQL_SERVER_AUTH_INFO *info)
{

 ......

  /* generate the scramble, or reuse the old one */
  if (mpvio->scramble[SCRAMBLE_LENGTH])
    generate_user_salt(mpvio->scramble, SCRAMBLE_LENGTH + 1); :生成一段隨機的數值

  /* 該這段隨機值傳送到client段,開始mysql連線的三次握手,write_packet指向server_mpvio_write_packet函式*/
  if (mpvio->write_packet(mpvio, (uchar*) mpvio->scramble, SCRAMBLE_LENGTH + 1))
    DBUG_RETURN(CR_AUTH_HANDSHAKE);

 ......

  /* read the reply with the encrypted password */
  if ((pkt_len= mpvio->read_packet(mpvio, &pkt)) < 0)    該函式指標指向:server_mpvio_read_packet, 讀取client返回的報文,該報文中含有加密後的密碼,該密碼=使用者輸入的密碼和mpvio->scrambl進行多次加密後計算所得
   ......
  if (pkt_len == SCRAMBLE_LENGTH)
  {
    if (!mpvio->acl_user->salt_len)
      DBUG_RETURN(CR_AUTH_USER_CREDENTIALS);

    DBUG_RETURN(check_scramble(pkt, mpvio->scramble, mpvio->acl_user->salt) ?
                CR_AUTH_USER_CREDENTIALS : CR_OK);    #密碼比較,根據客戶端返回的結果(處理後)同mysql.user表中的密碼進行驗證
  }

   ......
}

server_mpvio_read_packet函式:

read_packet:讀取返回報文到buffer中

parse_client_handshake_packet:解析客戶端的認證握手包

 

 

parse_client_handshake_packet函式,該函式根據不同的資料庫版本解析報文,獲取使用者名稱(連線時的輸入引數)和經過處理後的密碼

  char *user= get_string(&end, &bytes_remaining_in_packet, &user_len); 獲取使用者名稱

passwd= get_length_encoded_string(&end, &bytes_remaining_in_packet,
                                    &passwd_len);: 獲取經過處理後的密碼

find_mpvio_user(mpvio): 根據host和user匹配mysql.user中對應的一條記錄。匹配的順序是按照acl_users變數中的值

 

sql_auth_cache.cc: acl_load函式實現了載入mysql.user表的內容到acl_users中,其中對記錄進行了排序:

std::sort(acl_users->begin(), acl_users->end(), ACL_compare()); 注意排序規則函式:根據成員的sort變數的降序排列

sort變數的賦值:sql_auth_cache.cc:get_sort函式,根據這條記錄的host和user計算,先按照host,host相同按照user配需,排序的規則:

1.沒有萬用字元        2.有部分萬用字元  ,如::1                 3.%萬用字元       4. 空的 (匿名賬戶)

 

回到最初的問題;

登入命令: mysql -u test -p  -h 127.0.0.1  配置檔案中沒有配置 skip_name_resolve

登入時對應客戶端的host 和 user 分別是localhost 和 test,

資料庫中載入的acl的users值為:

+-----------+-----------+
| host      | user      |
+-----------+-----------+
| localhost | mysql.sys |
| localhost |           |
| %         | test      |
+-----------+-----------+

因此賬戶匹配到localhost和空user,而不是匹配到% 和test,導致密碼出錯。

如果配置了skip_name_resolve引數,不會進行ip_to_hostname的轉換,只會按照根據ip地址匹配,這是匹配到% test的賬戶,可以成功登入。

 

相關內容參考官方文件:https://dev.mysql.com/doc/refman/5.7/en/connection-access.html