记一次多进程同步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同步方案,所以采用了第二种方案。

参考连接

相关文章
相关标签/搜索