2020年3月26日 星期四

Vigor2960 使用者設定檔 - 使用 Active Directory Server 認證

這一篇是如何使用2960搭配AD/LDAP做VPN帳號認證連線一文的補充說明,或許是因為居易希望能夠同時適用 AD/LDAP,因此說明文件中對於 AD (有自己獨特的定義) 的設定說明實在是不夠清楚。


名詞解釋


屬性:

1. 名稱 (Common Name, CN):使用者名稱 (例如,John Smith) 或伺服器名稱,AD 使用的是 CN 而不是下列的 UID;
2. 使用者識別碼 (User ID, UID):使用者登入識別碼 (例如,jsmith) ,通常是 CN 簡化的識別碼;
3. 組織名稱 (Organizational Unit, OU):組織內單位的名稱 (例如,IT);
4. 組織 (Organizational, O):組織的名稱 (例如,ABC.com);
5. 網域元件 (Domain Component, DC):將組織的網域名稱拆成個別的網域元件 (例如,ABC 跟 com),AD 使用的是 DC 而不是 O;
6. 國別 (Country, C):國家名稱,一般較少使用;
7. 相對識別名稱 (Relative Distinguished Name, RDN):是跟目錄樹結構無關的部份,通常 RDN 的值會是 CN 或是 UID;
8. 識別名稱 (Distinguished Name, DN):用來代表一個 LDAP 物件的名稱 / 路徑的絕對位置,每一個 DN 都是由 RDN 跟路徑所組成。



屬性名稱對於大小寫字母沒有區別,但是一般在使用時屬性名稱會使用小寫字母。



屬性的定義:

屬性 (Attribute) = 值 (value),

例如,ou=IT、dc=ABC、或是 dc=com 等。



Vigor2960 AD 認證設定


在 "單一帳號/活動目錄管理" 設定畫面中,有下列幾個項目需要設定:

1. 綁定方式,分為:

1) 匿名模式:"使用 anonymous 匿名帳號登入 AD/LDAP 查詢";網域控制站 (Domain Controller, DC) 應該是沒有在接受匿名帳號查詢的,這個選項不適用於 AD 環境。

2) 簡易模式:"使用空白帳號登入 AD/LDAP 查詢";DC 應該是沒有在接受空白帳號查詢的,這個選項不適用於 AD 環境。

3) 常規模式:"使用指定帳號登入 AD/LDAP 查詢(需設定 Regular DN 和密碼)",只有這項是適用於一般的 AD 環境。可是居易對於這項設定的說明幾乎是沒有。

所以,AD 環境下只能夠選用常規模式


2. 伺服器IP位址:
AD/LDAP 的伺服器 IP,在 AD 環境具有全域目錄 (Global Catalog, GC) 功能 DC 的 IP。


3. 通用名稱識別符:
以什麼屬性做為帳號比對的依據,常見的有 cn 和 uid;AD 使用的是 cn 而不是 uid。



4. 基本識別名稱 (Base DN):
"從哪個基本網域名稱路徑開始查詢認證的帳號",

要從目錄樹狀結構的那裡開始查詢認證的帳號,一般會是 dc,例如,dc=ABC, dc=com;如果認證的帳號全部屬於某個組織單位,也可以加上 ou 成為 ou=IT, dc=ABC, dc=com。



5. Group DN (非必要):
檢查所要認證的帳號是否有在指定的群組內。



6. Regular DN:
"輸入指定的帳號路徑",

這個部份是最難的部份,因為我們通常不會記得 AD 物件的 DN。不過,透過在 DC 的命令提示字元下達 dsquery user -samid 命令可以得到物件的 DN。例如,要利用 AD 上的 VPNusr 這個帳號 (需要先在 AD 上建立這個帳號) 來進行認證查詢,可以利用下列的命令來查詢其 DN:

dsquery user -samid "VPNusr"


可以得到

"cn=VPNusr,ou=IT,dc=ABC,DC=com"


再將查到的 DN (去除隻引號) 填入 Regular DN 欄位。


7. 常規密碼:

認證查詢的帳號 (例如,上例的 VPNusr) 的密碼。



在完成上述的設定後,在 "基本識別名稱" 旁邊會出現按鈕,點進去可以看到 Base DN 下的樹狀結構,如果看不到,表示上面的設定有錯誤。


