前言
幾年前手上的一個維護客戶的網站出現了後台回應時間過長的問題,後來使用 New Relic 去查看到底是慢在哪邊,發現原來使用了一個列印訂單 PDF 的外掛,這個外掛有使用 PHP Session (以下稱 PHP 工作階段) 來記錄相關資料,而在啟用 PHP 工作階段時呼叫了 session_start() 這個函式,當初判斷可能是工作階段鎖定 (Session Locking) 的問題,將使用 PHP 工作階段的程式碼改寫後,這個狀況就排除了。也因為這次的經驗,讓我去了解到 PHP 工作階段的問題。
後來有個機會幫客戶撰寫了一個外掛,這個外掛需要將瀏覽器網址的參數儲存下來,然後在使用者完成訂單的時候將參數儲存起來。當時為了要決定如何儲存這個網址參數,思考了幾個解決方案,由於 PHP 工作階段在許多主機上無法正常運作 (後面會詳細說明),這個解決方案自然不在我的考慮之中。
這個外掛陸續有客戶來詢問,許多客戶一開始選擇了其他廠商的整合方案,但因為功能總是無法正常運作而改用我開發的外掛 (希望這些開發商會看到這篇文章…)。經過我的研究後發現原來他們都是使用 PHP 工作階段來存取這些參數值,但是 PHP 工作階段在一些專門做 WordPress 代管的主機環境 (例如:Kinsta 或 WP Engine) 是無法正常運作的,也讓我更加注意到這個問題似乎並不是廣泛地為開發者所知道,更別論是一般使用者了。
WordPress 核心沒有使用 PHP 工作階段
WordPress 本身並沒有使用 PHP 的工作階段來紀錄使用者狀態,而是依賴 cookie 來做這件事,雖然我們可以從很多其他文章中知道 PHP 工作階段的一些問題,但當初為何這麼決定的討論似乎已經不可考。另外像是熱門的電子商務外掛 WooCommerce 也沒有使用 PHP 工作階段,而是設計了一個自己的 工作階段處理機制。因此,身為開發者的你在思考解決方案時,也務必將此考慮進去。
如何使用 PHP 工作階段
工作階段的實作有很多種,不同的程式語言,甚至是不同的程式語言框架 (Framework) 都會有不同的實作方式。 過去我們在撰寫 PHP 應用程式時,為了要記錄使用者的狀態和資料作為下一次連線使用,可能會使用 PHP 的工作階段來實作這樣的功能。
使用 PHP 工作階段的方式很簡單,首先需要先呼叫 session_start()
<?php
session_start();
接著在你就可以開始設定或是存取工作階段內容
<?php
$_SESSION['my_name'] = 'yucheng'; //設定 session
$my_name = $_SESSION['my_name']; //存取 session
echo 'My name is:' . $my_name; //顯示 session 內容
PHP 工作階段的問題
使用 PHP 的工作階段有不少問題,我們條列如下:
安全性問題
PHP 工作階段預設會將資料以檔案的形式存放在主機上,當惡意程式取得主機權限時,可以很容易地存取這些檔案來獲得使用者的機密資料。
同時由於 PHP 工作階段會產生一個 PHPSESSID 的 cookie 存放在瀏覽器中,駭客可透過猜測、竊取等手法取得這個 ID 來偽裝成使用者,更多關於工作階段安全性可以參考 戴夫寇爾 針對工作階段安全性防護的介紹。
既然 cookie 有可能被竊取,同樣的 WordPress 產生的 cookie 也有可能被竊取,不過 WordPress 有一些其他防範的措施,這個我們留待之後的文章來探討。
效能和快取問題
由於 PHP 工作階段會針對不同的使用者來產生出一個唯一的識別 ID (PHPSESSID) 存放在使用者的瀏覽器 cookie 中,同時透過這個識別 ID 來讓伺服器知道要回傳什麼頁面資料給使用者。因此如果你在每個頁面都使用 PHP 工作階段,可能會使得你的快取頁面失效,或是比較糟糕的情況,會讓每個使用者發出的頁面請求,都建立一個伺服器頁面快取。當你的快取功能失效,會導致使用者發出的每一個請求,都落到伺服器和資料庫身上,而影響網站效能。
分散式架構環境的問題
預設的 PHP 工作階段是將資料存放在伺服器的檔案上,若你的主機架構有多台伺服器,那麼 PHP 工作階段可能無法正確運作。
試想,當使用者第一次連線時,連線到伺服器 A ,並在伺服器 A 上建立了一個 PHP 工作階段,而再次連線時連線到伺服器 B,這時候因為找不到之前在伺服器 A 上存放的資料,因此將同一個使用者的連線判斷為新的使用者連線,而顯示了不同的畫面,相信這個使用者一定會感到無比疑惑。如果你希望在多伺服器架構下使用 PHP 工作階段,可以使用 WP Session Manager 這個外掛,來將 PHP 工作階段改存放在資料庫中。
而 WordPress 因為是透過 cookie 來記錄使用者狀態,因此可以相對容易地部署在多伺服器的分散式架構,而不用處理惱人的 工作階段同步問題,這或許也是當初在設計時不使用 PHP 工作階段的原因之一吧。
PHP 工作階段在主機的執行限制
如果你的網站有使用到 PHP 工作階段,你的網站部分功能可能無法正常運作,這是因為許多主機商都建議避免使用 PHP 工作階段,甚至這樣的功能無法正常在主機上運作:
- Pantheon:Pantheon 在官方文件上提到,若要在 Pantheon 平台上使用 PHP 工作階段,需要另外安裝 WordPress Native PHP Sessions 這個外掛。
- Kinsta:在這篇文章中提到,”We don’t recommend using PHP sessions and they will usually not work in our Kinsta environment.” (我們不建議使用 PHP 工作階段, 而且通常 PHP 工作階段無法在 Kinsta 的環境中正常運作)。
- WP Engine:在這篇文章中提到,如果外掛中有使用 PHP 工作階段,請確認是否有更新,如果沒有,建議尋找替代方案。
- WP Engine:在禁止使用的外掛中,提到了 Digital Access Pass 這個會員系統外掛,因為使用了 PHP 工作階段,所以無法在 WP Engine 上正常運作。
- 10up:在開發者最佳實踐中提到,避免使用 PHP 工作階段,同時提到 WordPress VIP 在程式碼審查 (Code Review) 時會禁止使用 PHP 工作階段 (可以在 Automattic 的 VIP Coding Standard 中看到相關的 Rule Set )
- Litespeed Cache 在文件中提到,不支援 PHP 工作階段。如果外掛有使用 PHP 工作階段,建議修改程式改使用 cookie 來記錄狀態。
如何知道網站有使用 PHP 工作階段的外掛或佈景主題
透過瀏覽器來檢查
你可以在 chrome 選單中選擇 [更多工具] > [開發人員工具],或是使用快捷鍵 Ctrl+Shift+I (Windows) 或 Cmd+Opt+I (Mac) 來開啟開發人員工具。
在應用面板 (Application) 中可以檢查是否有 PHPSESSID 這個 cookie 存在。如果有,代表這個網站中有外掛或是佈景主題有使用到 PHP 工作階段。
使用文字編輯器來檢查是哪一個外掛或佈景主題使用 PHP 工作階段
透過瀏覽器只能檢查網站是否有使用 PHP 工作階段,若要明確知道是哪一個外掛或佈景主題使用 PHP 工作階段,你可以使用熟悉的文字編輯器或是程式碼編輯器,例如 Sublime Text 或 VS Code 來搜尋 session_start 字串。
結論
PHP 工作階段是過去許多開發者常使用來紀錄使用者狀態的方式之一,不過實際上很多程式語言框架都因為許多因素而選擇不使用,或是另外實作自己的工作階段處理程式 (例如 Laravel 的 Session )。
如果你發現你的 WordPress 網站有使用到 PHP 工作階段,建議你尋找其他替代方案,避免程式功能無法正常運作或是造成快取失效而影響網站效能。如果你是 WordPress 開發者,在思考儲存使用者狀態或是資料的解決方案時,相信你會有其他更好的選擇。
參考資料:
- Cookies and PHP Sessions (WP Engine)
- WordPress Cookies and PHP Sessions – Everything You Need to Know (Kinsta)
- 10up Engineering Best Practices: Avoid Sessions
- WordPress and PHP Sessions (Pantheon)
- LiteSpeed Troubleshooting Guide:PHP Session Issues
- WordPress Doesn’t Use PHP Sessions, and Neither Should You
- WooCommerce Developer Blog: New session handler in 2.5
請教大大,除了 cookie 之外,使用 Transients API 會是推薦的做法嗎?
Transients 也是一個可以採取的策略,不過還是要看你的使用情境再去決定要採用哪一種方式。
我的情境在購物車,分為三個步驟三個頁面,每個步驟會根據先前頁面輸入資料的不同的而顯示不同的內容,然後最後送出的時候需要把前面使用者輸入的資料一併傳送,這種情況下是否用 cookie 就能解?因為我查 Transients 好像都是用在快取外部 API 的查詢結果…
如果是這個情境,應該還是用 cookie 比較適合。如果是用 Transients, 那麼你必須要想辦法讓 Transient name 能夠對應到該使用者。這邊有相關的討論 https://wordpress.stackexchange.com/questions/105249/are-transients-private-or-public
https://wordpress.stackexchange.com/questions/168861/how-to-make-use-of-transient-api-as-cookie
我以前好像也有想過, 要怎麼對應。原本想說用使用者 IP 來當作 Transient name 的一部分, 但後來覺得太容易出錯了,就不採用這個解決方案。如果你有好的想法也可以建議一下 :)
Transients 比較適合用在每個使用者共用結果的情境, 例如你提到的 API 查詢, 如果參數一樣, 那麼 A 使用者透過 Transients 拿到的資料, 和 B 使用者透過 Transients 拿到的資料會是一樣的。
請教一下
那麼替代方案是什麼呢?
我目前實做的網路商店就是利用session去判斷使用者的
但我也對安全有所顧慮
WordPress 的登入是用 cookie 來實作的,你可以參考一下 WordPress 的做法喔!