企業 Java 專家 Dennis Sosnoski 從 Java 服務器技術如何適合 Linux 來開始闡述他的觀點,然後給出在 Linux 上安全地設定 Tomcat Java servlet 引擎的一點建議。
Linux 平台和 Java 平台有著久遠的但有經常經歷曲折的關系。建置高效能虛擬機器的同時又要跟上日益增長的核心 Java API 集合,這樣做所帶來的復雜性在很大程度上使開發 Java 平台的開放來源程序代碼「Clean Room」實作的早期行動困難重重。Java 技術的特許實作最終可用於 Linux,但這些實作並不是開放來源程序代碼。因此,大多數 Linux 分發版沒有包括該特許實作。
盡管有這些困難,Java 平台還是提供了許多好處,從而導致在 Linux 上越來越多地使用該特許實作,尤其是對於服務器應用程序。在本文中,我回顧了 Java 平台給服務器應用程序帶來的優點,然後研究了在 Linux 上簡單且安全地部署 Java 服務所涉及的問題。作為一個實際范例,我將討論設定 Apache Software Foundation 的廣泛使用的 Tomcat Java servlet 引擎的詳細信息以用於獨立作業。
為什麼使用 Java 平台?
有許多原因可以解釋為什麼 Java 平台成為基於服務器的商業應用程序的廣為接受的選擇。我將主要討論我認為對於該環境至關重要的三個原因︰跨平台兼容性、受管執行時環境和易於開發。
Java 應用程序提供了跨多種操作系統和硬件平台的二進制兼容性。對於非 GUI 服務器應用程序尤其是這樣,在此類別應用程序中,通常在實際目標系統中需要執行非常少的測試。工作人員可以在任何他們喜歡的平台上進行編碼和除錯,同時仍可以將這些應用程序部署到他們也許不能直接控制的環境中。
Java 虛擬機器(Java Virtual Machine,JVM)環境的執行時特性以幾種方式來加強程序安全性。最顯著的方面之一是嚴格的類別型檢查、數組邊界檢查和自動垃圾收集的組合徹底防止了最具破壞性形式的服務器程序代碼攻擊︰緩沖區溢位、重復釋放的錯誤和游離的指針。Java 語言早期用於 applet,經過不斷發展,該語言還有一個完善的系統,用於對那些已確信存在安全性風險的設施進行細微的存取控制。這些方法可供獨立應用程序選擇使用,但它們已建置在許多 Java 服務的架構中。
這些執行時程序安全特性還提供了用 Java 語言開發的便利性。要對便利性這類別問題作任何精確測量是困難的,但大多數具有諸如 C 和 C++ 之類別語言背景而轉向 Java 程序化的工作人員都承認在轉變之後他們的生產力提高了。其中部分是因為在編譯時和執行時嚴格執行類別型確定,以及自動內存管理的簡單性。另一個因素是為 Java 平台開發的標准 API 擴充的集合。這些 API 對於新的工作人員可能是一個重大挑戰,但是一旦學會了,API 會為各種企業需求提供優秀的跨平台支持。
當然,對於某些應用程序而言,Java 平台可能是一個糟糕的選擇。盡管 JVM 體系結構在持續改進,但 Java 應用程序通常會比使用相同算法的 C 或 C++ 應用程序執行得稍微慢一點。根據我的經驗和測試,我估計這個速度差異對於在特許 JVM 上執行的大多數服務器應用程序來說大約是在 20% 到 50% 的范圍內,然而這很大程度上取決於程序代碼的質量。與獨立程序相比,在這些 JVM 上執行的 Java 應用程序還忍受著比較慢的啟動,但是這對於長時間執行的服務器應用程序通常並不是一個重大問題。在大多數情況中,降低的效能和較慢的啟動祇是為獲得 Java 平台的增強的安全性和更快速的開發優點所付出的微小代價。
開放來源碼替代選擇
除了標准特許 JVM(免費使用,但是來源碼受到限制;可用於 Sun、IBM、BEA 和 Blackdown 組織的 Linux)之外,對於 Linux 還有其它幾個替代選擇。這些選擇包括「Clean Room」開放來源碼 JVM 實作,其中使用最廣泛的可能是 Kaffe(在許多 Linux 分發版中都包括它)。Kaffe 是一個非常有意義的項目,它已經完成了一些令人驚訝的工作,但它只能提供與目前特許 JVM 有限的兼容性。因此,它通常無法使用於本文所關注的企業類別型的服務器應用程序。
用於 Java 程序的本機程序代碼編譯器的開放來源碼工作也有幾個替代選擇。這裡最重要的項目是 GNU 編譯器集(GNU Compiler Collection)的 GCJ.使用諸如 CGJ 之類別的本機程序代碼編譯器會將獨立於平台的 Java 字節碼在其執行之前轉換成特定於平台的程序代碼(這與在 JVM 中執行成對比,在 JVM 中執行通常在執行時將字節碼轉換成特定於平台的程序代碼)。
本機程序代碼編譯顯示出它極有可能成為一種避免在 JVM 中執行的 Java 應用程序啟動較慢的方法。但是,使用這種方法的編譯器通常都不能與當代特許 JVM 的穩定狀態效能相匹配。如果 Java 應用程序使用 Java 平台的動態特性(如使用反射來存取字段或加載在執行時選擇的類別),這種情況尤其突出。根據所使用的實作和編譯選項,本機程序代碼編譯也許還會削弱 Java 平台的許多執行時安全特性。最後,由於許可證問題,許多 Java API 不能與已編譯的本機程序代碼一起使用。由於這些限制,本機程序代碼編譯目前還不是 Java 平台服務器應用程序的一個好選擇。
C# 怎麼樣?
與 Java 執行時環境有許多共同點的一個替代方法是 Microsoft 的 C# 語言和相關的公共語言執行時(Common Language Runtime,CLR)。C# 是 Java 語言的關系緊密的衍生物,CLR 可能容許 C# 在許多平台上使用。CLR 還提供了 JVM 的許多執行時安全特性(盡管有嚴重削弱安全保證的逃離出口)。Microsoft .Net 實作還支持預編譯成本機程序代碼的選項以獲得更快速啟動,這與 GCJ 對 Java 字節碼所做的工作相同。當然,Linux 使用者並不能直接使用這項功能,因為 .Net 只適用於 Windows 系統。
Mono Project 正致力於為多種 Linux 產品建置「CLean Room」開放來源碼 C# 和等價於 CLR 的產品。現在,該項目中的 C# 編譯器已開發完成,而且還完成了大部分的 CLR,Microsoft 已發布將它用於標准化。但是,無論從效能還是功能角度來看,在它成為合理的 Java 平台替代選擇之前還有許多工作要做。CLR 只包括了與 Java 核心類別庫等價的基本內容。在可以將它看作是企業軟件開發的合理選項之前,還需要用許多額外 API 來補充它。
Mono Project 正在致力於開發 CLR 以外的 .Net 其它部分的移植,如果這些移植成功了 ─ 並且如果 Microsoft 不對 .Net 的這些部分強加它的專利權 ─ 那麼它們會有助於滿足 C# 成為 Linux 上服務器軟件開發的可靠平台的需要。但要使那些假設成為現實,還需要做很多工作,同時,Java 程序的本機程序代碼編譯器和開放來源碼 JVM 向那些確實想要避免使用特許 JVM 並可以忍受有限功能性的使用者提供了比較穩定的替代選擇。
Apache Tomcat
最普遍存在的 Java 平台服務器應用程序之一是 Apache Tomcat.Tomcat 是基於最初由 Sun 捐贈的來源碼的開放來源碼項目。它是一個 HTTP 服務器,是 Sun 透過 Java Community Process 開發的、對廣泛使用的 servlet 和 JavaServer Page(JSP)技術的正式參考實作。我將在本文中使用 Tomcat 作為樣本 Java 應用程序,將其部署成 Linux 上的一個服務。如果您想要嘗試自己執行 Tomcat,那麼您將需要在系統上安裝 Java 開發工具箱(Java Development Kit,JDK),而不是安裝更小的 Java 執行時環境(Java Runtime Environment,JRE)。
servlet 和 JSP 技術用於建構 HTTP 服務器應用程序。雖然 servlet 技術中加入了許多特性(包括存取安全性、Session管理和執行緒控制),但它本身祇是粗略地等價於為快速直接的 Java 語言呼叫而定制的 CGI 接口。JSP 技術提供了一種處理動態生成的 HTML 頁面的簡便方法,這些 HTML 頁面被直接編譯成 servlet 以用於快速執行時作業。
在這兩種技術之外,Tomcat 還提供了其它許多特性。憑它本身的效能,它實際上是全功能 Web 服務器,但它通常在 Linux 系統上與 Apache Web 服務器前端共同使用。Apache 向 Tomcat 提供了許多進階效能以適合靜態內容。對於靜態內容所占比例比較高且使用率很高的 Web 應用程序,Apache 前端非常有用。但對於許多簡單的 Web 應用程序,就沒必要使用它了,當更易於組態和管理時,單獨執行 Tomcat 就可提供足夠的效能(至少對於以前沒有使用過 Apache 的工作人員來說是這樣)。
連接埠難題
單獨執行 Tomcat 的一個大問題是它無法存取標准 HTTP 連接埠 80,除非是作為 root 使用者執行。作為 root 使用者執行服務器應用程序的想法通常並不是上串流公司所討論的問題,因此我將完全放棄這個想法﹗使用除 80 以外的連接埠是一個更好的選擇(例如,Tomcat 缺省連接埠 8080)。這通常適用於測試,但當使用者正在存取服務時,它會導致雜亂的 URL,因為需要在請求中清楚地說明連接埠號。使用非標准連接埠還意味著如果需要外部存取,就需要重新組態所有的防火牆。
xinetd 解決方案
幸好,Linux 支持一些利用 Tomcat(或任何其它使用者方式應用程序)處理連接端口 80 請求的簡便方式。一種常用方式是透過 xinetd.xinetd 是帶有廣泛存取控制和日志記錄支持的因特網服務守護程序,它還擁有方便的重新導向特性。重新導向讓您將系統組態成接受一個連接埠上的進入請求,然後將請求傳遞到另一個連接埠或者甚至另一個 IP 地址進行處理。
如果您想要在系統上設定 Tomcat 以處理連接埠 80 請求,就需要加入 xinetd 組態文件來實作這一目的。假設按一般在正常路徑上安裝了 xinetd,那麼您可以透過對 /etc/xinetd.d 目錄加入一個文件(以 root 使用者身份)來執行這一作業。清單 1 提供了用於 Tomcat 的一個樣本組態文件。
清單 1. xinetd 重新導向組態
# Redirects any requests on port 80 # to port 8080 (where Tomcat is listening) service tomcat { socket_type = stream protocol = tcp user = root wait = no port = 80 redirect = localhost 8080 disable = no }
在加入了組態文件之後,需要重新啟動 xinetd 來真正活化重新導向。在大多數 Linux 安裝上,透過以 root 使用者身份執行以下指令來重新啟動 xinetd︰
/sbin/service xinetd restart
祇要將組態文件放在 /etc/xinetd.d 目錄中,當重新啟動系統時,重新導向就會自動啟動。如果沒有將 Tomcat 設定成自動啟動,那麼在啟動 Tomcat 之前,會拒絕進入請求。
iptables 解決方案
xinetd 是處理請求重新導向的一種好方法,但它執行了一個處理序以在連接端口之間實際轉信數據,這確實增加了一些開銷。最新的 Linux 內核版本透過使用 iptables 來支持一種更好的設定重新導向的方法。iptables 與 xinetd 的區別之處在於它是一個真正的內核組件。因此,它可以避免 xinetd 方法增加的開銷。使用 iptables 的唯一缺點是它可能比 xinetd 更難以組態,而且它只可用於相當新的內核版本。
您需要執行支持 iptables 的 2.4.x 或更新的內核,以便使用我在這裡描述的技術。組態和設定 iptables 是一個確信由幾篇文章來單獨描述的主題,所以我不打算在這裡嘗試討論該主題。如果對 iptables 的入門需要幫助,請閱讀 Linux 分發版的手冊。要快速檢查 iptables 是否在您的系統上執行,嘗試以 root 使用者身份執行︰
/sbin/service iptables status
如果它正在執行,您將會在控制台上看到表和鏈的清單。
iptables 使用幾個不同的表和封包鏈來處理規則的。為了將進入 HTTP 請求從連接端口 80 重新導向到系統中的另一個連接埠,您將要使用 nat 表(表示網絡地址轉換,Network Address Translation)和 PREROUTING 鏈。清單 2 提供了要執行的實際指令(以 root 使用者身份),以便於加入一條處理這一請求的規則。這條規則的作用是將進入包的目標連接端口 80 修改成目標連接端口 8080,因此祇有在您沒有阻止從外部使用連接埠 8080 時,這條規則才會正確工作。一旦執行了該指令,您就應該能夠立即處理進入請求。
清單 2. iptables 重新導向規則
/sbin/iptables -t nat \ -A PREROUTING -j REDIRECT -p tcp \ ——destination-port 80:80 ——to-ports 8080 /sbin/service iptables save
以便於儲存目前 iptables 組態。
自動啟動 Tomcat
當執行諸如 Tomcat 之類別的 Java 服務時的另一個問題是當系統啟動時,如何自動啟動該應用程序,以及當系統關機時如何自動停止它(換句話說,將它當作守護程序執行)。經驗豐富的 Linux 使用者已經知道怎樣做,但如果您還是個 Linux 新手,以下就是一些基礎知識。
如果您就在您的個人系統上執行它並且想要使用與直接執行 Tomcat 一樣的對 Tomcat 的文件和目錄的存取權,那麼您可以用您自己的使用者名稱來設定它。但是,通常,一個比較好的想法是︰為將要作為守護程序執行的任何程序設定一個單獨的使用者。要針對 Tomcat 執行這一作業,以 root 使用者身份執行︰
/usr/sbin/useradd tomcat
這將建立一個名為 tomcat 的使用者賬戶並建立一個用於 Tomcat 安裝的主目錄 /home/tomcat.所建立的主目錄的所有者是 tomcat 使用者,而且通常只容許這個使用者存取(當然還有 root 使用者)。如果想要從其它賬戶存取 Tomcat 安裝,可以將許可權變更成包括組存取權,並將 tomcat 組新增到這些其它賬戶。
總之,要將 Tomcat 當作守護程序執行,需要將服務組態文件新增到 /etc/init.d 目錄中,而您可能要將該文件命名為「tomcat」。清單 3 提供了該文件的樣本。這假設了 Tomcat 安裝在 /home/tomcat 下,並且該位置中有兩個 shell 指令碼文件,用於處理啟動和停止服務器(tcstart.sh 和 tcstop.sh)。在執行實際的 Tomcat 啟動或停止指令碼之前,需要使用這些文件來設定 Tomcat 所需的環境變量(包括 JAVA_HOME 和 JDK_HOME)。
清單 3. Tomcat 服務定義
#!/bin/bash # # tomcat Starts Tomcat Java server. # # # chkconfig: 345 88 12 # description: Tomcat is the server for Java servlet applications. ### BEGIN INIT INFO # Provides: $tomcat ### END INIT INFO # Source function library. 。 /etc/init.d/functions [ -f /home/tomcat/tcstart.sh ] || exit 0 [ -f /home/tomcat/tcstop.sh ] || exit 0 RETVAL=0 umask 077 start() { echo -n $"Starting Tomcat Java server: " daemon su -c /home/tomcat/tcstart.sh tomcat echo return $RETVAL } stop() { echo -n $"Shutting down Tomcat Java server: " daemon su -c /home/tomcat/tcstop.sh tomcat echo return $RETVAL } restart() { stop start } case "$1" in start) start ;; stop) stop ;; restart|reload) restart ;; *) echo $"Usage: $0 {start|stop|restart}" exit 1 esac exit $?
以下是一個樣本 tcstart.sh,可以修改它來適合您的安裝。
清單 4. 樣本 tcstart.sh
#!/bin/bash export JDK_HOME=/usr/java/jdk export JAVA_HOME=/usr/java/jdk #run the startup script from Tomcat installation /home/tomcat/server/bin/startup.sh
chroot︰最大的安全性監獄
對於真正的偏執狂,還可能進一步討論保護 Java 語言服務。當該服務提供對本地文件系統的某種形式的存取權時,這尤其有用。JVM 執行時安全性特性並不能阻止已經有權存取文件系統的應用程序存取除了使用者專用的文件之外的文件。在 Tomcat 的情況中,文件存取是它作為 HTTP 服務器使用時所固有的。它通常將為每個 Web 應用程序提供服務的文件限制為該應用程序目錄中的那些文件,但 servlet 應用程序可以避開這些限制。當 Tomcat 與諸如 Apache 之類別的前端 Web 服務器共同執行時,也會發生這種情況。
透過使用 chroot,您可以阻止 Tomcat(和所有在 Tomcat 下執行的 Web 應用程序)存取為該服務器留出的空間以外的任何東西。chroot 並不以任何方式特定於 Java 應用程序,但它是為 JVM 提供的安全性加入最終包裝器的簡便方法。我將在這裡為那些不熟悉 chroot 概念的人介紹設定它的要點。
chroot 所做的類別似於用於執行 Java 程序代碼的 JVM 沙箱,但它適用於文件系統本身。chroot 執行一條指令並將您指定的位置設定成有效根目錄。所執行的指令(可以是執行其它指令(包括應用程序)的 shell 指令碼)只能存取所指定的有效根目錄下的文件系統的一部分。文件系統的其余部分對於該指令來說完全不存在。
要對諸如 Tomcat 之類別的應用程序使用 chroot,需要將一些基本系統應用程序和鏈接庫(包括實際的 Java JDK 安裝)復制到新的虛擬根目錄下。這可能會占用許多空間 ─ 也許從一百兆字節到一千兆字節或更多 ─ 這取決於您想盡多少努力來將這些應用程序和鏈接庫減少到最小。設定它的最簡單方法也最浪費空間︰祇要將整個 /bin、/lib、/usr/bin 和 /usr/lib 目錄樹以及 Java 安裝復制到新的根目錄下,使 root 使用者成為對所有文件的擁有者和唯一授權的寫入者。如果想要讓磁盤使用率保持到最小,可以有選擇地只復制 chroot 中需要的指令(包括基本指令,如 ls、rm、echo 和 cat 等,以及實際的 Java 安裝)以及那些指令使用的庫(透過使用 ldd 可以找到那些庫)。
接下來,您將需要建立一些額外目錄作為一般系統的縮減版本。這包括 /dev,以及設備 /dev/null 和 /dev/zero;/etc 以及 /etc/passwd 和 /etc/group 文件的已編輯版本(只保留 root 和 tomcat 項),也許還有主機。如果正在多處理器系統上執行,您還需要在新的根目錄下掛裝 /proc 系統,因為 JVM 使用該系統來協調各個處理器。
最後,為了在設定了 chroot 後仍作為使用者 tomcat 執行,也許需要建置一個可以在虛擬根目錄下執行的 su 指令版本(許多分發版的一般版本不容許這樣做)。要這樣做,可以從 GNU 項目獲取 sh-utils 來源碼並根據該來源碼直接建置 su,然後將它復制到新的根目錄的 /bin 目錄中。
完成了所有這些設定後,可以嘗試以 root 使用者身份執行(假設新的根目錄位於 /home/tomcat)︰
/usr/sbin/chroot /home/tomcat /bin/su tomcat
如果設定正確,這應該讓您在新的根目錄下作為 tomcat 執行,而且您可以嘗試使用您的指令碼來啟動和停止 Tomcat.在證實了所有東西都工作之後,最後一步就是將清單 3 中的 Tomcat 服務定義變更成使用 chroot 來代替 su,將 su 指令移到 tcstart.sh 指令碼和 tcstop.sh 指令碼。還需要確保只能由 root 使用者修改這些指令碼。
希望這篇概述的解釋很清楚,這個過程並不適合膽小的人﹗如果您選擇走使用 chroot 這條路線,但以前沒有使用過 chroot,那麼您一定要參考網上的 chroot 參考數據之一,以獲取詳細信息。但是這確實會給您的 Java 服務器程序代碼可能最好的隔離,而且在某些情況下,帶來的內心的寧靜值得這樣做。
結語
Linux 和 Java 技術都正在贏得商業系統的市場份額。盡管開放來源碼 Linux 和特許 Java 技術之間存在著原理上的差異,但這兩者在一起確實配合得很好。Linux 對於 Java 應用程序,尤其是對於服務器類別型的應用程序是一個極好的部署環境,而 Java 技術是作為企業軟件開發的先進方法而建立且得到了認可。
透過正確的預防措施,在 Linux 上執行的 Java 服務器應用程序可以提供非常高的安全性程度 ─ 甚至高於本機應用程序 ─ 因為 Java 技術消除了服務器應用程序中許多弱點的常見來源。Java 技術的跨平台性質所帶來的是包括企業工作人員和 Linux 立即就緒的應用程序的巨大資源集合。Java 服務器應用程序開始在日益增長的 Linux 的服務器市場份額中扮演重要角色,而且這種趨勢在將來只會幫助這兩種技術。
我要感謝 Miles Sabin 向我指出了 /proc 文件系統,來解決在多處理器系統上 chroot Java 的問題。