另外,Vigor2960無法顯示有中文的 DN,當組織單位等屬性有使用中文時,會查不到使用者。









2020年3月16日 星期一

Windows Server 2008 R2 Event Log 無法啟動

事由:使用者無法登入,DNS無法查詢。在登入 DC (Windows Server 2008 R2) 後,在開啟事件檢視器時出現:Event Log 服務未啟動。到 services.msc 啟動 Windows Event Log 出現:Windows 無法啟動本機的 Windows Event Log 服務,錯誤5:存取被拒絕。


解法:

1. 把 C:\Windows\System32\winevt\Logs 裡的檔案先移到其他資料夾;

2. 重設相關的權限 (參考自 https://support.microsoft.com/en-us/help/2751670/we-are-seeing-an-error-where-we-are-unable-to-access-the-security-log):

1) 檢查 C:\Windows\System32\winevt\Logs 的 NTFS 權限 (permissions),eventlog 使用者需要有完全控制的權限;

2) 執行 regedit,檢查 HKLM\SYSTEM\CurrentControlSet\Services\EventLog\Security 的權限,移除 eventlog 的權限;

3) 新增 "本機" 的 "NT service\EventLog" (就是 eventlog ) 的讀取權限到 HKLM\SYSTEM\CurrentControlSet\Services\EventLog\Security;

3. 重新啟動  Windows Event Log 服務。


備註:
因為步驟 1 跟 步驟 2 是同時做的,所以無法驗證是不是只要做步驟 1 或是步驟 2 即可。

在 httpd 上使用 PHP-FPM

在撰寫本文時,使用的是利用 homebrew 安裝的 PHP 7.4.3 (含內建的 PHP-FPM) 以及 httpd 2.4.1。PHP-FPM 適用於重負載 (Heavy-Loaded) 的網站,對於一般在本機上開發 PHP 的開發者,並不一定需要使用 PHP-FPM。通常 PHP-FPM 會配合對於多執行緒處理較佳的 Nginx,而非 httpd。在閱讀本文前,建議先閱讀 PHP 與伺服器之間的應用程式介面以及在 Apache HTTP Server 的多程序處理模組


基礎常識


PHP-FPM (FastCGI Process Manager) 是 PHP 實作快速共用閘道介面 (Fast Common Gateway Interface, FastCGI) 的管理套件。FastCGI 是一個加強版的 CGI 協定,它將 CGI 應用程式包裝起來,利用 FastCGI 伺服器來建立程序以及管理從網頁伺服器傳送來的請求。它不僅保留了 CGI 的好處,而且還提升了 CGI 的性能,適合應用於重負載的網站。


根據 PHP 手冊,PHP-FPM 具有下列的特性

1. 能夠優雅起動或停止的先進程序管理;
2. 能夠根據不同的使用者 ID (uid) / 群組 ID (gid) / 根目錄更改 (chroot) / 環境 (environment) 等啓動工作者 (Worker) 子程序,監聽不同的通訊埠以及使用不同的 php.ini;
3. 標準串流 stdout 以及 stderr 記錄;
4. 操作碼 (Opcode) 快取意外損壞時緊急重新啟動;
5. 支援加速上傳;
6. 慢速記錄 (slowlog):記錄執行異常緩慢的腳本,包含腳本名稱以及 PHP 回溯 (Backtraces),使用 ptrace 系統呼叫或是類似的機制去讀取遠端程序的執行資料;
7. 提供特殊的 fastcgi_finish_request() 函式,能夠在完成請求以及刷新全部資料時,繼續執行耗時的操作 (例如,影音轉檔、統計處理等);
8. 動態或靜態的子程序生成;
9. 和 Apache 模組狀態類似的基本 SAPI 狀態資訊;
10. 以 php.ini 為基礎的組態檔。


安裝


使用 Homebrew 安裝的 PHP 已經包含了 相同版本的 PHP-FPM。

設定 httpd.conf


PHP-FPM 大多使用在重負載 (Heavy-Loaded) 的網站上,因此建議 httpd 的多程序處理模組 (Multi-Processing Module, MPM) 使用 Event 模組 (停用預設的 Prefork 模組)。需要開啟 httpd.conf 的代理 (Proxy) 模組以及 FastCGI 代理模組,讓 httpd 能夠將請求轉傳到 FastCGI 伺服器處理。httpd.conf 需要進行下列的組態設定
LoadModule mpm_event_module lib/httpd/modules/mod_mpm_event.so
LoadModule proxy_module lib/httpd/modules/mod_proxy.so
LoadModule proxy_fcgi_module lib/httpd/modules/mod_proxy_fcgi.so


