理解Session實現原理及安全運用

##實現原理##

java的web容器都實現了session機制,實現的邏輯思想都是一致的,但是具體方案可能會存在一定差異,這裏我以tomcat容器爲例,探討下session實現的機制。

下圖是tomcat源碼裏session實現:

實現包的路徑是:org.apache.catalina.session,tomcat對外提供session調用的接口不在這個實現包裏,對外接口是在包javax.servlet.http下的HttpSession,而實現包裏的StandardSession是tomcat提供的標準實現,當然對外tomcat不希望用戶直接操作StandardSession,而是提供了一個StandardSessionFacade類,tomcat容器裏具體操作session的組件是servlet,而servlet操作session是通過StandardSessionFacade進行的,這樣就可以防止程序員直接操作StandardSession所帶來的安全問題。(StandardSessionFacade使用了設計模式裏的Facade(外觀)模式,外觀模式能讓不同邏輯層的組件進行解耦)。

實現類是有Manager的類是用來管理session的工具類,它負責創建和銷燬session對象,其中ManagerBase是所有session管理工具類的基類,它是一個抽象類,所有具體實現session管理功能的類都要繼承這個類,該類有一個受保護的方法,該方法就是創建sessionId值的方法(tomcat的session的id值生成的機制是一個隨機數加時間加上jvm的id值,jvm的id值會根據服務器的硬件信息計算得來,因此不同jvm的id值都是唯一的),StandardManager類是tomcat容器裏默認的session管理實現類,它會將session的信息存儲到web容器所在服務器的內存裏。PersistentManagerBase也是繼承ManagerBase類,它是所有持久化存儲session信息的基類,PersistentManager繼承了PersistentManagerBase,但是這個類只是多了一個靜態變量和一個getName方法,目前看來意義不大,對於持久化存儲session,tomcat還提供了StoreBase的抽象類,它是所有持久化存儲session的基類,另外tomcat還給出了文件存儲FileStore和數據存儲JDBCStore兩個實現。

##安全運用##

###運用問題###

由上面所描述的session實現機制,我們發現,爲了彌補http協議的無狀態的特點,服務端會佔用一定的內存和cpu用來存儲和處理session計算的開銷,這也就是tomcat這個的web容器的併發連接那麼低(tomcat官方文檔裏默認的連接數是200)原因之一。因此很多java語言編寫的網站,在生產環境裏web容器之前會加一個靜態資源服務器,例如:apache服務器或nginx服務器,靜態資源服務器沒有解決http無狀態問題的功能,因此部署靜態資源的服務器也就不會讓出內存或cpu計算資源專門去處理像session這樣的功能,這些內存和cpu資源可以更有效的處理每個http請求,因此靜態資源服務器的併發連接數更高,所以我們可以讓那些沒有狀態保持要求的請求直接在靜態服務器裏處理,而要進行狀態保持的請求則在java的web容器裏進行處理,這樣能更好的提升網站的效率。

當下的互聯網網站爲了提高網站安全性和併發量,服務端的部署的服務器的數量往往是大於或等於兩臺,多臺服務器對外提供的服務是等價的,但是不同的服務器上面肯定會有不同的web容器,由上面的講述我們知道session的實現機制都是web容器裏內部機制,這就導致一個web容器裏所生成的session的id值是不同的,因此當一個請求到了A服務器,瀏覽器得到響應後,客戶端存下的是A服務器上所生成的session的id,當在另一個請求分發到了B服務器,B服務器上的web容器是不能識別這個session的id值,更不會有這個sessionID所對應記錄下來的信息,這個時候就需要兩個不同web容器之間進行session的同步。Tomcat容器有一個官方的解決方案就是使用apache+tomcat+mod_jk方案,當一個web容器裏session的信息發生變化後,該web容器會向另一個web容器進行廣播,另一個web收到廣播後將session信息同步到自己的容器裏,這個過程是十分消耗系統資源,當訪問量增加會嚴重影響到網站的效率和穩定性。

我現在所做的網站有一個解決方案,當用戶請求網站的時候會先將請求發送給硬件的負載均衡設備,該設備可以截獲客戶端發送過來的session的id值,然後我們根據這個id值找到產生這個session的服務器,井請求直接發送給這臺服務器。這種解決方案看起來解決了session共享問題,其實結果是將集羣系統最終變回了單點系統,如果處理請求的web容器掛掉了,那麼用戶的相關會話操作也就廢掉了。此外,這種做法也干擾了負載均衡服務器的負載均衡的計算,讓請求的分發並不是公平的。

