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