記一次多進程同步Cookie的解惑歷程

前言

談起Cookie,若是沒有了解過它,可能會望文生畏。作過WebView開發的人可能會對它比較瞭解。Android的Cookie是由系統去管理的,其特色是會被持久化成一個db文件,保存在/data/data/{packageName}/app_webview/Cookies中(不一樣系統、不一樣瀏覽器實現可能不同,但大致如此)。一般,網站的登陸信息是使用Cookie來保存的,若是App也是使用Cookie來實現鑑權,那麼在WebView和App之間就須要創建一套Cookie同步機制。html

儘管考拉的鑑權機制不是使用Cookie來實現的,但咱們也遇到了相似的需求,使用WebView打開一個特定的url,這個url的響應會寫入指定的Cookie,而後url通過一次302重定向,通過url攔截後打開一個App頁面,並把url響應中攜帶的Cookie帶到這個App頁面。java

同進程Cookie同步

若是App和WebView處於同一個進程,那麼實現起來是比較簡單的,能夠參考這篇文章,代碼不作過多解釋,以okhttp爲例:android

import android.webkit.CookieManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;

/**
 * Provides a synchronization point between the webview cookie store and okhttp3.OkHttpClient cookie store
 */
public final class WebviewCookieHandler implements CookieJar {
    private CookieManager webviewCookieManager = CookieManager.getInstance();

    @Override
    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
        String urlString = url.toString();

        for (Cookie cookie : cookies) {
            webviewCookieManager.setCookie(urlString, cookie.toString());
        }
    }

    @Override
    public List<Cookie> loadForRequest(HttpUrl url) {
        String urlString = url.toString();
        String cookiesString = webviewCookieManager.getCookie(urlString);

        if (cookiesString != null && !cookiesString.isEmpty()) {
            //We can split on the ';' char as the cookie manager only returns cookies
            //that match the url and haven't expired, so the cookie attributes aren't included
            String[] cookieHeaders = cookiesString.split(";");
            List<Cookie> cookies = new ArrayList<>(cookieHeaders.length);

            for (String header : cookieHeaders) {
                cookies.add(Cookie.parse(url, header));
            }

            return cookies;
        }

        return Collections.emptyList();
    }
}
複製代碼

代碼來自 gist.github.com/justinthoma…git

多進程Cookie同步

可是若是App和WebView處於不一樣的進程,事情就沒那麼簡單了。因爲不一樣進程之間數據是不共享的,進程之間的Cookie同步就成了一個問題。隨後的測試發現,App的多進程間是共享同一個Cookies文件的,但進程之間的Cookie數據不必定可以實時同步。咱們遇到的問題是,WebView進程訪問攜帶了特定Cookie的url後,這些Cookie並無同步到主進程。因而,帶着層層疑問,咱們開始了進程間同步Cookie的猜測實驗。考慮一下兩個進程間可能致使Cookie數據不一致的地方(如下假設App在A進程,WebView在B進程):github

  1. WebView訪問一個url,B進程的WebView寫入Cookie之後,沒有當即寫入Cookies.db持久化,致使A進程讀取不到最新的Cookie;
  2. 因爲Cookie是和WebView掛鉤的,可能須要在A進程建立一個WebView來讓Cookie在進程間同步;
  3. A進程須要調用CookieManager.getInstance().setAcceptCookie(true)保證A進程可以讀取到Cookie;
  4. B進程的Cookie可能失效了,致使A進程讀取不到Cookie(後面解釋爲何會出現這種狀況);
  5. A進程和B進程的Cookie文件根本不是同一個,致使數據沒法同步;
  6. A進程建立了WebView而且訪問了同域的url,而後沖掉了B進程以前已經持久化的Cookie;
  7. Cookie是經過CookieManager管理的,CookieManager是個單例,可能只會讀取一次Cookies.db,而後緩存在內存中;

下面咱們一一分析上述7種狀況,並加以條件進行測試。須要說明的是,爲了避免影響每次實驗的結果,都須要在加載url以前,清空/data/data目錄下的Cookie文件。web

第一個猜測——Cookie持久化時間

WebView訪問一個url,B進程的WebView寫入Cookie之後,沒有當即寫入Cookies.db持久化,致使A進程讀取不到最新的Cookie。chrome

WebView在加載url時,服務端返回須要寫入的Cookie可使用Chrome Inspect來查看。針對WebView的Cookie持久化時機,咱們能夠作一個簡單的實驗。瀏覽器

實驗步驟:
一、使用WebView加載url;
二、加載完成後(調用WebViewClient.onPageFinished()),拿到Cookie文件,查看是否有寫入Cookie。緩存

https://m.baidu.com爲例,未加載WebView組件以前,咱們能夠找一臺root過的手機,查看/data/data/{packageName}目錄下是沒有app_webview目錄的。bash

加載url之後,可使用chrome inspect查看Cookie信息,m.baidu.com會生成如下Cookie:

baidu_cookie

此時再次訪問上述目錄,能夠發現app_webview目錄已經存在了,而且生成了Cookie文件。說明在第一次打開WebView加載完https://m.baidu.com的時候就已經生成了Cookie而且持久化,

爲了進一步證明,咱們導出/data/data/{packageName}/app_webview/Cookies文件,並查看是否包含上面的Cookie,來證明Cookie是否有被持久化。

結果顯而易見——Cookie在WebView加載完成url之後幾乎是當即持久化的,咱們的第一個猜測不成立。

第二個猜測——Cookie同步條件

因爲Cookie是和WebView掛鉤的,可能須要在A進程建立一個WebView來讓Cookie在進程間同步。

咱們知道,WebView的Cookie是交由系統去管理的[^1],WebView在實例化過程當中可能對Cookie進行必定的操做。若是沒有實例化WebView,是否是Cookie就同步不過來呢?基於這個猜測,咱們進行第二次實驗。

實驗步驟:
一、B進程加載https://m.baidu.com後,在B進程使用CookieManager查看m.baidu.com的Cookie;
二、A進程實例化WebView,不加載,而後在A進程使用CookieManager查看m.baidu.com的Cookie;
三、B進程再次使用WebView加載https://m.taobao.com,在B進程查看m.taobao.com的Cookie;
四、A進程再次實例化WebView,不加載,在A進程查看m.taobao.com的Cookie。

咱們看到一個有趣的現象:

首次實例化A進程的WebView時,能夠拿到B進程以前寫入的Cookie。但當B進程再次寫入其餘Cookie時,此時再實例化A進程的WebView卻取不到了。這個過程可能說明了只有在第一次實例化WebView的時候纔會去同步持久化的Cookie,當Cookie再次更新時,別的進程讀取不到更新後的Cookie數據。第二個猜測不成立。

第三個猜測——setAcceptCookie(true)

A進程須要調用CookieManager.getInstance().setAcceptCookie(true)保證A進程可以讀取到Cookie。

既然須要使用到Cookie,而進程是否默認容許記錄Cookie是個未知的行爲,索性咱們能夠測試一下,強制讓進程容許記錄Cookie。可使用以下代碼:

CookieManage.getInstance().setAcceptCookie(true);
複製代碼

實驗步驟:
一、在Application啓動的時候調用CookieManage.getInstance().setAcceptCookie(true); 二、重複猜測二的實驗步驟,觀察A進程和B進程的Cookie同步狀況。 三、在Application啓動的時候調用CookieManage.getInstance().setAcceptCookie(false); 四、再次重複猜測二的步驟。

不管是否設置容許記錄Cookie,測試結果和猜測二的結果同樣,圖就不貼了,說明Cookie在進程間的同步和是否容許記錄Cookie無關。第三個猜測不成立。

第四個猜測——Cookie失效問題

B進程的Cookie可能失效了,致使A進程讀取不到Cookie。

B進程的Cookie可能失效了,致使A進程讀取不到Cookie。出現這個猜測的緣由是咱們使用chrome inspect查看Cookie時,它顯示的時間的確是過時了的,好比剛纔訪問的https://m.baidu.com

baidu_cookie_expired

有一條Cookie的時間表示爲2019-04-28T05:38:12.000Z,可是注意到時間最後的字母Z,它表示的是GMT/UTC時間裏的GMT+0時區[^2]。轉換成北京時間(GMT+8)後,就是下午1點38分。

gmt_time_converter

說明這條Cookie仍是有效的,排除了因爲Cookie失效致使A進程訪問不到的可能。另外,在Android中,即便Cookie已經失效,也可以經過CookieManager.getInstance().getCookie(url)取得,而且該方法返回一個字符串,不包含Cookie的Expires字段。第四個猜測不成立。

第五個猜測——Cookie文件進程讀取

A進程和B進程的Cookie文件根本不是同一個,致使數據沒法同步。

A進程和B進程的Cookie文件根本不是同一個,致使數據沒法同步。通過上面的猜測和實驗,其實能夠說明這個猜測是不成立的,若是進程讀取的Cookie文件不是同一個的話,那麼在B進程訪問https://m.baidu.com後,A進程不可能拿到B進程的WebView寫入的Cookie,測試二的結論說明了這一點。爲了讓事實更具備說服力,仍是以實驗說明這一點。

實驗步驟:
一、B進程訪問https://m.baidu.com
二、保存Cookie文件的最後修改時間;
三、A進程再次訪問https://m.baidu.com(或者別的url也能夠);
四、查看Cookie文件的最後修改時間並與步驟二的進行比對。

咱們分別在14:06的B進程和14:08的A進程訪問了https://m.baidu.com,結果以下:

說明App裏的不一樣進程使用的是同一個Cookie文件進行讀取和寫入。第五個猜測不成立。

第六個猜測——Cookie同進程同域訪問

A進程建立了WebView而且訪問了同域的url,而後覆蓋了B進程以前已經持久化的Cookie

由第五個猜測的實驗結果可知,不一樣進程間是使用同一個Cookie文件進行持久化。若是A進程和B進程都容許寫Cookie,那麼進程間就可能產生Cookie覆蓋的現象。咱們能夠測試一下。

實驗步驟:
一、使用B進程WebView打開https://m.baidu.com,記錄當前的Cookie文件;
二、使用A進程WebView打開https://m.baidu.com,記錄當前的Cookie文件;
三、對第一步和第二步的Cookie文件進行對比。

baidu_cookie_b
(B進程訪問 https://m.baidu.com

baidu_cookie_a
(A進程訪問 https://m.baidu.com

從圖中能夠看到,B進程訪問url後的Cookie和A進程訪問url後的Cookie數據幾乎是一致的,只有一列不同——last_access_utc。咱們猜想這個字段表示上一次成功讀取/寫入該Cookie的時間(沒有找到相關的文檔介紹),但至少說明Cookies這個文件發生了覆蓋,也就是說,App裏的不一樣進程對同一個域訪問,可能會形成Cookie覆蓋

即使如此,到目前爲止,尚未可以解釋B進程的部分Cookie在A進程獲取不到的現象。

第七個猜測——CookieManager的鍋

Cookie是經過CookieManager管理的,CookieManager是個單例,單個進程可能只會讀取一次Cookies.db,而後緩存在內存中。

Android中全部與Cookie的操做都與CookieManager有關,上面的幾種猜測都沒有考慮到CookieManager的問題,CookieManager是一個單例,一旦建立,除非進程被清除,不然便不會銷燬。若是說CookieManager只有在建立時纔讀取一次Cookies.db文件,後面對Cookie的讀取優先使用內存中的緩存,那麼上面的現象即可以解釋得通了。仍是經過實驗來驗證。

實驗步驟:
一、A進程未初始化CookieManager的狀況下,使用進程B訪問https://m.baidu.com,Cookie持久化後,而後分別在初始化A進程的CookieManager先後,查看A進程的Cookie狀況;接着再使用進程B訪問https://m.taobao.com,Cookie持久化後,再次查看A進程的Cookie狀況。
二、A進程未初始化CookieManager的狀況下,使用進程B訪問https://m.baidu.comhttps://m.taobao.com,Cookie持久化後,初始化A進程的CookieManager,並查看A進程的Cookie狀況。

cookie_manager_problem

結果證明了猜測!CookieManager在未初始化時取不到m.baidu.com的Cookie,一旦初始化了CookieManager,則可以取到m.baidu.com的Cookie。但步驟二再一次說明,只要初始化了CookieManager,那麼該進程的Cookie再也取不到其餘進程更新後的Cookie信息

多進程Cookie同步結論

至此,多進程下Cookie同步問題的猜測所有驗證完畢了,能夠得出的結論是——Cookie在多進程間的獲取只和第一次初始化CookieManager有關係,一旦CookieManager實例建立,則須要重啓進程才能同步進程間的Cookie。

回到本文遇到的問題,既然問題的緣由已經找到了,那麼確定有解決辦法。一種不完美的方案是先啓動B進程並加載url,等到加載完成即將跳轉到App頁面的時候通知主進程初始化CookieManager,這樣即可以取到url中指定的Cookie信息。這種方案的缺點是再次訪問這個url寫入新的指定Cookie時不會當即同步到主進程,須要等到App重啓主進程之後纔會同步;另一種解決方案是把WebView和App都放在主進程便可。本文最終因爲沒有可以完美解決多進程Cookie同步方案,所以採用了第二種方案。

參考連接

相關文章
相關標籤/搜索