如何實現RESTful Web API的身份驗證
最近想拿一個小專案來試水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來避免了重發攻擊,唯一需要擔心的是使用者洩露了自己的密碼