前言
很高興遇見你~
cookie在HTTP1.1版本中被添加,目的是為了解決HTTP的無狀態特性,使HTTP變得“有狀態”。
我們在做android開發,很多時候并不能很好理解cookie的存在價值、優化。這其實正常。Http中文翻譯為超文本傳輸協議,是為web開發的傳輸協議。而cookie作為其中的一個功能,他的設計自然也是服務于web。為了更好地理解cookie他的本質,需要站在web開發的角度來理解他,具體而言就是使用瀏覽器網頁具體化客戶端。
android的發展要比web晚的多。雖然都使用cookie,但cookie相對于android并沒有跟web一樣有那么多的好處。但cookie也并不是完全沒用,例如我們都知道cookie可以實現記住登錄狀態。
這篇的文章的目的在于幫助android開發者理解cookie的本質是什么,這樣我們在使用cookie的時候會更加心里有底。
購物車的故事
我們都知道,HTTP是一個無狀態的協議。無狀態體現在每一次的請求與響應都是獨立的,他們之間不會互相記錄、互相影響。
嗯?我們在后端不是會維護購物車的狀態,提供api給客戶嗎?是的,可以有,我們后端完全可以使用數據結構來存儲購物車信息。但這不屬于HTTP的范疇了,單純的HTTP是無法實現這樣的功能的,是吧?HTTP的每一次請求都是獨立、不互相影響的,他不會記得上一次訪問發送了什么返回了什么。
為了記住操作狀態,http設計者在請求頭中附加一些信息,來識別我們的操作狀態:
客戶端每次操作購物車,服務端都會為其生成一個字段,并把這個字段發送給客戶端。這個字段存儲了客戶端前面的操作狀態,下次客戶端請求服務端的時候只需要把這個字段附加在請求頭中,服務端通過解析這個字段的內容,就知道前面的請求都做了什么。
例如上圖,客戶端添加了一個橘子,服務端生成 【orange=1】 并發揮給客戶端存儲??蛻舳讼乱淮尾僮髦恍枰?【orange=1】 附加在請求頭中,服務端就知道客戶已經添加一個橘子在購物車了。
到這里已經很明顯了,這個字段就是cookie。
cookie由服務端生成,在客戶端中存儲;客戶端每次請求附加上cookie,服務端通過解析cookie的內容實現狀態記錄。
整個cookie的機制中,客戶端做的事情很簡單:存儲cookie,附加cookie。客戶端不參與cookie的生成,也不參與cookie的修改。而服務端則負責cookie的生成與解析,但是不負責存儲。
cookie的實現
cookie在HTTP中通過兩個頭部字段來實現:cookie和set-cookie。
set-cookie
在響應報文中附帶,告知客戶端需要存儲該cookie。例如set-cookie:orange=1
。cookie
是請求報文頭部的一個字段,附帶上服務端生成的cookie,例如cookie:orange=1
。
sequenceDiagram
客戶端->>服務端: 添加一個橘子
服務端-->>客戶端: set-cookie:orange=1
客戶端->>服務端: cookie:orange=1, 添加兩個香蕉
服務端-->>客戶端: set-cookie:orange=1&bananer=2
在cookie機制中,客戶端和服務端都必須各司其職。
- 客戶端,具體而言就是瀏覽器,他需要自動把一個網站的生成的cookie存儲下來,下一次請求自動把cookie附加在請求頭中。
- 服務端,在收到cookie字段時,需要自動解析并響應對應的結果。如收到orange=1必須自動解析出來添加了一個橘子,再次添加香蕉就是orange=1&bananer=2。
同時,cookie是可以擁有多個的,也就是可以不止有一個cookie,這樣一個網站就可以使用cookie同時記錄多個狀態。我這里我們使用postman看一下百度網站的cookie:
可以看到百度返回了多達6個set-cookie。此外,我們可以通過瀏覽器來查看一個網站的cookie:
可以看到百度這個網站累積使用多達37個cookie。點擊cookie就可以查看具體的內容了。
同時需要注意的是,cookie一般不會明文在網絡中傳輸,而是會進行加密。這在早期沒有https的情況下是非常重要的,否則網絡中任何節點都有可能劫持到我們的cookie。上面再postman中百度網站的Set-Cookie字段就可以很明顯看的出來是經過了加密。
下面我們再通過兩個例子來進一步理解cookie。
主題偏好功能
有一些網站具有的一個功能是:不需要登錄,但是卻可以記住我們的選擇的主題,例如暗色或亮色;下一次訪問還是我們上次的選擇。我們當然可以在后端為每一個ip和端口記錄選擇結果,但越來越多的訪問量占用的空間很大、查詢的性能也收到了影響。這個時候就輪到cookie上場了,使用cookie即可更加輕量級地實現這個功能。他的功能模型如下圖:
sequenceDiagram
客戶端->>服務端: 設置暗色主題
服務端-->>客戶端: set-cookie:theme=dark
客戶端->>服務端: cookie:theme=dark
服務端-->>客戶端: 返回暗色主題的網頁
當我們設置主題的時候,服務端會生成一個主題偏好的cookie交給我們存儲。下一次只需要把cookie附加在請求頭中,服務端解析cookie中的內容,就可以返回對應主題的網頁了。相比與在服務端使用數據結構來存儲用戶的信息,這種方式是不是更加輕量、更加簡單?且無需登錄注冊既可以記住自己的主題偏好。
這種功能在android中似乎不太實用,因為我們的主題使用的是本地的配置,界面的設計內容也都是存儲在本地,因而無需服務端來為我們記住主題偏好問題。
但站在web的角度這個功能就非常實用了。網頁的具體內容都是存儲在服務端,而瀏覽器只負責渲染界面。這個時候需要cookie來告訴服務端,上次我選擇了什么樣的主題,這次你也給我返回一樣主題的網頁界面。
記住登錄狀態
嘿,登錄注冊,我們android工程師,就很好理解了。畢竟第一次接觸到cookie可能都是使用他來實現記住登錄狀態,筆者就是如此。當然,在商業項目中,由于cookie設計的不安全性,并不會拿來當記住登錄狀態的手段,這是后話了,屬于登錄注冊更加具體的內容。先來看看cookie是如何實現記住登錄狀態的:
sequenceDiagram
客戶端->>服務端: user=admin&password=123123
服務端-->>客戶端: set-cookie:sessionId=ABC123
客戶端->>服務端: cookie:sessionId=ABC123
服務端-->>客戶端: 返回暗色主題的網頁
- 當我們訪問服務端進行登錄之后,服務端會為我們創建一個session,會話。
- 服務端把sessionId放在cookie中返回給我們。
- 服務端下一次請求服務器的時候把cookie附加在請求頭中。
- 服務端通過解析其中的sessionId,找到對應的session,就知道我們已經登錄了且可以識別我們的身份,因為我們的登錄信息都記錄在session中。
此外還有另外一種比較方便但不太安全記住登錄的方式:直接把用戶名和密碼通過某種加密手段加密后存儲在cookie中,交由瀏覽器存儲。這種非常簡單粗暴,對于加密算法的要求比較高。通常這種類型的cookie都有一個有效期,過期之后則必須重新登錄。
在web的環境下,使用cookie來記住登錄還是不太安全的,下面了解一下cookie具有哪些缺點。
cookie的缺點
cookie的缺點在于他的 自動 特性,無論是瀏覽器自動存儲和附加cookie,還是服務器自動識別cookie并直接響應,都是不安全的。舉幾個例子來理解一下。
假如一個銀行網站,使用cookie來記錄登錄狀態。當我們訪問攻擊者的網站是,網站的js腳本可以直接訪問該銀行的轉賬接口。瀏覽器會自動幫我們把銀行網站的cookie附加上,而銀行服務端解析到我們的cookie,判斷在登錄中,就直接把錢轉賬了。
又比如,本地js腳本拿到我們存儲的cookie之后,就可以代替我們的身份去訪問各個網站。和上面一樣,服務端只要看到cookie,就判斷是我們本人在操作了。
此外,web bugs這一類的問題也導致了我們的隱私泄露問題--簡單來說就是把我們的訪問網站信息存儲在cookie中,然后請求收集信息的服務器,該服務器通過解析cookie就可以收集到我們的瀏覽信息,可以給我們精準投遞廣告。這種方案也常用語瀏覽器行為跟蹤,在合法的范圍內有助于提高我們的網絡服務質量,但難免會有不法分子整一些不好的操作。
針對這些缺點,HTTP也進行了一些優化。例如httpOnly頭部,設置有這個頭部的響應報文,會讓本地js腳本無法拿到cookie,從而保障了安全。但這個特性需要瀏覽器進行配合,如果瀏覽器沒有這個實現,依舊是不安全的。
但這些缺點,在android這個環境中是天然安全的。因為我們的app不會被植入腳本代碼,也不存在被其他的程序拿到cookie的情況。
在android中使用
首先明確一點是,客戶端對于cookie的操作是很有限的。我們只需要負責兩件事:對網站的cookie進行存儲,在請求的時候附加上cookie。
這里需要注意的是不同網站的cookie是需要分開的,不要把邏輯寫死每次都附加上所有的cookie。但對于自家的app一般后端只有自家的服務器,那么這個也就相對來說無關緊要。
android開發的網絡框架一般用的都是okHttp,下面分析一下如何使用okHttp來進行cookie操作。okHttp框架默認是不實現cookie存儲的。如果需要操作cookie,那么有兩個方法:使用okHttp的cookie存儲接口、使用攔截器。
使用cookie存儲接口
okHttp預留了一個接口讓我們可以很方便地進行cookie操作。我們可以通過調用okHttpClient的方法來實現,如下代碼:
val okHttpBuilder = OkHttpClient.Builder()
okHttpBuilder.apply {
cookieJar(object : CookieJar{
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
// 實現存儲cookie的邏輯
}
override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
// 實現添加cookie的邏輯
}
})
}
CookieJar
是一個接口,這個接口有兩個方法分別對應于存儲和添加cookie。在收到響應報文時,okHttp會把響應頭部的cookie取出來,并回調saveFromResponse
方法;在發起一個請求的時候,會調用loadForRequest
來返回一個cookie列表,用于添加到請求頭部中。
因此我們只需要在創建OkHttpClient的時候,設置好cookie的回調監聽即可。
這里需要注意的是,不管是直接使用OkHttp還是Retrofit,都盡量保持OkHttpClient全局單例,這樣配置的cookie邏輯才不會失效。Retrofit可通過下面的方法來自定義okHttpClient:
mRetrofit = Retrofit.Builder()
.client(okHttpBuilder.build())
.baseUrl(BASE_URL)
.build()
使用攔截器
okHttp在發送一個請求會經歷一系列的攔截器。攔截器可以簡單理解為,每一次請求發出去會經過我們配置的攔截器,返回的時候也會經過我們設置的攔截。這樣我們就可以在請求時把請求攔截下來添加cookie之后再發送出去;然后在響應的時候,把cookie取下來,再把響應報文返回。如下代碼:
class CookieInterceptor() :Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val requestBuilder = chain.request().newBuilder()
// 為請求添加cookie
addCookie(requestBuilder)
// 執行請求
var response = chain.proceed(requestBuilder.build())
// 為響應存儲cookie
storeCookie(response)
return response
}
}
代碼中request
就是我們的請求,調用proceed
方法之后就會發起請求并拿到response,之后我們把resp中的cookie取下來,再把response返回即可。addCookie
和storeCookie
方法需要我們自己去實現,具體怎么實現就比較靈活了,Room、SharePreference都是可以的。
然后通過配置client來添加攔截器:
val okHttpBuilder = OkHttpClient.Builder()
okHttpBuilder.apply {
addInterceptor(CookieInterceptor()))
}
這里我們為OkHttpClient添加了一個我們自定義的攔截器了。
這種方法比第一種直接使用cookieJar要復雜一點,但攔截器的能做的事情比較多,更加靈活。攔截器可以修改一個request中的所有內容,例如我把baidu.com
全部重定向到google.com
,攔截器是可以做到的,但是cookieJar只專與cookie存儲。
關于攔截器的方面就不展開了,感興趣的讀者可以深入去了解一下。
最后
與HTTP相關的很多東西,在一定程度上都是為web端設計。學習的時候覺得云里霧里,可能是打開的方式不對。了解一點前端的知識,從web的角度來理解,再運用到android開發中,會是一個更好的姿勢。
cookie的功能更多的還是需要和后端配合,cookie本身只是服務端生成,客戶端存儲,自動附加與解析的一個字段。在此之上要建立什么功能,則由開發者而定了。
針對于前端而言,這些關于cookie的知識肯定是不夠的,但對于android工程師已經差不多,常規的業務開發也已經游刃有余了。
金三銀四,最近大家也都在春招吧,那就預祝各位可以順利上岸,拿到喜歡的大廠offer。
文章如果有幫助,還希望可以點個贊鼓勵一下作者~
全文到此,原創不易,覺得有幫助可以點贊收藏評論轉發。
有任何想法歡迎評論區交流指正。如需轉載請評論區或私信告知。
另外歡迎光臨筆者的個人博客:傳送門