級別: 中級 |
資深程序員, Interpublic Corporation, Department of Global Information Services
2005 年 6 月 16 日
許多 Web 部署的應用程序都是在精心設計的數據庫驅動的服務器端開發框架中編寫的,例如 PHP 和 Java™ servlet,但是對于一些簡單的程序(例如,整個數據庫要能夠存放在 Web 服務器的 RAM 中)來說,使用加鎖的 DMB 文件和 Perl MLDBM
模塊可以很容易地實現數據持久性。本文將給出一個基于 Web 的投票系統的真實的例子,重點介紹如何利用最小的外部模塊、如何舍棄基于客戶機的 cookie 以及如何利用 CGI 屬性的優點。
軟件正日益變得更加復雜,這并不是什么秘密;我們也看到一些額外的層次被添加到系統中,以保持軟件組件的模塊化。最重要的結果是,這些系統現在更易于維護,而且可擴展性也更好;但是有時這些技術有過多的重復,會導致軟件的過度設計。在另外一些情況下,開發開發人員寧愿選擇一些過度復雜但卻非常有名的技術,也不愿意集成一些簡單但卻不太熟悉的技術。
不管怎樣,如果所有人都有一把錘子,那么每個問題看起來都不過像一顆釘子而已。
我最近被請求為一所大學的學生組織設計一個小程序,以統計選舉票數。這是一個非常簡單的項目,每周處理的學生請求不會超過 500 個;之后該程序會立即統計并發布結果。
由于這個項目對服務級別的要求很低,因此使用一個外部數據庫來處理查詢并沒有什么好處。相反,使用腳本可以直接快速讀寫數據結構。不過,我仍然希望能夠將一些經過良好設計的功能封裝在一起,而不是采用一些像意大利面那樣,將雜亂的代碼拼裝在一起。我希望可以采用一個經過仔細考慮的自成體系的設計,該設計將提供一些簡化的部署。
CGI 的考慮:簡單性與復雜性
Perl 看來是這個項目的首選語言 —— 在很多平臺上似乎都受到支持,此外,在 Perl 的知識庫 CPAN 中,還有很多方便的庫可以使用。
對于底層架構來說,CGI(Common Gateway Interface)是第一種廣泛用來擴展 Web 服務器從而提供交互內容的方法。開發人員通常會鼓吹一些新的標準,例如 JSP、.NET、mod_perl、PHP 和 ISAPI,這些技術也的確可以彌補 CGI 的一些不足。但是在這個項目中,我們只需要對幾百個用戶計算投票數,這樣一個 CGI 腳本很難構成一個大型的應用程序,因為所有的投票信息都可以放到 Web 服務器的系統 RAM 中。在用戶每次提交一個讀寫數據的請求時,這可以將要查詢的整個表裝入內存中。
還有,通過將邏輯數據分隔成 3 個不同的物理文件,可以實現填寫選票、確認選票和統計結果的邏輯順序;這樣做可以最大限度地減少打開已加鎖的文件。
如果一個事務在很偶然的情況下因為加鎖的文件而失敗了,那么這并不會產生實際的問題。不管一個事務是由于網絡問題還是加鎖文件而失敗的,結果都是相同的:用戶只需再等待一會兒即可,選票隨后很有可能對其中的一次嘗試進行統計。我們應該記住這種行為,然而,對于不同的應用程序來說,情況并非總是如此,因此可能無法處理并發事務。
對于這個項目來說,CGI 提供了以下幾個優點:
然而,需要記住的是,由于平臺的限制,CGI 程序(它們會創建一些新的進程)在 Win32 的系統上運行速度非常慢。此外,盡管 Apache Web 服務器已經可以在 Windows® 上運行得很好,但是它依然被認為是一個 Linux™/UNIX® 系統上的程序。參考資料 部分提供了有關在 Win32 系統上可以使用的其他(非 IIS) Web 服務器的信息,在最初的 National Center for Supercomputing Applications (NCSA)站點上,還提供了一份 CGI 規范的經典介紹。
功能設計的考慮
現在讓我們立即開始考慮這個簡單項目的主要問題:功能設計。
以下是我們的一些考慮。開始的時候,用戶面前會出現一個屏幕,要求輸入用戶自己的電子郵件地址,并從一個 Web 表單中選擇幾個候選人。選中候選人后就可以提交他們,結果會記錄在本地的一個預選票中。然后,會向提供的電子郵件發送一個電子郵件確認。在這種情況中,我們假設一個經過驗證的電子郵件地址就足以建立用戶的身份。
這樣會出現多次投票的問題。從實踐角度來說,我想我們沒有什么辦法限制一個用戶使用多個電子郵件地址進行多次投票,但是我們可以對選票進行限制,只允許一個電子郵件帳號投一票。這個電子郵件的驗證中包含一個鏈接,它指向原來的 CGI 腳本,這樣就可以將該鏈接與本地 DBM 文件中保存的數據進行比較。如果兩個記錄匹配,那么這張選票就是有效的。如果這兩個記錄不能匹配,那么這張選票就不會被核實。相反,會生成一個新的電子郵件確認,其中包含數據庫中的一條新驗證記錄。這將覆蓋對應電子郵件地址的預選票項,從而有效地從頭再次處理選票。
如果這兩條記錄可以匹配,那么投票者就可以確認預選票?,F在,如果投票者改變了注意,那么他可以只返回 Web 表單,并輸入一個新的預選票,替換原來的預選票。這種設計可以得到一個比較安全的系統;條件是每個投票的用戶都有且只有一個可以接受的電子郵件帳號,這樣就可以保證每個用戶都不會投兩次票。(稍后我會回到這個問題上。)
現在讓我們開始詳細介紹系統的細節。
細節: 哈希鍵值
在 Perl 中,可以使用哈希鍵值來創建聯合數組,從而使我們能夠動態開發復雜的數據結構。當您將這種特性與將這些(任意復雜的)數據結構保存在二進制 DBM 文件中的能力結合在一起使用時,就可以開發出一個小型的數據庫系統。完成這些工作所缺少的組件可以由 MLDBM
和 MLDBM::Sync
模塊提供。
MLDBM
模塊可以將復雜的 Perl 哈希鍵值無縫地保存在一個本地文件中。MLDBM::Sync
模塊使得對這些文件進行安全加鎖成為可能,它使用了 $sync->Lock
和 $sync->ReadLock
方法。在加載或保存所需要的結構之后,再調用 UnLock()
方法來刷新 I/O 并釋放變量。(關于這方面的更多信息,請參閱 Perl 文檔中有關 MLDBM::Sync
模塊的內容:man 3 MLDBM::Sync
。)
從根本上來說,邏輯流程非常簡單,如清單 1 所示。
清單 1. 邏輯流程偽碼
|
在確定底層的條件流程之后,剩下的惟一任務就是構建適合以后使用的對象。正如我在前面介紹的那樣,可以使用 tie'd
變量和 MLDBM
文家鎖定來檢索和更新所需的哈希數據結構。所使用的對象更像是一些精巧的數據結構,而不像是一些羽翼豐滿的對象;在這些對象之間,數據是以某種并行方式處理的:選票也從最初的預選票轉換成為最終的正式選票。
換言之,選票清單被用來構建一個 DraftBallot
,而這個 DraftBallot
又被用來創建 CastBallot
和 BallotBox
類。這樣,對于主要的投票 CGI 程序,耦合性就是最小的。
從另外一方面來說,雖然我通常認為使用一些依賴于外部資源(例如文件)的構造函數不是一個好的實踐方法(因為這樣可能會引起失敗,并導致一些不可預知的狀態),但是在這種情況下,以這種方式實現的代碼將更易于理解。由于 Perl 并不依賴于指針,所以沒什么理由不利用這種簡單性。
細節:電子郵件
允許用戶從您的 Web 服務器上發送電子郵件是一個危險的舉動,因為垃圾郵件可以利用您的主機來胡亂發送電子郵件。為了將這種威脅降至最低,腳本通常會檢查要發送的電子郵件地址是否是一個可到達的地址。您可以通過修改 DraftBallot
類中的驗證方法 voter_is_okay()
來加強這種限制,使其在進行驗證時參考一個可接受的電子郵件地址。這樣可能會要求用戶在進行投票之前進行注冊。
防止出現重復投票的其他方法包括搜集 IP 地址或在客戶機上設置 cookie,但是我不想采用這種方法,因為很多學生可能會在校園中使用共享的公用終端。
SASL 身份驗證 在使用 SASL 認證時,您有兩個選擇:可以將這個腳本指向一臺可以轉發使用正確證書的電子郵件的機器,或者使用 Perl |
細節:不太安全的投票
調用 $castBallot->dumpHTMLentrys()
方法會回顯一個詳細的信息,指出誰投票給了誰。實際上,我要注釋掉這個調用,在選舉結束之后使用 Linux at
批處理命令來關閉 Web 服務器。
當服務器關閉之后,您可以注釋掉這一部分,并重新啟動 Web 服務器,將其臨時設置為 只監聽 localhost 的地址。然后,通過單擊一個之前提交的鏈接,可以將完整的結果回顯給所有用戶,并且可以通過向一個專用的免費電子郵件帳號發送一個副本,來收集完整的結果。
注意,在這個例子下,每個選票都不會被統計兩次。在那些確定需要保密的情況中,可以使用一個簡短的 JavaScript 函數來隱藏結果。誠然,有些人可能希望完全采用匿名投票,但是由于俱樂部的選舉通常都是通過舉手表決的,因此這很難實現安全的投票。
在考慮這種工作流程模式時,我意識到使用基于 GET
的驗證鏈接以及使用非加密驗證鏈接的必要性,這樣可以進行一些實驗,讀取這些鏈接,并基于指定的電子郵件地址和一些已知的驗證鏈接來構建一些錯誤的確認投票。為了防止這種事情的發生,同時為了仍然能夠通過非加密鏈接進行簡單的調試,我決定在驗證步驟中添加一、兩項內容:為每個預選票添加一個惟一的標識符。
這個標識符是基于操作系統中正在執行的腳本的集成標識符(PID)的。為了讓預測驗證預選票的 URL 更加困難,我們可以再使用一個隨機數。我之所以關心這個問題,是因為會有一些惡意的用戶可能會對非常直觀的 URL 模式進行破解,從而試圖構建一些虛假的驗證選票。這是代碼的一部分,它不會直接轉換為一個 mod_perl
版本,因為它要依賴于正在運行的 Perl 的 PID,以及另外一個隨機數。如果這個表單是從一個重用的 mod_perl
實例中生成的,那么在兩次調用之間,PID 可能并不需要改變。
然后,我又意識到能使這個鏈接更具迷惑性的方法是使用一個 MD5 生成的哈希值,從而有效地隱藏所有投票者的信息。這具有雙向受益的優點:既可以使它很難被偽造,同時還維護了基于 mod_perl
的腳本的可移植能力。缺點是代碼有些難以調試,因為需要對客戶機與服務器之間交換的信息進行監視。
細節:文件布局
安裝過程要求 Web 服務器上有三種類型的目錄:
還要注意的是,這種權限可以進行修改,這樣,Web 服務器就可以向這個目錄中寫入 DBM 文件的內容了。
清單 2 顯示了在 Web 服務器上創建典型目錄的過程。
清單 2. 在 Web 服務器上設置目錄
|
嚴格來說,只有 cgi-bin(/var/www/cgi-bin)和 DBM(/var/www/db)目錄是絕對必需的,因為它們分別保存了腳本的可執行文件和投票數據。清單 1 中給出的文件布局是專用于 Linux 的,Web 服務器進程的用戶和組名可能有所不同,但實質上都需要在文件系統的適當地方放上幾個 Web 服務器可以訪問的組件。在將支持文件復制到各自的目錄中之后,要確保對 Web 服務器的配置文件(例如 httpd.conf)中的別名進行了正確更新。
在創建清單 2 中所給出的目錄之后,將 ZIP 文件中展開的內容復制到您的系統的類似目錄中。其中最重要的是,ballot、DraftBallot.pm、BallotBox.pm 和 CastBallot.pm 文件都需要位于 cgi-bin 目錄中。我們只需要使用 3 個非標準的 Perl 模塊;安裝過程如清單 3 所示(更詳細的信息,請參閱模塊的 README 文件)。
清單 3. 安裝 Perl 模塊
|
細節: 靜態 DNS 與動態 DNS
雖然我可以用一個靜態 IP 地址在擁有已分配的域的站點中建立這種服務,但是我覺得動態 DNS 應該可以提供一些安全上的好處。通常,如果一個服務器沒有靜態 IP 地址,那么來自 Web 上的訪問流量就不可能太大,動態 DNS 讓我們可以在另外一個頂級域名之上臨時建立一個可解析的機器名。這樣我們就可以在 Internet 上快速出現,并快速消失,將遭受黑客攻擊的風險降至最低。最好的方法是,這種服務是免費的。
還需要指出的是,將服務器配置為監聽一個非標準的大一些的端口(例如 8000)是很明智的,因為很多 ISP 都阻塞了端口 80 上的連接請求??蛻魴C(投票者)通??梢詮囊粋€知名的靜態地址(例如學校提供的主頁)上的鏈接重定向到投票服務器上。在投票完成之后,提供 Web 服務的服務器就可以從 Web 上完全消失了,無需關閉或重新配置這臺服務器。其中并沒有任何缺點可以影響到所引用的頁面(這臺服務器是由其他人進行管理的)。在一些對政策敏感的環境中,這種考慮尤其重要。(有關使用動態 DNS 的更詳細內容,請參閱 參考資料 一節的內容。)
細節:GET 有害嗎?
瀏覽器可以使用 GET
和 POST
方法將數據傳遞到所引用的頁面中,從而對狀態進行維護;也可以通過傳遞給服務器上的消息頭中包含的 cookie 信息對狀態進行維護。為了確認一張選票是從一個真實的人(至少是從一個有效的電子郵件帳號中)那里發出的,應該先將預選票發送到一個電子郵件地址進行確認。此外,cc: 或 bcc: 消息也可以在以后引用。正如我前面介紹的一樣,實現這種功能的最直接的方法是向投票者發送一個 HTTP GET
結構化的鏈接。當然,有些作者會宣稱用來更新記錄的 GET
方法并不好用。但是在這種情況下,任何這之后單擊某一個鏈接的用戶都只會接收一條更新消息,并且可以從這條消息了解每個候選人的目前有效選票,因此,這是無害的。
其他可用的改進
在使用這個腳本時,還要考慮其他一些安全問題,我們也應該考慮這些問題。任何允許外部實體來輸入數據的程序都容易受到惡意的攻擊,例如緩沖區溢出和嵌入式控制字符。反之,使用專用的程序來讀寫本地 DBM 文件至少具有以下優點:在沒有 SQL 后門的地方,是不可能存在 SQL 插入攻擊的。
在您同意需要對到達的數據進行過濾之后,我要將變量 $CGI::DISABLE_UPLOADS
和 $CGI::POST_MAX
設置為非常嚴格的值。另外我建議采用如下設置:
DATA
偽文件句柄之類的不完善系統在腳本的末尾保存數據。 MLDBM
模塊提供的機制。 MLDBM
模塊等效的 PHP 模塊。 結束語
假如有機會構建一個這樣的系統,同時試著保持它的簡單性并使其自成一體,那么該系統可以使我能夠研究一些非常有用的 Perl 模塊。我發現為這樣一個簡單的項目定義特性和開發功能規范的過程既很有趣又是一種享受。我希望本文中在構建這種系統時的一些考慮事項可以為您實現類似的項目提供一些幫助。