驗證用戶名和密碼的過程,我們平時做一個系統的時候,很多時候都會涉及到身份驗證。今天我們就來看下Mysql是如何進
行驗證的。(注意是登錄,不是登陸^_^) 一、用戶認證原理 我們在應用程序中實現驗證的方式基本上都是創建一張用戶表,裡面至少包含username和password兩個字段, password基本上都是加密後進行存儲的。作為數據庫,對用戶的限制較多,不是像我說的僅僅只有username和password 這麼簡單了。首先粗略的講下訪問控制。 信息系統中,訪問控制分為自主訪問控制(DAC)和強制訪問控制(MAC)。具體到DBMS,自主訪問控制就是我們所熟悉 的GRANT,REVOKE,大多數數據庫都支持自助的訪問控制。強制訪問控制就是ORACLE中的LABEL,只有很少的一些系統支持MAC。 嚴格來說,登錄並不屬於訪問控制機制,而應該屬於用戶身份識別和認證。在Mysql中,將登錄和DAC的相關接口都實現在了 sql_acl.cc中(其實說登錄是用戶擁有的一種權限也未嘗不可,正如ORACLE中的CREATE SESSION,不過登錄並不僅僅是一種權 限,還包含很多其他的屬性),從文件名大家可以看出來,ACL即ACCESS CONTROL LIST,訪問控制列表,這是實現訪問控制的 基本方法。下圖是Mysql的整個訪問控制的流程。 Mysql中用戶管理模塊的信息存儲在系統表mysql.User中,這個表不僅僅存放了授權用戶的基本信息,還存放一些權限 信息。我們首先大概看一下這個表的結構。代碼如下 復制代碼這個表包含了39個字段,對於我們登錄來說,應該主要是使用前三個字段,即Host,User,Password。+-----------------------+-----------------------------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-----------------------+-----------------------------------+------+-----+---------+-------+ | Host | char(60) | NO | PRI | | | | User | char(16) | NO | PRI | | | | Password | char(41) | NO | | | | | Select_priv | enum('N','Y') | NO | | N | | | Insert_priv | enum('N','Y') | NO | | N | | | Update_priv | enum('N','Y') | NO | | N | | | Delete_priv | enum('N','Y') | NO | | N | | | Create_priv | enum('N','Y') | NO | | N | | | Drop_priv | enum('N','Y') | NO | | N | | | Reload_priv | enum('N','Y') | NO | | N | | | Shutdown_priv | enum('N','Y') | NO | | N | | | Process_priv | enum('N','Y') | NO | | N | | | File_priv | enum('N','Y') | NO | | N | | | Grant_priv | enum('N','Y') | NO | | N | | | References_priv | enum('N','Y') | NO | | N | | | Index_priv | enum('N','Y') | NO | | N | | | Alter_priv | enum('N','Y') | NO | | N | | | Show_db_priv | enum('N','Y') | NO | | N | | | Super_priv | enum('N','Y') | NO | | N | | | Create_tmp_table_priv | enum('N','Y') | NO | | N | | | Lock_tables_priv | enum('N','Y') | NO | | N | | | Execute_priv | enum('N','Y') | NO | | N | | | Repl_slave_priv | enum('N','Y') | NO | | N | | | Repl_client_priv | enum('N','Y') | NO | | N | | | Create_view_priv | enum('N','Y') | NO | | N | | | Show_view_priv | enum('N','Y') | NO | | N | | | Create_routine_priv | enum('N','Y') | NO | | N | | | Alter_routine_priv | enum('N','Y') | NO | | N | | | Create_user_priv | enum('N','Y') | NO | | N | | | Event_priv | enum('N','Y') | NO | | N | | | Trigger_priv | enum('N','Y') | NO | | N | | | ssl_type | enum('','ANY','X509','SPECIFIED') | NO | | | | | ssl_cipher | blob | NO | | NULL | | | x509_issuer | blob | NO | | NULL | | | x509_subject | blob | NO | | NULL | | | max_questions | int(11) unsigned | NO | | 0 | | | max_updates | int(11) unsigned | NO | | 0 | | | max_connections | int(11) unsigned | NO | | 0 | | | max_user_connections | int(11) unsigned | NO | | 0 | | +-----------------------+-----------------------------------+------+-----+---------+-------+ 39 rows in set (0.01 sec)
代碼如下 復制代碼這裡比我們預想的只需要用戶名和密碼的方式有所出入,多了一個Host字段,這個字段起到什麼作用呢?!原來Mysql的登錄認證不僅需要驗證用戶名和密碼,還需要驗證連接的主機地址,這樣也是為了提高安全性吧。那如果我想一個用戶在任何地址都可以進行登錄豈不是要設置很多地址?Mysql提供了通配符,可以設置Host字段為*,這就代表可以匹配任何Host。具體看下這三行的意思,這三行的密碼均為空。針對root用戶,不需要輸入密碼,客戶端的地址為本機。第三行的用戶名為空,Host為localhost,說明本地的任何用戶均可以進行登錄,即使是個不存在的用戶也可以登錄成功,但是僅限於登錄,沒有其他相關的權限,無法進行實際操作。 二、源碼跟蹤 在Connection Manager中提到了login_connection函數用於檢查用戶名和密碼等相關信息,其源碼如下(重點的函數代碼 會著色):mysql> select Host,User,Password from user; +-----------+------+----------+ | Host | User | Password | +-----------+------+----------+ | localhost | root | | | 127.0.0.1 | root | | | localhost | | | +-----------+------+----------+ 3 rows in set (0.00 sec)復制代碼
代碼如下 復制代碼此函數主要是功能是調用函數check_connection進行用戶認證,由於函數check_connection過長,對其進行簡化,如下所示:static bool login_connection(THD *thd) { NET *net= &thd->net; int error; DBUG_ENTER("login_connection"); DBUG_PRINT("info", ("login_connection called by thread %lu", thd->thread_id)); /* Use "connect_timeout" value during connection phase */ my_net_set_read_timeout(net, connect_timeout); my_net_set_write_timeout(net, connect_timeout); error= check_connection(thd); //此處是驗證的具體函數 net_end_statement(thd); if (error) { // Wrong permissions #ifdef __NT__ if (vio_type(net->vio) == VIO_TYPE_NAMEDPIPE) my_sleep(1000); /* must wait after eof() */ #endif statistic_increment(aborted_connects,&LOCK_status); DBUG_RETURN(1); } /* Connect completed, set read/write timeouts back to default */ my_net_set_read_timeout(net, thd->variables.net_read_timeout); my_net_set_write_timeout(net, thd->variables.net_write_timeout); DBUG_RETURN(0); }
static int check_connection(THD *thd) { uint connect_errors= 0; NET *net= &thd->net; ulong pkt_len= 0; char *end; DBUG_PRINT("info", ("New connection received on %s", vio_description(net->vio))); #ifdef SIGNAL_WITH_VIO_CLOSE thd->set_active_vio(net->vio); #endif if (!thd->main_security_ctx.host) // If TCP/IP connection { char ip[30]; if (vio_peer_addr(net->vio, ip, &thd->peer_port)) { my_error(ER_BAD_HOST_ERROR, MYF(0), thd->main_security_ctx.host_or_ip); return 1; } if (!(thd->main_security_ctx.ip= my_strdup(ip,MYF(MY_WME)))) return 1; /* The error is set by my_strdup(). */ thd->main_security_ctx.host_or_ip= thd->main_security_ctx.ip; vio_in_addr(net->vio,&thd->remote.sin_addr); if (!(specialflag & SPECIAL_NO_RESOLVE)) { vio_in_addr(net->vio,&thd->remote.sin_addr); thd->main_security_ctx.host= ip_to_hostname(&thd->remote.sin_addr, &connect_errors); /* Cut very long hostnames to avoid possible overflows */ if (thd->main_security_ctx.host) { if (thd->main_security_ctx.host != my_localhost) thd->main_security_ctx.host[min(strlen(thd->main_security_ctx.host), HOSTNAME_LENGTH)]= 0; thd->main_security_ctx.host_or_ip= thd->main_security_ctx.host; } if (connect_errors > max_connect_errors) { my_error(ER_HOST_IS_BLOCKED, MYF(0), thd->main_security_ctx.host_or_ip); return 1; } } ... if (acl_check_host(thd->main_security_ctx.host, thd->main_security_ctx.ip))//此處驗證主機名或IP是否存在 { my_error(ER_HOST_NOT_PRIVILEGED, MYF(0), thd->main_security_ctx.host_or_ip); return 1; } } else /* Hostname given means that the connection was on a socket */ { ... } vio_keepalive(net->vio, TRUE); ... char *user= end; char *passwd= strend(user)+1; uint user_len= passwd - user - 1; char *db= passwd; char db_buff[NAME_LEN + 1]; // buffer to store db in utf8 char user_buff[USERNAME_LENGTH + 1]; // buffer to store user in utf8 uint dummy_errors; uint passwd_len= thd->client_capabilities & CLIENT_SECURE_CONNECTION ? (uchar)(*passwd++) : strlen(passwd); db= thd->client_capabilities & CLIENT_CONNECT_WITH_DB ? db + passwd_len + 1 : 0; uint db_len= db ? strlen(db) : 0; if (passwd + passwd_len + db_len > (char *)net->read_pos + pkt_len) { inc_host_errors(&thd->remote.sin_addr); my_error(ER_HANDSHAKE_ERROR, MYF(0), thd->main_security_ctx.host_or_ip); return 1; } ... /* If username starts and ends in "'", chop them off */ if (user_len > 1 && user[0] == ''' && user[user_len - 1] == ''') { user[user_len-1]= 0; user++; user_len-= 2; } if (thd->main_security_ctx.user) x_free(thd->main_security_ctx.user); if (!(thd->main_security_ctx.user= my_strdup(user, MYF(MY_WME)))) return 1; /* The error is set by my_strdup(). */ return check_user(thd, COM_CONNECT, passwd, passwd_len, db, TRUE);//驗證用戶名和密碼 }上面的源碼主要做了如下幾件事情: