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

網易考拉移動端團隊發表於2018-08-10

前言

談起Cookie,如果沒有了解過它,可能會望文生畏。做過WebView開發的人可能會對它比較瞭解。Android的Cookie是由系統去管理的,其特點是會被持久化成一個db檔案,儲存在/data/data/{packageName}/app_webview/Cookies中(不同系統、不同瀏覽器實現可能不一樣,但大體如此)。通常,網站的登入資訊是使用Cookie來儲存的,如果App也是使用Cookie來實現鑑權,那麼在WebView和App之間就需要建立一套Cookie同步機制。

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

同程式Cookie同步

如果App和WebView處於同一個程式,那麼實現起來是比較簡單的,可以參考這篇文章,程式碼不做過多解釋,以okhttp為例:

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…

多程式Cookie同步

但是如果App和WebView處於不同的程式,事情就沒那麼簡單了。由於不同程式之間資料是不共享的,程式之間的Cookie同步就成了一個問題。隨後的測試發現,App的多程式間是共享同一個Cookies檔案的,但程式之間的Cookie資料不一定能夠實時同步。我們遇到的問題是,WebView程式訪問攜帶了特定Cookie的url後,這些Cookie並沒有同步到主程式。於是,帶著層層疑問,我們開始了程式間同步Cookie的猜想實驗。考慮一下兩個程式間可能導致Cookie資料不一致的地方(以下假設App在A程式,WebView在B程式):

  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檔案。

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

WebView訪問一個url,B程式的WebView寫入Cookie以後,沒有立即寫入Cookies.db持久化,導致A程式讀取不到最新的Cookie。

WebView在載入url時,服務端返回需要寫入的Cookie可以使用Chrome Inspect來檢視。針對WebView的Cookie持久化時機,我們可以做一個簡單的實驗。

實驗步驟:
1、使用WebView載入url;
2、載入完成後(呼叫WebViewClient.onPageFinished()),拿到Cookie檔案,檢視是否有寫入Cookie。

https://m.baidu.com為例,未載入WebView元件之前,我們可以找一臺root過的手機,檢視/data/data/{packageName}目錄下是沒有app_webview目錄的。

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

載入url以後,可以使用chrome inspect檢視Cookie資訊,m.baidu.com會生成以下Cookie:

baidu_cookie

此時再次訪問上述目錄,可以發現app_webview目錄已經存在了,並且生成了Cookie檔案。說明在第一次開啟WebView載入完https://m.baidu.com的時候就已經生成了Cookie並且持久化,

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

為了進一步證實,我們匯出/data/data/{packageName}/app_webview/Cookies檔案,並檢視是否包含上面的Cookie,來證實Cookie是否有被持久化。

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

結果顯而易見——Cookie在WebView載入完成url以後幾乎是立即持久化的,我們的第一個猜想不成立。

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

由於Cookie是和WebView掛鉤的,可能需要在A程式建立一個WebView來讓Cookie在程式間同步。

我們知道,WebView的Cookie是交由系統去管理的[^1],WebView在例項化過程中可能對Cookie進行一定的操作。如果沒有例項化WebView,是不是Cookie就同步不過來呢?基於這個猜想,我們進行第二次實驗。

實驗步驟:
1、B程式載入https://m.baidu.com後,在B程式使用CookieManager檢視m.baidu.com的Cookie;
2、A程式例項化WebView,不載入,然後在A程式使用CookieManager檢視m.baidu.com的Cookie;
3、B程式再次使用WebView載入https://m.taobao.com,在B程式檢視m.taobao.com的Cookie;
4、A程式再次例項化WebView,不載入,在A程式檢視m.taobao.com的Cookie。

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

記一次多程式同步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);
複製程式碼

實驗步驟:
1、在Application啟動的時候呼叫CookieManage.getInstance().setAcceptCookie(true); 2、重複猜想二的實驗步驟,觀察A程式和B程式的Cookie同步情況。 3、在Application啟動的時候呼叫CookieManage.getInstance().setAcceptCookie(false); 4、再次重複猜想二的步驟。

無論是否設定允許記錄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,測試二的結論說明了這一點。為了讓事實更具有說服力,還是以實驗說明這一點。

實驗步驟:
1、B程式訪問https://m.baidu.com
2、儲存Cookie檔案的最後修改時間;
3、A程式再次訪問https://m.baidu.com(或者別的url也可以);
4、檢視Cookie檔案的最後修改時間並與步驟二的進行比對。

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

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

說明App裡的不同程式使用的是同一個Cookie檔案進行讀取和寫入。第五個猜想不成立。

第六個猜想——Cookie同程式同域訪問

A程式建立了WebView並且訪問了同域的url,然後覆蓋了B程式之前已經持久化的Cookie

由第五個猜想的實驗結果可知,不同程式間是使用同一個Cookie檔案進行持久化。如果A程式和B程式都允許寫Cookie,那麼程式間就可能產生Cookie覆蓋的現象。我們可以測試一下。

實驗步驟:
1、使用B程式WebView開啟https://m.baidu.com,記錄當前的Cookie檔案;
2、使用A程式WebView開啟https://m.baidu.com,記錄當前的Cookie檔案;
3、對第一步和第二步的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的讀取優先使用記憶體中的快取,那麼上面的現象便可以解釋得通了。還是通過實驗來驗證。

實驗步驟:
1、A程式未初始化CookieManager的情況下,使用程式B訪問https://m.baidu.com,Cookie持久化後,然後分別在初始化A程式的CookieManager前後,檢視A程式的Cookie情況;接著再使用程式B訪問https://m.taobao.com,Cookie持久化後,再次檢視A程式的Cookie情況。
2、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同步方案,因此採用了第二種方案。

參考連結

相關文章