一般大型互聯公司的網站都是有一個個獨立的頻道所組成的,例如我們常用的百度,會有百度搜索,百度音樂,百度百科等等,我相信他們不會把這些不同頻道都給一個開發團隊完成,應該每個頻道都是一個獨立開發團隊,因爲每個頻道的應用的都是獨立的web應用,那麼就存在一個跨站點的session同步的問題,跨站點的登錄可以使用單點登錄的(SSO)的解決方案,但是不管什麼解決方案,跨站點的session共享任然是逃避不了的問題。

由上所述,Session一共有兩個問題需要解決:(1) Session的存儲應該獨立於web容器,也要獨立於部署web容器的服務器;(2)如何進行高效的Session同步;

在講到解決這些問題之前,我們首先要考慮下session如何存儲纔是高效,是存在內存、文件還是數據庫了?文件和數據庫的存儲方式都是將session的數據固化到硬盤上,操作硬盤的方式就是IO,IO操作的效率是遠遠低於操作內存的數據,因此文件和數據庫存儲方式是不可取的,所以將session數據存儲到內存是最佳的選擇。因此最好的解決方案就是使用分佈式緩存技術,例如:memcached和redis,將session信息的存儲獨立出來也是解決session同步問題的方法。

Tomcat的Session同步也有使用memcache的解決方案,大家可以參加下面的文章: http://blog.sina.com.cn/s/blog_5376c71901017bqx.html

但是該方案只是解決了同步問題,session機制仍然和web容器緊耦合,我們需要一個高效、可擴展的解決方案,那麼我們就應該不是簡單的把session獨立出來存儲而是設計一個完全獨立的session機制,它既能給每個web應用提供session的功能又可以實現session同步,下面是一篇用zookeeper實現的分佈式session方案:

http://www.open-open.com/lib/view/open1378556537303.html

###安全問題###

Http是一種無狀態性的協議。這是因爲此種協議不要求瀏覽器在每次請求中標明它自己的身份,並且瀏覽器以及服務器之間並沒有保持一個持久性的連接用於多個頁面之間的訪問。當一個用戶訪問一個站點的時候,用戶的瀏覽器發送一個http請求到服務器,服務器返回給瀏覽器一個http響應。其實很簡單的一個概念,客戶端一個請求,服務器端一個回覆,這就是整個基於http協議的通訊過程。

因爲web應用程序是基於http協議進行通訊的,而我們已經講過了http是無狀態的,這就增加了維護web應用程序狀態的難度, 對於開發者來說,是一個不小的挑戰。Cookies是作爲http的一個擴展誕生的,其主要用途是彌補http的無狀態特性,提供了一種保持客戶端與服務器端之間狀態的途徑,但是由於出於安全性的考慮,有的用戶在瀏覽器中是禁止掉cookie的。這種情況下,狀態信息只能通過url中的參數來傳遞到服務器端,不過這種方式的安全性很差。事實上,按照通常的想法,應該有客戶端來表明自己的身份,從而和服務器之間維持一種狀態,但是出於安全性方面的考慮,我們都應該明白一點 – 來自客戶端的信息都是不能完全信任的。

儘管這樣,針對維持web應用程序狀態的問題,相對來說,還是有比較優雅的解決方案的。不過,應該說是沒有完美的解決方案的,再好的解決方案也不可能適用所有的情況。這篇文章將介紹一些技術。這些技術可以用來比較穩定地維持應用程序的狀態以及抵禦一些針對session的攻擊,比如會話劫持。並且你可以學習到cookie是怎樣工作的,php 的session做了那些事情,以及怎樣才能劫持session。

如何才能保持web應用程序的狀態以及選擇最合適的解決方案呢?在回答這個問題之前,必須得先了解web的底層協議 – Hypertext Transfer Protocol (HTTP)。

當用戶訪問http://example.com這個域名的時候,瀏覽器就會自動和服務器建立tcp/ip連接,然後發送http請求到example.com的服務器的80端口。該個請求的語法如下所示:

GET / HTTP/1.1 Host: example.org

以上第一行叫做請求行,第二個參數(一個反斜線在這個例子中)表示所請求資源的路徑。反斜線代表了根目錄;服務器會轉換這個根目錄爲服務器文件系統中的一個具體目錄。

第二行描述的是http頭部的語法。在這個例子中的頭部是Host, 它標識了瀏覽器希望獲取資源的域名主機。還有很多其它的請求頭部可以包含在http請求中,比如user-Agent頭部,在php可以通過$_SERVER['HTTP_USER_AGENT']獲取請求中所攜帶的這個頭部信息。

