2012年2月3日

【PHP】PDO,PHP Data Object 一個安全又快速的資料庫連接物件


PHP.net 已經發佈基於安全因素,在未來一定會刪除mysql extention (原文: http://news.php.net/php.internals/53799)

想當年,我初次碰上php的時候也用了好一陣子的mysql_connect()
直到去年,痛下決心學習新的技術!
原因有二:
1. PHP.net已經決定放棄 php_mysql 這個擴充套件,學習只是遲早的事,趁早轉換才是明智的選擇!

2. 之前要寫一個function用人工方式過濾sql injection
 但你沒那麼聰明,永遠跟不上駭客的腳步,所以決定交給PDO來做這件事!

剛剛說到的SQL Injection,簡單來說就是一個填空遊戲,把本來要這樣做的事情變成另一個樣子。

SELECT * FROM `users` WHERE `user_name` = '$name' AND `passwd` = '$passwd';

中間的變數,就是空格,看起來好像是在找符合的使用者名稱和密碼,但其實如果沒有對變數做過濾,很可能變成什麼都列出來,甚至被清空整個DB。

而PDO則可以讓空格就是空格,只要明確定義變數的型態和長度,就可以避開SQL Injection或buffer overflow之類的攻擊

以下用實例來講解使用PDO連結MySQL資料庫的過程

//mysql:host=localhost;dbname=db,name,pw
$dbh = new PDO($dbType.":host=".$hostname.";dbname=".$database, $username, $password);
//attribute請參考 http://www.php.net/manual/en/pdo.getattribute.php
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
//set charset
$dbh->query("SET character_set_connection=".$dbcharset.", character_set_results=".$dbcharset.", character_set_client=binary");

$presql = select * from `users` where `user_account` = :user and `user_passwd` = :pass Limit 1;
$stmt = $dbh->prepare($presql);
//http://www.php.net/manual/en/pdostatement.bindparam.php
$stmt->bindParam(":user", "myUserName", "string", 20);
$stmt->bindParam(":pass", "myUserPass", "string", 20);
$result = $stmt->execute(); //return true or false
//fetch type可參考http://www.php.net/manual/en/pdostatement.fetch.php
while( $arr = $stmt->fetch(PDO::FETCH_ASSOC) ){
      //do something
}


一開始先實體化(new)一個PDO物件
建構子可參考官網
PDO::__construct() ( string $dsn [, string $username [, string $password [, array $driver_options ]]] )
這裡我只傳了前三個參數
第一個dsn的定義是

The Data Source Name, or DSN, contains the information required to connect to the database.
In general, a DSN consists of the PDO driver name, followed by a colon, followed by the PDO driver-specific connection syntax. Further information is available from the PDO driver-specific documentation.

簡單來說要清楚描述欲連結的DB細節
有三種描述方式
在此我採用Driver invocation
dsn = mysql:host=localhost;dbname=db
接下去的兩個參數是登入的帳號跟密碼

再來是設定db handler的參數
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
在此設定了如有錯誤時會觸發Exception例外事件

接下來開始寫預備SQL敘述
官方也明確定義了何謂Prepare sql statement

Prepares an SQL statement to be executed by the PDOStatement::execute() method. The SQL statement can contain zero or more named (:name) or question mark (?) parameter markers for which real values will be substituted when the statement is executed.

在prepare sql內可包含0或多個命名點或問號,這些都會在被執行時被取代為一個實際的值
在此例我們將username和password各當作一個命名點
並在之後使用bindParam將命名點與實際的值組合起來
bindParam(":user", "myUserName", "string", 20);
在組合時即可明確定義該變數的型態為string並且長度為20
因此不管前端傳來任何奇形怪狀的字串(例如: ',;,--)都會被視為字串,而不會被當成sql語句的一部分!


最後再用execute來執行此SQL語句,並使用fetch function取得結果
就完成一次query了!


在網路上其實比較多用問號來取代變數的方式
但其實我覺得這樣會大大降低程式可讀性
尤其是在INSERT時,變數一多
想修改中間某個值,還要數是第幾個問號
實在浪費太多時間
在參考完PHP.net範例之後決定使用命名的方式瞜!


以上只是一個簡單的例子
建議各位熟悉操作以後
可以包成自己的class
像我就自己寫了一個class
把一些例行性的行為寫進function內
像是連接資料庫就寫一個connect()
這樣讓我節省了不少行code也節省了很多時間呢!

不過要特別注意!
PDO只能避免SQL Injection
XSS和其他的攻擊行為還是要特別小心喔!!

沒有留言:

張貼留言