##實現原理## Java的web容器都實現了session機制,實現的邏輯思想都是一致的,可是具體方案可能會存在必定差別,這裏我以tomcat容器爲例,探討下session實現的機制。php
下圖是tomcat源碼裏session實現: html
實現包的路徑是:org.apache.catalina.session,tomcat對外提供session調用的接口不在這個實現包裏,對外接口是在包javax.servlet.http下的HttpSession,而實現包裏的StandardSession是tomcat提供的標準實現,固然對外tomcat不但願用戶直接操做StandardSession,而是提供了一個StandardSessionFacade類,tomcat容器裏具體操做session的組件是servlet,而servlet操做session是經過StandardSessionFacade進行的,這樣就能夠防止程序員直接操做StandardSession所帶來的安全問題。(StandardSessionFacade使用了設計模式裏的Facade(外觀)模式,外觀模式能讓不一樣邏輯層的組件進行解耦)。java
實現類裏有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兩個實現。nginx
##安全運用## ###運用問題### 由上面所描述的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信息同步到本身的容器裏,這個過程是十分消耗系統資源,當訪問量增長會嚴重影響到網站的效率和穩定性。web
我如今所作的網站裏有一個解決方案,當用戶請求網站的時候會先將請求發送給硬件的負載均衡設備,該設備能夠截獲客戶端發送過來的session的id值,而後咱們根據這個id值找到產生這個session的服務器,將請求直接發送給這臺服務器。這種解決方案看起來解決了session共享問題,其實結果是將集羣系統最終變回了單點系統,若是處理請求的web容器掛掉了,那麼用戶的相關會話操做也就廢掉了。此外,這種作法也干擾了負載均衡服務器的負載均衡的計算,讓請求的分發並非公平的。redis
通常大型互聯公司的網站都是有一個個獨立的頻道所組成的,例如咱們經常使用的百度,會有百度搜索,百度音樂,百度百科等等,我相信他們不會把這些不一樣頻道都給一個開發團隊完成,應該每一個頻道都是一個獨立開發團隊,由於每一個頻道的應用的都是獨立的web應用,那麼就存在一個跨站點的session同步的問題,跨站點的登陸可使用單點登陸的(SSO)的解決方案,可是無論什麼解決方案,跨站點的session共享任然是逃避不了的問題。算法
由上所述,Session一共有兩個問題須要解決: (1)Session的存儲應該獨立於web容器,也要獨立於部署web容器的服務器; (2)如何進行高效的Session同步;數據庫
在講到解決這些問題以前,咱們首先要考慮下session如何存儲纔是高效,是存在內存、文件仍是數據庫了?文件和數據庫的存儲方式都是將session的數據固化到硬盤上,操做硬盤的方式就是IO,IO操做的效率是遠遠低於操做內存的數據,所以文件和數據庫存儲方式是不可取的,因此將session數據存儲到內存是最佳的選擇。所以最好的解決方案就是使用分佈式緩存技術,例如:memcached和redis,將session信息的存儲獨立出來也是解決session同步問題的方法。apache
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, *;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']) { /* 彈出密碼輸入框 */ 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的工做機制,以及簡單地闡述了一些安全措施。但要記住,以上的方法都是可以增強安全性,不是說可以徹底保護你的系統,但願讀者本身再去調研相關內容。在這個調研過程當中,相信你會學到頗有實際使用價值的方案。