但是遺憾的是,在這個請求例子中,沒有任何信息可以唯一標識當前這個發出請求的客戶端。有些開發者藉助請求中的ip頭部來唯一標識發出此次請求的客戶端,但是這種方式存在很多問題。因爲,有些用戶是通過代理來訪問的,比如用戶A通過代理B連接網站www.example.com, 服務器端獲取的ip信息是代理B分配給A的ip地址,如果用戶這時斷開代理,然後再次連接代理的話,它的代理ip地址又再次改變,也就說一個用戶對應了多個ip地址,這種情況下,服務器端根據ip地址來標識用戶的話,會認爲請求是來自不同的用戶,事實上是同一個用戶。 還用另外一種情況就是,比如很多用戶是在同一個局域網裏通過路由連接互聯網,然後都訪問www.example.com的話,由於這些用戶共享同一個外網ip地址,這會導致服務器認爲這些用戶是同一個用戶發出的請求,因爲他們是來自同一個ip地址的訪問。

保持應用程序狀態的第一步就是要知道如何來唯一地標識每個客戶端。因爲只有在http中請求中攜帶的信息才能用來標識客戶端,所以在請求中必須包含某種可以用來標識客戶端唯一身份的信息。

現在,我們來看下一個比較常規的針對session的攻擊:

用戶訪問http://www.example.org,並且登錄。 example.org的服務器設置指示客戶端設置相關cookie – PHPSESSID=12345 攻擊者這時訪問http://www.example.org/,並且在請求中攜帶了對應的cookie – PHPSESSID=12345 這樣情況下,因爲example.orge的服務器通過PHPSESSID來辨認對應的用戶的,所以服務器錯把攻擊者當成了合法的用戶。

整個過程的描述,請看下面的示例圖:

當然這種攻擊的方式,前提條件是攻擊者必須通過某種手段固定,劫持或者猜測出某個合法用戶的PHPSESSID。雖然這看起來難度很高,但是也不是不可能的事情。

有很多技術可以用來加強Session的安全性,主要思想就是要使驗證的過程對於合法用戶來說,越簡單越好,然後對於攻擊者來說,步驟要越複雜越好。當然,這似乎是比較難於平衡的,要根據你應用程序的具體設計來做決策。

最簡單的居於HTTP/1.1請求包括請求行以及一些Host的頭部:

GET / HTTP/1.1

Host: example.org

如果客戶端通過PHPSESSID傳遞相關的session標識符,可以將PHPSESSID放在cookie頭部中進行傳遞:

GET / HTTP/1.1

Host: example.org

Cookie: PHPSESSID=12345

同樣地,客戶端也可以將session標識符放在請求的url中進行傳遞。

GET /?PHPSESSID=12345 HTTP/1.1

Host:example.org

當然,session標識符也可以包含在POST數據中,但是這對用戶體驗有影響,所以這種方式很少採用。

因爲來自TCP/IP信息也不一定可以完全信任的,所以,對於web開發者來說,利用TCP/IP中的信息來加強安全性也是不太合適的。 不過,攻擊者也必須提供一個合法用戶的唯一的標識符,才能假扮成合法用戶進入系統。因此,看起來唯一能夠有效的保護系統的措施,就是儘量地隱藏session標識符或者使之難於猜測出來。最好就是兩者都能實施。

PHP會自動生成一個隨機的session ID,基本來說是不可能被猜測出來的,所以這方面的安全還是有一定保障的。但是,要防止攻擊者獲取一個合法的session ID是相當困難的,這基本上不是開發者所能控制的。

事實上,許多情況下都有可能導致session ID的泄露。 比如說,如果通過GET數據來傳遞session ID的話,就有可能暴露這個敏感的身份信息。因爲,有的用戶可能會將帶有session ID的鏈接緩存,收藏或者發送在郵件內容中。Cookies是一種像相對來說安全一點的機制,但是用戶是可以在客戶端中禁止掉cookies的!在一些IE的版本中也有比較嚴重的安全漏洞,比較有名的就是會泄露cookies給一些有安全隱患的邪惡站點。

因此,作爲一個開發者,可以肯定session ID是不能被猜測出來的,但是還是有可能被攻擊者使用某些方法獲取到。所以,必須採取一些額外的安全措施來防止此類情況在你的應用程序中發生。

實際上,一個標準的HTTP請求中除了Host等必須包含的頭部,還包含了一些可選的頭部.舉一個例子,看下面的一個請求:

GET / HTTP/1.1

Host:example.org

Cookie: PHPSESSID=12345

User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.1) Gecko/20061204 Firefox/2.0.0.1

Accept: text/html;q=0.9,/;q=0.1

Accept-Charset:ISO-8859-1, utf-8;q=0.66

Accept-Language: en

我們可以看到,在以上的一個請求例子中包含了四個額外的頭部,分別是User-Agent, Accept, Accept-Charset以及Accept-Language。因爲這些頭部不是必須的,所以完全依賴他們在你的應用程序中發揮作用是不太明智的。但是,如果一個用戶的瀏覽器確實發送了這些頭部到服務器,那麼可以肯定的是在接下來的同一個用戶通過同一個瀏覽器發送的請求中,必然也會攜帶這些頭部。當然,這其中也會有極少數的特殊情況發生。假如以上例子是由一個當前的跟服務器建立了會話的用戶發出的請求,考慮下面的一個請求:

GET / HTTP/1.1 Host:example.org

Cookie:PHPSESSID=12345

User-Agent: Mozilla/5.0

因爲有相同的session id包含在請求的Cookie頭部中,所以相同的php session將會被訪問到。但是,請求裏的User-Agent頭部跟先前的請求中的信息是不同的,系統是否可以假定這兩個請求是同一個用戶發出的?

像這種情況下,發現瀏覽器的頭部改變了,但是不能肯定這是否是一次來自攻擊者的請求的話,比較好的措施就是彈出一個要求輸入密碼的輸入框讓用戶輸入,這樣的話,對用戶體驗的影響不會很大,又能很有效地防止攻擊。

當然,你可以在系統中加入覈查User-Agent頭部的代碼,類似PHP代碼:

<?php
session_start();
if(md5($_SERVER['HTTP_USER_AGENT']) != $_SESSION['HTTP_USER_AGENT'])
{
    &nbsp;&nbsp;/* 彈出密碼輸入框*/&nbsp;&nbsp;exit;
}
?>

當然,你先必須在第一次請求時,初始化session的時候,用MD5算法加密user agent信息並且保存在session中,類似PHP代碼:

<?php
session_start();
$_SESSION['HTTP_USER_AGENT'] = md5($_SERVER['HTTP_USER_AGENT']);
?>

雖然不一定需要用MD5來加密這個User-Agent信息,但使用這種方式以後就不需要再過濾這個$_SERVER['HTTP_USER_AGENT']數據了。不然的話,在使用這個數據以前必須要進行數據過濾,因爲任何來自客戶端的數據都是不可信任的,必須要注意這一點。

在你檢查這個User-Agent客戶端頭部信息以後,做爲一個攻擊者必須要完成兩步才能劫持一個session:

獲取一個合法的session id包含一個相同的User-Agent頭部在僞造的請求中

你可能會說,居然攻擊者能獲得有效的session id,那麼以他的水平,僞造一個相同的User-Agent不是件難事。不錯,但是我們可以說這至少給他添加了一些麻煩,在一定程度上也增加了session機制的安全性。

你應該也能想到了,既然我們可以檢查User-Agent這個頭部來加強安全性,那麼不妨再利用其它的一些頭部信息,把他們組合起來生成一個加密的token,並且讓客戶端在後續的請求中攜帶這個token!這樣的話,攻擊者基本上不可能猜測出這樣一個token是怎麼生成出來的。這好比你用信用卡在超市付款,一個你必須有信用卡(好比session id),另外你也必須輸入一個支付密碼(好比token),這有這兩者都符合的情況下,你才能成功進入賬號付款。 看下面一段代碼:

<?php
session_start();
$token='SHIFLETT' . $_SERVER['HTTP_USER_AGENT'];
$_SESSION['token'] = md5($token, session_id());
?>

注意:Accept這個頭部不應該被用來生成token,因爲有些瀏覽器會自動改變這個頭部,當用戶刷新瀏覽器的時候。

在你的驗證機制中加入了這個非常難於猜測出來的token以後,安全性會得到很大的提升。假如這個token通過像session id一樣的方式來進行傳遞,這種情況下,一個攻擊者必須完成必要的3步來劫持用戶的session:

獲取一個合法的session ID 在請求中加入相同的User-Agent頭部,用與生成token 在請求中攜帶被攻擊者的token

這裏面有個問題。如果session id以及token都是通過GET數據來傳遞的話,那麼對於能獲取session ID的攻擊者,同樣就能夠獲取到這個token。所以,比較安全靠譜的方式應該是利用兩種不同的數據傳遞方式來分別傳遞session id以及token。例如,通過cookie來傳遞session id,然後通過GET數據來傳遞token。因此,假如攻擊者通過某種手段獲得了這個唯一的用戶身份標識,也是不太可能同時輕鬆地獲取到這個token,它相對來說依然是安全的。

還有很多的技術手段可以用來加強你的session機制的安全性。希望你在大致瞭解session的內部本質以後,可以設計出適合你的應用系統的驗證機制,從而大大的提高系統的安全性。畢竟,你是最熟悉當下你開發的系統的開發者之一,可以根據實際情況來實施一些特有的,額外的安全措施。

以上只是大概地描述了session的工作機制,以及簡單地闡述了一些安全措施。但要記住,以上的方法都是能夠加強安全性,不是說能夠完全保護你的系統,希望讀者自己再去調研相關內容。在這個調研過程中,相信你會學到很有實際使用價值的方案。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章