設定 PHP 檔案的處理器,讓副檔名為 php 的請求傳送到 PHP-FPM 伺服器:
<IfModule mpm_event_module>
    <FilesMatch \.php$>
        SetHandler "proxy:unix:/usr/local/var/run/php/php-fpm.sock|fcgi://localhost/"
    </FilesMatch>
    AddType text/html .php
</IfModule>


上面的組態設定使用了 FilesMatchsetHandler 兩個指令將 PHP 檔案傳送到指定的 Unix 插座 (Socket)。其中,FilesMatch 透過指定的檔案名稱來限制區塊內指令的適用範圍,它可以使用規則運算式 (Regular Expressions) 來表示檔案名稱;setHandler 指定代理要傳遞請求到那一種插座 (Socket),以及插座的識別碼。


PHP-FPM 和 httpd 之間的通訊可以使用網路插座或是 Unix 插座。Unix 插座是一種程序間的通訊機制,它允許在同一台電腦上的兩個程序可以同時開啟一個 Unix 插座來進行數據交換。它使用檔案的位址作為識別碼 (例如上面的 /usr/local/var/run/php/php-fpm.sock),通訊在系統核心進行處理。網路插座是另一種程序間的通訊機制,它允許兩個程序通過同一網路插座來進行數據交換。它使用網路位址加上通訊埠作為識別碼,通訊經由網路堆疊處理。使用網路插座的兩個程序可以是不同電腦上的程序,也可以是同一個電腦上經由 IP 繞回 (Loopback) 介面通訊的兩個不同程序。網路插座透過網際網路協定通訊,可以跨電腦通訊,使用上較為靈活但是額外的處理成本較高,Unix 插座則是相反。


上面的組態檔使用的是 Unix 插座。如果要用網路插座,則處理器的設定要改為:
setHandler "proxy:fcgi://127.0.0.1:9000"


其中,127.0.0.1 為本機的繞回位址,如果 PHP-FPM 跟 httpd 在不同的主機,則要改成 PHP-FPM 主機的 IP 位址;9000 為 PHP-FPM 的監聽埠。


除了使用上述的 setHandler 來進行通訊之外,還可以使用 ProxyPassMatch 或是 ProxyPass。除非有特殊的需求,建議使用FilesMatch 和 setHandler 即可。

PHP-FPM 組態檔 php-fpm.conf


php-fpm.conf 設定組態選項的語法跟 php.ini 一樣。預設的路徑前綴為 /usr/local/var,需要注意的組態選項只有 error_log (其他建議使用預設值即可):
error_log = log/php-fpm.log


注意:
1. 這項設定僅會記錄 PHP-FPM 本身的錯誤訊息;PHP 腳本錯誤會記錄在 php.ini 的 error_log 或是程序池組態檔的 error_log。
2. 可以變更記錄檔的路徑;如果使用 brew services 來啟動 PHP-FPM,記得要修改 /usr/local/opt/php/homebrew.mxcl.php.plist 中 StandardErrorPath 下的字串值,不然每次 brew services 在重啟 PHP-FPM 時,預設的檔案夾內還是會再產生記錄檔。
3. 當啓動 PHP-FPM 時出現類似下列訊息,代表組態檔裡的錯誤記錄指令沒有設定好,或是啟動的是 macOS 內建的 PHP-FPM:
ERROR: failed to open error_log (/usr/var/log/php-fpm.log): No such file or directory (2)



