最近想拿一個小項目來試水RESTful Web API,項目只有幾個調用,比較簡單,但同樣需要身份驗證,如果是傳統的網站的話,那不用說,肯定是用戶名+密碼在登錄頁獲得登錄Token,並把登錄Token記在Cookie和Session中作為身份標識的這種方式,但現在不同了,關鍵是RESTful,這意味着我們設計出來的這些API是無狀態的(Stateless),下一次的調用請求和這一次的調用請求應該是完全無關的,也就是說,正宗的RESTful Web API應該是每次調用都應該包含了完整的信息,沒錯,包括身份信息!
那如何確保安全?傳輸時給密碼做MD5加密?得了吧!這樣做只能讓你自己感覺“安全”點,其實沒什么任何用處,利用現在的技術(有種叫什么Rainbow Table啥的來着?本人外行,不是很懂)很快就能算出明文密碼了,而且如何防止挾持和重發攻擊?
也許你想到了,SSL,如果你打算采用SSL,請忘記一切自行設計的加密方案,因為SSL已經幫你做好了一切,包括防止監聽,防止挾持,防止重發……一切都幫你考慮好了,你大膽地把明文密碼寫在你的包中就OK了,我向你保證沒問題。
但SSL的缺點是服務器端配置相對有點復雜,更關鍵的就是客戶端對此支持可能不好,那你考慮一種自己的加密方法,有木有?我這里提供一種方法,思路來自於:http://www.thebuzzmedia.com/designing-a-secure-rest-api-without-oauth-authentication/,我只是把上面的內容中整理了一下變成了我的方法。(傳說中的剽竊?呵呵)方法描述如下:
- 假設有一個用戶,用戶名是guogangj,密碼是123456(呃……這也能叫密碼?)
- 他要GET http://test.com/api/orders/
- 於是把 http://test.com/api/orders/這個URL和一個新生成的GUID拼在一起,再用123456這個密碼執行對稱加密,生成的密文為XXXXOOOOXXXXOOOO(假設而已)
- 數據包中帶上用戶名guogangj和XXXXOOOOXXXXOOOO這個密文,發送給服務器
- 服務器收到包后,根據guogangj這個用戶名到數據庫中查找到123456這個密碼
- 服務器使用123456這個密碼來解密XXXXOOOOXXXXOOOO這個密文,得到了明文,即http://test.com/api/orders/這個URL和前面由客戶端生成的那個GUID
- 服務器到一個全局的集合中查找這個GUID,看看是否已經存在,如果存在,則驗證不通過,如果不存在,就將其放入這個集合中。這是為了避免重發攻擊。這個全局的集合會越來越大,所以還要定期清理。
- 服務器再比對解密出來的URL和用戶真實請求的URL是否一致,如果一致,那么認為這是合法用戶,驗證通過!
這是大致過程,如果數據庫里找不到該用戶,或者解密錯誤,都被認為驗證不通過。以下是一些改進:
- 數據庫中的密碼最好做一下摘要(MD5之類的),客戶端對應地也要做一下。
- 在生成密文的時候可以考慮加入另外一些不希望被明文傳輸的敏感內容,甚至可以加入IP地址,並在服務器端驗證。
- 並非每次都要真正去數據庫里拿一次用戶信息,也許你有更好的辦法,比如一個簡單的緩存(不過需要處理緩存更新的問題),或者當你的系統大到一定程度的時候,你考慮使用統一的服務來獲取用戶信息,這就不是緩存那么簡單了,里面的文章很多,我相信現在大規模的門戶網站都有自己的一套復雜的機制,所以表明上看RESTful Web API很“低效”,但這種RESTFul的思路和模式卻在實際中有很大的可塑性和威力。
這種方法應該足夠安全了!
密碼根本沒有在網絡上傳輸,密文采用的是非驗證的對稱加密,沒有密鑰就無法逆轉,URL驗證避免了傳統的身份挾持攻擊(即攔截一個用戶的包並冒充此用戶來訪問其它的資源,即便無法破解用戶密碼), 再用GUID來避免了重發攻擊,唯一需要擔心的是用戶泄露了自己的密碼。
元芳,你怎么看?