前幾節跟蹤了Connection Manager和Thread Manager,在連接的過程中,還有一個身份認證的過程,就是大家所熟悉的
驗證用戶名和密碼的過程,我們平時做一個系統的時候,很多時候都會涉及到身份驗證。今天我們就來看下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中,這個表不僅僅存放了授權用戶的基本信息,還存放一些權限
信息。我們首先大概看一下這個表的結構。
+-----------------------+-----------------------------------+------+-----+---------+-------+
| 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)
這個表包含了39個字段,對於我們登錄來說,應該主要是使用前三個字段,即Host,User,Password。
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)
這裡比我們預想的只需要用戶名和密碼的方式有所出入,多了一個Host字段,這個字段起到什麼作用呢?!原來Mysql的登錄認證不僅需要驗證用戶名和密碼,還需要驗證連接的主機地址,這樣也是為了提高安全性吧。那如果我想一個用戶在任何地址都可以進行登錄豈不是要設置很多地址?Mysql提供了通配符,可以設置Host字段為*,這就代表可以匹配任何Host。具體看下這三行的意思,這三行的密碼均為空。針對root用戶,不需要輸入密碼,客戶端的地址為本機。第三行的用戶名為空,Host為localhost,說明本地的任何用戶均可以進行登錄,即使是個不存在的用戶也可以登錄成功,但是僅限於登錄,沒有其他相關的權限,無法進行實際操作。
二、源碼跟蹤
在Connection Manager中提到了login_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);
}
此函數主要是功能是調用函數check_connection進行用戶認證,由於函數check_connection過長,對其進行簡化,如下所示:
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);//驗證用戶名和密碼
}
上面的源碼主要做了如下幾件事情:
獲取客戶端的IP和主機名
acl_check_host函數驗證USER表中是否存在相應的IP或HOST,如果不存在直接報錯
獲取用戶名和密碼
check_user函數驗證用戶名和密碼(不輸入用戶名默認為ODBC),如果系統表中不存在匹配的報錯返回
獲取用戶的權限列表,驗證用戶的相關屬性是否合法,如連接數是否超過上限,連接是否超時,操作是否超過限制等信息,如果不合法,則報錯返回。
由於在一個認證的過程中涉及到的東西比較多,各個方面吧,我不能一一跟蹤,只能大概了解其中的實現流程,撿重點進行
跟蹤,有興趣的童鞋自己具體跟蹤吧
題外話:
Mysql中權限系統表都是在系統啟動時,載入內存的(當然User表也是這樣),一般情況下,不需要進行頻繁的授權和回收
操作,這中情況下,權限表基本保持不變,將其在系統啟動的時候載入內存的好處自然是快速的進行權限判斷,減少磁盤的I/O,
你懂的^_^。有好處自然有壞處,就是在頻繁進行授權和回收相關操作時,權限表需要重新載入內存,Mysql為了避免這種情況,
在手冊中已經說的很清楚了,授權和回收只會反應到磁盤中,內存的數據字典信息是不會改變的,如果想立即生效,需要調用
FLUSH PRIVILEGES系統函數,這個系統函數的工作應該就是對權限系統表的RELOAD。
下篇進入實質性的介紹,通過跟蹤一個建表語句,來學習Mysql是如何存儲表的元數據的,即frm格式文件的剖析。
<script></script>
PS.最近工作比較清閒,卻迷失了方向,一會想看OS的實現,一會想看逆向,一會又想看計算機組成原理,哎,轉專業的學生傷
不起啊,計算機很神奇,我很迷茫…
摘自 心中無碼