在啟動 PHP-FPM 時需要至少建立一個主程序 (Master Process),每個主程序則會建立多個稱為工作者 (Worker) 的子程序來回應及處理請求;這些子程序會形成一個 PHP 程序池 (A Pool of PHP Processes)。為了方便管理,PHP-FPM 將建立程序池所使用的組態檔獨立出來。程序池組態檔通常會存放在 php-fpm.d 檔案夾裡。在 php-fpm.conf 的最後一行設定會將組態檔包含進來:
include=/usr/local/etc/php/7.4/php-fpm.d/*.conf


會使用 *.conf 是因為 PHP-FPM 的特性之一是可以有多個程序池,以用來處理不同的請求 (例如,當 httpd 有多個虛擬主機時,每個主機可以有自己的 PHP 程序池)。讓每個程序池有自己的組態檔,可以方便進行組態設定。

程序池組態檔 pool_name.conf


PHP-FPM 會依據程序池組態檔的設定值來建立 PHP 程序池。程序池組態檔的檔案名稱一般為程序池名稱加上 conf 副檔名。例如,在 php-fpm.d 檔案夾裡有一個  www.conf  程序池組態檔,它的程序池名稱就是 www。一般在建立程序池組態檔時都會藉由複製或是修改 www.conf 來完成。程序池組態檔需要設定的組態選項有下列幾項:
[userName]

user = userName
group = userGruop
listen = /usr/local/var/run/php/php-fpm.sock
php_admin_value[error_log] = /usr/local/var/log/php-fpm/$pool.error.log


首先,如果要根據不同的 uid 跟 gid 建立程序池,可以利用使用者名稱做為程序池的名稱。監聽 (listen) 的設定需要和 httpd.conf 配合,上例為使用 unix 插座時的檔案位置。如果是使用網路插座,則要更改為下列的設定:
listen = 127.0.0.1:9000


每個程序池可以有自己的錯誤記錄檔。在啟用程序池的錯誤記錄檔前,需要先啟動 log_errors 指令,php.ini 的 error_log 的組態設定將會被取代。最後,更改完後記得將檔名更改為 userName.conf。


在程序池設定檔中有 listen.* 指令用來設定監聽的權限,因為 macOS 是 BSD 衍生系統,所以不用進行設定。另外,程序管理員 (pm) 的相關組態選項,則需要依照使用情況進行調整,不在本文的範圍內。


PHP-FPM 的特性之一是能夠根據不同的 uid/gid/chroot/environment 等啓動工作者 (Worker) 子程序,監聽不同的通訊埠以及使用不同的 php.ini。要讓 PHP-FPM 同時執行多個 PHP 程序池,可以透過多個的程序池組態檔進行設定。


啓動 PHP-FPM


在使用 Homebrew 安裝完 PHP後,因為是伺服器的關係,會在 /usr/local/sbin 檔案夾建立 php-fpm 的符號連結 (Symbolic Link)。/usr/local/sbin 一般並不在 PATH 環境變數中,再加上 macOS 也有內建 php-fpm,如果直接執行 php-fpm 會出現下列的錯誤:
ERROR: failed to open configuration file '/private/etc/php-fpm.conf


透過下列的指令,可以知道執行的並不是 /usr/local/sbin/php-fpm
which php-fpm


因此執行 PHP-FPM 時,需要加上完整的路徑:
/usr/local/sbin/php-fpm


因為 PHP-FPM 預設會在前景執行 (使用的是 nodaemonize 模式),要停止 PHP-FPM 只要在終端機按下 control + c 即可。


比較方便的做法是使用 brew services,將 PHP-FPM 設為登入後自動執行的任務項:
brew services start php


當修改過相關的組態檔,需要重新啓動 PHP-FPM 時,只需要執行:
brew services restart php


2020年3月12日 星期四

PHP 的組態設定

在撰寫本文時,使用的是利用 homebrew 安裝的 PHP 7.4.3 (以及 PHP 內建的 PHP-FPM)、httpd 2.4.1。

提到 PHP 的組態設定,第一個想到的一定是 PHP 的初始化組態檔 -- php.ini,它負責 PHP 各種指令 (Directives) 的預設組態設定。 但是除了 php.ini 之外,PHP 還提供了許多組態設定的方式:當 SAPI 為 Apache 2.0 Handler 時,使用者可以透過 httpd 組態檔 httpd.conf 以及每個檔案夾下的超文件取存 (Hypertext) 設定檔 .htaccess 進行組態設定;當 SAPI 為 CGI/FastCGI 時,從 PHP 5.3.0 開始可以透過使用者組態檔 .user.ini (user_ini.filenamephp.ini 的預設值) 對每一個檔案夾進行組態設定;在執行時期則可以使用組態設定函式 ini_set() 在腳本中進行組態設定。

雖然這些方式都可以進行組態設定,但是可以設定的內容卻不盡相同。PHP 將組態設定指令區分為 5 類 (針對 PHP_INI_* 分成 4 類,在指令列表中多了php.ini only),下表列出了這 5 類可以設定的方式:

分類設定方式
PHP_INI_USER 指令可以在使用者的腳本 (使用 ini_set()) 或是在 Windows registry 設定。從 PHP 5.3 開始,指令可以在 .user.ini 設定。PHP 5.0 開始沒有指令屬於這個分類。
PHP_INI_PERDIR 指令可以在 php.ini.htaccesshttpd.conf 或是 .user.ini (從 PHP 5.3 開始) 設定。
PHP_INI_SYSTEM 指令可以在 php.ini 或是 httpd.conf 設定。
PHP_INI_ALL 指令可以在所有的地方設定。
php.ini only 指令只可以在 php.ini 設定。


PHP 組態設定的指令非常的多,如果有需要可以參考指令列表的介紹。


初始化組態檔 php.ini


當 PHP 啟動時,初始化組態檔 php.ini 會被讀取,並且完成相關的指令設定。在 PHP 與伺服器之間的應用程式介面一文中介紹過:
1)使用 SAPI 模組 (Apache 2.0 Handler) 時,PHP 會在網頁伺服器的程序中執行,不需要針對每個請求啟動一個新的程序,所以 php.ini 只會在網頁伺服器啟動時被讀取一次;但是當更改組態檔的內容後,需要重新啟動網頁伺服器才會生效。
2) 使用 CGI 時,每次收到新的請求時,都要啟動一個執行 php-cgi 的程序,讀取 php.ini;所以在更改組態檔內容後,有新的請求就會生效。
3) 使用 FastCGI 時,FastCGI 伺服器會建立一個主要程序以及多個工作者子程序,不需要針對每個請求啟動一個新的程序,所以php.ini 只會在 FastCGI 伺服器啟動時被讀取一次;更改完組態檔內容後,需要重新啟動 FastCGI 伺服器才會生效。
4) 使用 CLI 時,每次收到新的請求時都要讀取 php.ini;在更改組態檔內容後,有新的請求就會生效。


另外,在使用 CGI/FastCGI 時,如果要對全網站或是部份檔案夾進行設定,分別要在 php.ini 中的 [HOST=www.example.com] 或是 [PATH=/www/mysite] 區塊進行設定。布林 (boolean) 組態指令的開啟值可以是 1、On、True 或是 Yes;關閉值可以是 0、Off、False 或是 No。

httpd 組態檔 httpd.conf 以及 .htaccess


當 PHP 做為 httpd 的模組使用時,使用者可以透過 httpd.conf 以及 .htaccess 進行組態設定。要能夠利用這兩個檔案設定組態,使用者還需要對檔案夾開啟 "AllowOverride Options" 或是 "AllowOverride All" 權限。httpd.conf 可以設定 PHP_INI_ALL、PHP_INI_PERDIR、以及 PHP_INI_SYSTEM 等類型的指令;.htaccess 則只能夠設定 PHP_INI_ALL 以及 PHP_INI_PERDIR。在 httpd.conf 更改 PHP 組態設定需要重啟 httpd,.htaccess 則是在更改組態設定後,有新的請求就會生效。

httpd.conf 可以使用下列四項指令進行組態設定:


指令用途說明
php_value name value用來設定指令的數值。PHP_INI_ALL、PHP_INI_PER。不能用來設定布林組態指令。
php_flag name on|off用來設定布林組態指令。PHP_INI_ALL、PHP_INI_PER。
php_admin_value name value用來設定指令的數值。不能夠在 .htaccess 中設定,任何指令使用php_admin_value 設定後,不能夠被 .htaccessini_set() 覆寫;要清除先前的設定值,值要設為 none。
php_admin_flag name on|off用來設定布林組態指令。不能夠在 .htaccess 中設定,任何指令使用php_admin_value 設定後,不能夠被 .htaccessini_set() 覆寫。


設定範例 1 (httpd.conf):

<ifmodule mpm_prefork_module>
    LoadModule php7_module /usr/local/opt/php/lib/httpd/modules/libphp7.so
    AddHandler application/x-httpd-php .php
    AddType text/html .php
    php_flag display_errors Off
</ifmodule>


上面的設定會將錯誤顯示更改為不顯示 (display_errors Off)。

設定範例 2 (.htaccess):

php_flag display_errors On
php_value error_reporting 32759


範例 1 在 httpd.conf 中使用 php_flag 而不是 php_admin_flag,所以範例 2 可以在 .htaccess 中改寫錯誤顯示的設定為開啟。另外,PHP 常數只可以在 PHP 內使用,無法在 PHP 外 (例如,httpd.conf 或是 .htaccess) 使用。如果要在這兩個檔案使用,必需要使用位元遮罩 (Bitmask) 數值代替。這裡的 32759 代表的是 E_ALL & ~E_NOTICE。

PHP-FPM 組態檔 php-fpm.conf 以及程序池組態檔



適用於 PHP-FPM 的組態檔,設定的方式同 php.ini,詳細的設定請參考

使用者組態檔 .user.ini


從 PHP 5.3.0 開始,當 SAPI 為 CGI/FastCGI 時,PHP 支援基於每個檔案夾的使用者組態 .user.ini;同時將原有 PECL 的 htscanner 延伸套件廢棄。因為每個請求都會由網頁伺服器轉交給 FastCGI 伺服器 (例如,PHP-FPM) 處理,.user.ini 的組態設定方式和 php.ini 相同。當使用 CGI/FastCGI 時,不可以再使用 php_value、php_flag、php_admin_value、以及 php_admin_flag 進行設定,因為這 4 個組態設定指令是專門提供給 PHP 模組 (Apache 2.0 Handler) 使用的,而當採用 CGI/FastCGI 時,httpd 就不會再載入這個模組。

如果檔案夾中的 .htaccess 中有使用 php_value 或是 php_flag,在瀏覽時該檔案夾內的網頁時會出現 500 Internal Server Error。在 httpd 的 error log 則會出現:

/temp/.htaccess: Invalid command 'php_flag', perhaps misspelled or defined by a module not included in the server configuration


同樣地,當沒有使用 PHP 模組時,不可以在 httpd.conf 使用 php_value、php_flag、php_admin_value 或是 php_admin_flag 進行組態設定。



注意:
1. .user.ini 存放在公開的檔案夾中,它的內容可以被使用者讀取,不要在檔案內進行敏感性的設定。建議可以在 .htaccess 禁止使用者讀取:
<Files ".user.ini">
    Require all denied
</Files>


2. PHP-FPM 的程序池裡的工作者程序需要一小段時間才能夠全部都讀取到重新設定後的 .user.ini (user_ini.cache_ttl 的預設值是 5 分鐘),如果要設定值立刻生效,可以重新啟動 PHP-FPM。

組態設定函式 ini_set()


使用組態設定函式 ini_set() 進行組態設定是最靈活,但是可以設定的指令最少的方式。在腳本中使用 ini_set() 對特定的組態選項進行設定後,組態選項將會在腳本執行期間保留此新值,並且在腳本結束後恢復原來的值。因此可以根據腳本執行上的需求,對每個腳本進行不同的組態設定。ini_set() 只能夠設定 PHP_INI_ALL 類型的指令 (因為唯一的 PHP_INI_USER 指令 tidy.clean_output 從 PHP 5.0 開始變成了PHP_INI_PERDIR 了)。

ini_set() 的函式定義如下:
ini_set ( string $varname , string $newvalue ) : string


如果設定成功會傳回設定選項的舊值,失敗則會傳回 FALSE。

範例 3:
<?php
declare(strict_types = 1);

echo ini_get('display_errors');
if (!ini_get('display_errors')) {
    ini_set('display_errors', '1');
}
echo ini_get('display_errors');
ini_set('error_reporting', '32759'); //32759 為 E_ALL & ~E_NOTICE 的位元遮罩值


範例 3 會使用 ini_get() 檢查錯誤顯示的設定,並且設定為顯示錯誤資訊,方便進行程式除錯。


注意:因為用 ini_get() 的參數宣告為 string,當使用強型別 (declare(strict_types = 1);) 時,'E_ALL'、'E_WARNING'、'E_NOTICE' 等整數常數會被當成是字串傳入;此時只能夠使用數值進行組態值設定。若要使用常數設定 error_reporting,可以使用 error_reporting() 函式:
error_reporting(E_ALL & ~E_NOTICE);


2020年3月4日 星期三

物件導向 PHP 與 MySQL (MariaDB) - 1. 使用 MySQLi 進行連線與簡易的 CRUD 操作


使用 MySQLi 進行連線與簡易的 CRUD 操作

本文將介紹如何以物件導向的方式使用 MySQLi 延伸套件連結 MySQL (MariaDB) Server,並進行簡易的資料新增、查詢、更新、刪除等操作。


建立連線

PHP 可以使用 mysqli 類別建立與 MySQL Server 間的連線,要實例化一個 mysqli 物件需要依序提供:MySQL Server 的 IP、資料庫的使用者名稱、使用者密碼、資料庫的名稱、MySQL Server 的連接埠、以及要使用的插座 (Socket) 或是具名管道 (Named Pipe);後兩者一般都是使用預設值即可。

下列的程式碼為物件導向 PHP 使用 mysqli 來建立與 MySQL Server 間的連線:
$dbHost = 'localhost';
$dbUser = 'db_username';
$dbPassword = 'db_user_password';
$dbName = 'db_name';
$connect = new mysqli($dbHost, $dbUser, $dbPassword, $dbName);
if ($connect->connect_errno) {
    echo "Failed to connect to MySQL: (" . $connect->connect_errno
             . ") " . $connect->connect_error . PHP_EOL;
}
echo $connect->host_info . PHP_EOL;


CRUD

在建立了跟資料庫伺服器的連線後,就可以對選定的資料庫進行資料的建立 (Create)、讀取 (Read)、更新 (Update)、以及刪除 (Delete) 等操作;這四項資料操作取其首字母,而被簡稱為 CRUD。MySQL 是關聯式資料庫管理系統 (Relational DataBase Management System, RDBMS),使用結構化查詢語言 (Structured Query Language, SQL) 進行管理。SQL 使用資料操作語言 (Data Manipulation Language, DML) 來處理資料庫中資料表裡的資料,這些操作對應到 CRUD 分別是 INSERT、SELECT、UPDATE、以及 DELETE。

以 PHP 開發的應用中,查詢敘述通常是動態產生的 (例如,使用者在表單上輸入要查詢的資料),如果沒有妥善的處理查詢敘述,非常容易讓惡意的使用者以 SQL 隱碼攻擊 (SQL Injection) 的方式,破壞或是入侵資料庫系統。mysqli 提供了預備敘述,讓輸入參數化以避免 SQL 隱碼攻擊。

INSERT

下例會在資料表中新增資料,假設資料 (姓名、電子郵件信箱、住址、以及訊息等) 是由使用者輸入而取得:
$name = '許杰夫';
$email = 'jeff.hsu@gmail.com';
$zip = 10001;
$address = '台北市中正區中正路1號';
$score = 98.9;
$message = '資料新增';

$sql = "INSERT INTO `users` (`name`, `email`, `zip`, `address`, `score`, `message`) 
            VALUES (?, ?, ?, ?, ?, ?)";
$stmt = $connect->prepare($sql);
if (!$stmt) {    
    echo "Prepare failed: (" . $connect->errno . ") " . $connect->error;
}
$stmt->bind_param('ssisds', $name, $email, $zip, $address, $score, $message);
$stmt->execute();
$stmt->close();

為了避免 SQL 隱碼攻擊,需要:
1. 將查詢敘述參數化,mysqli 使用問號 ? 做為佔位符 (Placeholder) 來取代變數名稱;
2. 利用 mysqli::prepare 產生預備敘述 (建立 mysqli_stmt 物件);
3. 使用 mysqli_stmt::bind_param 將變數綁定到預備敘述作為參數,其中第一個參數為變數型別,分別以:i 代表整數、s 代表字串、d 代表雙精度浮點數、b 代表二進位大型物件;
4. 使用 mysqli_stmt::execute 執行綁定參數後的預備敘述;
5. 在查詢完成後使用 mysqli_stmt::close() 將查詢敘述關閉。

SELECT

下例會讀取資料表中第一筆到最後一筆的資料:
$id = $connect->insert_id;
$sql = "SELECT * FROM `users` WHERE `id` <= ?";
$stmt = $connect->prepare($sql);
if (!$stmt) {    
    echo "Prepare failed: (" . $connect->errno . ") " . $connect->error;
} 
$stmt->bind_param('i',$id);
$stmt->execute();

$stmt->bind_result($id, $name, $email, $zip, $address, $score, $message);
while($stmt->fetch()){
    echo $id . " , " . $name . " , " . $email . " , " . $zip
        . $address . " , " . $score . " , " . $message . PHP_EOL;
}
$stmt->close();


在本例中:
1. 使用了 mysqli::$insert_id,傳回最後查詢 (通常是 INSERT) 時自動生成的 ID (當資料表中有 AUTO_INCREMENT 欄位時);
2. 預備敘述的參數綁定與執行;
3. mysqli_stmt::bind_result 將預備敘述執行結果 (結果集合) 中的欄位名稱綁定到指定的變數;
4. mysqli_stmt::fetch 會將結果集合中的資料逐筆對應到由  mysqli_stmt::bind_result 綁定的變數;
5. 每執行一次 mysqli_stmt::fetch,結果集合就會少一筆記錄,直到結果集合成為空集合, mysqli_stmt::fetch 會傳回 false。
6. 在查詢完成後使用 mysqli_stmt::close() 將查詢敘述關閉。

除了上述的輸出方式之外,也可以透過 mysqli_stmt::get_result 將結果集合轉換為 mysqli_result 物件集合後,再使用 mysqli_result::fetch_object 以物件的方式輸出資料。如下列的程式:
$objSet = $stmt->get_result();
while($obj = $objSet->fetch_object()){
    echo $obj->id . " , " . $obj->name . " , " . $obj->email . " , " . $obj->zip
        . $obj->address . " , " . $obj->score . " , " . $obj->message , PHP_EOL;
}

要注意的是,在使用 mysqli_stmt::get_result 後,會將結果集合清空。

UPDATE

下例會更新資料表中的最後一筆新增的資料:
$name = '許班杰';
$email = 'benjamin@test.com';
 
$sql = "UPDATE `users` SET `name` = ?, `email` = ? WHERE `id` = ?";
$stmt = $connect->prepare($sql);
if (!$stmt) {    
    echo "Prepare failed: (" . $connect->errno . ") " 
            . $connect->error;
} 
$stmt->bind_param('ssi', $name, $email, $id);
$stmt->execute();
$stmt->close();


DELETE

下例會刪除資料表中的最後一筆新增的資料:
$sql = "DELETE FROM `users` WHERE `id` = ?";
$stmt = $connect->prepare($sql);
if (!$stmt) {    
    echo "Prepare failed: (" . $connect->errno . ") "
         . $connect->error;
} 
$stmt->bind_param('i',$id);
$stmt->execute();
$stmt->close();
$connect->close();

在所有的查詢工作結束後,記得要將連線關閉,釋放佔用的資源。

小結

本文介紹了如何使用 MySQLi 延伸套件 (包含 mysqli、mysqli_stmt、mysqli_result 等類別),以物件導向的方式操作 MySQL (MariaDB) 資料庫;並進行簡單的 CRUD 操作。使用MySQLi 最大的限制是只能夠對  MySQL (MariaDB) 資料庫進行操作,當資料庫系統轉換到其他的系統時,需要重新撰寫程式碼。在下文中將介紹 PHP Data Object (PDO) 延伸套件,可以避免這個問題。

2020年3月1日 星期日

PHP 開啟 MySQL 的錯誤報告

為了避免敏感的資料庫資訊因為程式碼的錯誤而曝露,PHP 預設不會報告在使用 mysqli 或是 PDO 操作 MySQL 時的錯誤。程式設計人員需要決定是否啟用相關的錯誤報告。 要特別注意的是,當開啟錯誤報告後,不要讓報告內容呈現在一般的使用者前。

MySQLi


mysqli 使用的是mysqli_driver::$report_mode (或是mysqli_report)。例如,如果要在發生查詢錯誤時丟出例外 (但是忽略警告),可以進行下列的設定:
$driver = new mysqli_driver();
$driver->report_mode = MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT;


或是簡單的使用
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

其中,MYSQLI_REPORT_ERROR 代表開啟錯誤報告,MYSQLI_REPORT_STRICT 代表在錯誤時丟出例外 (throw Exceptions for errors);管道符號 | 是位元運算子中的 "或",可以用來設定多個旗標 (bitwise disjunction of flags)。

PDO


PDO 使用的是 PDO::setAttribute。例如,如果要在發生查詢錯誤時丟出例 (但是忽略警告),可以進行下列的設定:
$connection = new pdo("mysql:localhost;dbname=db_name", "user_name", "user_password");
$connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

其中,PDO::ATTR_ERRMODE 代表開啟錯誤報告,PDO::ERRMODE_EXCEPTION 代表在錯誤時丟出例外。