Android OkHttp Cookie持久化问题总结

说明

最近封装一个SDK时,遇到一个需求就是登录成功之后,APP需要持久保存Cookie,当APP退出再进入时需要从本地读取Cookie值,类似于浏览器,一个网站登录成功之后,关闭浏览器再打开,还能继续访问这个网站网页。

Cookie

图片来源:https://www.cnblogs.com/zhuanzhuanfe/p/8010854.html

分析

首先我们清除谷歌浏览器里面缓存的Cookie,当首次访问百度https://www.baidu.com/,请求体中还没有携带Cookie,响应体中会出现Set-Cookie字段,要求浏览器保存Cookie,当第二次请求时会携带这个Cookie信息。

请求头(第一次请求):

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Host: www.baidu.com
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3472.3 Safari/537.36

响应头:

Bdpagetype: 1
Bdqid: 0xe1a8fd3600011fd8
Cache-Control: private
Connection: Keep-Alive
Content-Encoding: gzip
Content-Type: text/html
Cxy_all: baidu+c1a146ec227bccffbb8afe4da97bdf3e
Date: Sat, 06 Apr 2019 09:48:35 GMT
Expires: Sat, 06 Apr 2019 09:47:45 GMT
P3p: CP=" OTI DSP COR IVA OUR IND COM "
Server: BWS/1.1
Set-Cookie: PSTM=1554544115; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: BAIDUID=F7EBDE8F1230A7DDF1DD141A458BD04B:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: BIDUPSID=F7EBDE8F1230A7DDF1DD141A458BD04B; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: delPer=0; path=/; domain=.baidu.com
Set-Cookie: BDSVRTM=0; path=/
Set-Cookie: BD_HOME=0; path=/
Set-Cookie: H_PS_PSSID=1439_28794_21081_28774_28721_28558_28585_26350_28604_28625_22159; path=/; domain=.baidu.com
Strict-Transport-Security: max-age=172800
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-Ua-Compatible: IE=Edge,chrome=1

请求头(第二次请求):
里面携带Cookie信息

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: max-age=0
Connection: keep-alive
Cookie: BAIDUID=F7EBDE8F1230A7DDF1DD141A458BD04B:FG=1; BIDUPSID=F7EBDE8F1230A7DDF1DD141A458BD04B; PSTM=1554544115; delPer=0; BD_HOME=0; H_PS_PSSID=1439_28794_21081_28774_28721_28558_28585_26350_28604_28625_22159; BD_UPN=12314353
Host: www.baidu.com
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3472.3 Safari/537.36

现象

使用的是鸿洋的 okhttputils网络框架,PersistentCookieStore其中存在一个bug;github上也有类似的问题https://github.com/hongyangAndroid/okhttputils/pull/140

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .connectTimeout(10000L, TimeUnit.MILLISECONDS)
                .readTimeout(10000L, TimeUnit.MILLISECONDS)
                .cookieJar(new CookieJarImpl(new PersistentCookieStore(this)))
//              .cookieJar(new CookieJarImpl(new MemoryCookieStore()))
                .addInterceptor(new LoggerInterceptor("TAG"))
                .build();

OkHttpUtils.initClient(okHttpClient);
String top250 = "http://api.douban.com/v2/movie/top250";
// 配置基本网络请求
OkHttpUtils.get().url(top250)
        .build()
        .execute(new StringCallback() {
            @Override
            public void onError(Call call, Exception e, int id) {
                Log.d(TAG, " 失败:" + e.toString());
            }

            @Override
            public void onResponse(String response, int id) {
                Log.d(TAG, " 成功:" + response);
            }
        });

当设置内存保存Cookie时(MemoryCookieStore),第二次访问携带上Cookie,但是退出APP之后就丢失了。

当设置永久保存Cookie时(PersistentCookieStore),第二次访问还是没有携带上Cookie,

image.png
PersistentCookieStore代码实现
persistent值

从源码上可以看出,当请求头中存在expires和max-age时,返回为True,这个时候PersistentCookieStore是不对Cookie进行磁盘、内存存储的,这里只是设置一个Cookie的有效期,此时Cookie值并没有过期。

维持持久化Cookie,推荐使用持久化cookie框架,PersistentCookieJar

ClearableCookieJar cookieJar =
                new PersistentCookieJar(new SetCookieCache(), new SharedPrefsCookiePersistor(this));

OkHttpClient okHttpClient = new OkHttpClient.Builder()
        .connectTimeout(10000L, TimeUnit.MILLISECONDS)
        .readTimeout(10000L, TimeUnit.MILLISECONDS)
        .cookieJar(cookieJar)
//         .cookieJar(new CookieJarImpl(new PersistentCookieStore(this)))
//         .cookieJar(new CookieJarImpl(new MemoryCookieStore()))
        .addInterceptor(new LoggerInterceptor("TAG"))
        .build();

OkHttpUtils.initClient(okHttpClient);
Cookie未保存
Cookie过滤条件
persistent
Cookie判断

从源码上可以看出,当请求头中不存在expires和max-age时,返回为False,这个时候PersistentCookieJar是不对Cookie进行磁盘存储的。

另外一种情况

okttp3访问IP地址Cookie丢失的现象,这里使用百度的IP地址:http://220.181.112.244:80/,

//这里使用百度IP地址
String baidu = "http://220.181.112.244:80/";
// 配置基本网络请求
OkHttpUtils.get().url(baidu)
        .build()
        .execute(new StringCallback() {
            @Override
            public void onError(Call call, Exception e, int id) {
                Log.d(TAG, " 失败:" + e.toString());
            }

            @Override
            public void onResponse(String response, int id) {
                Log.d(TAG, " 成功:" + response);
            }
        });
丢失Cookie情况

查看OkHttp-3.3.1底层Cookie实现,可以看到这一部分代码:

...
  } else if (attributeName.equalsIgnoreCase("domain")) {
        try {
          domain = parseDomain(attributeValue);
          hostOnly = false;
        } catch (IllegalArgumentException e) {
          // Ignore this attribute, it isn't recognizable as a domain.
        }
 }
...

 // If the domain is present, it must domain match. Otherwise we have a host-only cookie.
    if (domain == null) {
      domain = url.host();
    } else if (!domainMatch(url, domain)) {
      return null; // No domain match? This is either incompetence or malice!
    }

...

    for (int i = 0, size = cookieStrings.size(); i < size; i++) {
      Cookie cookie = Cookie.parse(url, cookieStrings.get(i));
      if (cookie == null) continue;
      if (cookies == null) cookies = new ArrayList<>();
      cookies.add(cookie);
    }

当请求头中存在domain时,这个时候主地址为ip与domian不等,Cookie解析失败为null,导致保存Cookie失败,这个浏览器也是存在问题的,这个得后台注意格式。

浏览器情况

代码实现

第一种实现方式(拦截器实现)

这里为了安全可以对Cookie进行加密存储,可以使用这个SharedPreferences加密库,https://github.com/iamMehedi/Secured-Preference-Store

 mSharedPreferences = getSharedPreferences("Cookie_Pre", Context.MODE_PRIVATE);
        cookies = new HashMap<>();
OkHttpClient okHttpClient = new OkHttpClient.Builder()
        .connectTimeout(10000L, TimeUnit.MILLISECONDS)
        .readTimeout(10000L, TimeUnit.MILLISECONDS)
        //网络拦截器
        .addInterceptor(new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                //获取请求链接
                Request originalRequest = chain.request();
                //获取url的主机地址
                String hostString = originalRequest.url().host();

                if (!cookies.containsKey(hostString)) {
                    //获取磁盘里面的spCookie字符串
                    String spCookie = mSharedPreferences.getString(hostString, "");
                    if (!TextUtils.isEmpty(spCookie)) {
                        //获取spCookie解密放到内存中
                        cookies.put(hostString, spCookie);
                    }
                }

                //获取内存中的Cookie
                String memoryCookie = cookies.get(hostString);
                //拦截网络请求数据
                Request request = originalRequest.newBuilder()
                        //设置请求头Cookie值
                        .addHeader("Cookie", memoryCookie == null ? "" : memoryCookie)
                        .build();

                //拦截返回数据
                Response originalResponse = chain.proceed(request);
                //判断请求头里面是否有Set-Cookie值,更新Cookie
                if (!originalResponse.headers("Set-Cookie").isEmpty()) {
                    //字符串集
                    StringBuilder stringBuilder = new StringBuilder();
                    for (String header : originalResponse.headers("Set-Cookie")) {
                        stringBuilder.append(header);
                        stringBuilder.append(";");
                    }
                    //拼接Cookie成字符串
                    String cookie = stringBuilder.toString();

                    //更新内存中Cookies值
                    cookies.put(hostString, cookie);
                    //存储到本地磁盘中
                    SharedPreferences.Editor editor = mSharedPreferences.edit();
                    //存储cookie(为了安全这里可以加密存储)
                    editor.putString(hostString, cookie);
                    editor.apply();
                    Log.e("Set-Cookie", "cookies: " + cookie + " host: " + hostString);
                }
                return originalResponse;
            }
        })
        .addInterceptor(new LoggerInterceptor("TAG"))
        .build();

OkHttpUtils.initClient(okHttpClient);

第二种实现方式(继承CookieJar实现)

这里可以参考OKGO里面实现的库,Cookie,实现
CookieJarImpl继承CookieJar和SPCookieStore。

public class SPCookieStore implements CookieStore {

    private static final String COOKIE_PREFS = "okhttp_cookie";           //cookie使用prefs保存
    private static final String COOKIE_NAME_PREFIX = "cookie_";         //cookie持久化的统一前缀

    private final Map<String, ConcurrentHashMap<String, Cookie>> cookies;
    private final SharedPreferences cookiePrefs;

    public SPCookieStore(Context context) {
        cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, Context.MODE_PRIVATE);
        cookies = new HashMap<>();

        //将持久化的cookies缓存到内存中,数据结构为 Map<Url.host, Map<CookieToken, Cookie>>
        Map<String, ?> prefsMap = cookiePrefs.getAll();
        for (Map.Entry<String, ?> entry : prefsMap.entrySet()) {
            if ((entry.getValue()) != null && !entry.getKey().startsWith(COOKIE_NAME_PREFIX)) {
                //获取url对应的所有cookie的key,用","分割
                String[] cookieNames = TextUtils.split((String) entry.getValue(), ",");
                for (String name : cookieNames) {
                    //根据对应cookie的Key,从xml中获取cookie的真实值
                    String encodedCookie = cookiePrefs.getString(COOKIE_NAME_PREFIX + name, null);
                    if (encodedCookie != null) {
                        Cookie decodedCookie = SerializableCookie.decodeCookie(encodedCookie);
                        if (decodedCookie != null) {
                            if (!cookies.containsKey(entry.getKey())) {
                                cookies.put(entry.getKey(), new ConcurrentHashMap<String, Cookie>());
                            }
                            cookies.get(entry.getKey()).put(name, decodedCookie);
                        }
                    }
                }
            }
        }
    }

    private String getCookieToken(Cookie cookie) {
        return cookie.name() + "@" + cookie.domain();
    }

    /** 当前cookie是否过期 */
    private static boolean isCookieExpired(Cookie cookie) {
        return cookie.expiresAt() < System.currentTimeMillis();
    }

    /** 将url的所有Cookie保存在本地 */
    @Override
    public synchronized void saveCookie(HttpUrl url, List<Cookie> urlCookies) {
        for (Cookie cookie : urlCookies) {
            saveCookie(url, cookie);
        }
    }

    @Override
    public synchronized void saveCookie(HttpUrl url, Cookie cookie) {
        if (!cookies.containsKey(url.host())) {
            cookies.put(url.host(), new ConcurrentHashMap<String, Cookie>());
        }
        //当前cookie是否过期
        if (isCookieExpired(cookie)) {
            removeCookie(url, cookie);
        } else {
            saveCookie(url, cookie, getCookieToken(cookie));
        }
    }

    /** 保存cookie,并将cookies持久化到本地 */
    private void saveCookie(HttpUrl url, Cookie cookie, String cookieToken) {
        //内存缓存
        cookies.get(url.host()).put(cookieToken, cookie);
        //文件缓存
        SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
        prefsWriter.putString(url.host(), TextUtils.join(",", cookies.get(url.host()).keySet()));
        prefsWriter.putString(COOKIE_NAME_PREFIX + cookieToken, SerializableCookie.encodeCookie(url.host(), cookie));
        prefsWriter.apply();
    }

    /** 根据当前url获取所有需要的cookie,只返回没有过期的cookie */
    @Override
    public synchronized List<Cookie> loadCookie(HttpUrl url) {
        List<Cookie> ret = new ArrayList<>();
        if (!cookies.containsKey(url.host())) return ret;

        Collection<Cookie> urlCookies = cookies.get(url.host()).values();
        for (Cookie cookie : urlCookies) {
            if (isCookieExpired(cookie)) {
                removeCookie(url, cookie);
            } else {
                ret.add(cookie);
            }
        }
        return ret;
    }

    /** 根据url移除当前的cookie */
    @Override
    public synchronized boolean removeCookie(HttpUrl url, Cookie cookie) {
        if (!cookies.containsKey(url.host())) return false;
        String cookieToken = getCookieToken(cookie);
        if (!cookies.get(url.host()).containsKey(cookieToken)) return false;

        //内存移除
        cookies.get(url.host()).remove(cookieToken);
        //文件移除
        SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
        if (cookiePrefs.contains(COOKIE_NAME_PREFIX + cookieToken)) {
            prefsWriter.remove(COOKIE_NAME_PREFIX + cookieToken);
        }
        prefsWriter.putString(url.host(), TextUtils.join(",", cookies.get(url.host()).keySet()));
        prefsWriter.apply();
        return true;
    }

    @Override
    public synchronized boolean removeCookie(HttpUrl url) {
        if (!cookies.containsKey(url.host())) return false;

        //内存移除
        ConcurrentHashMap<String, Cookie> urlCookie = cookies.remove(url.host());
        //文件移除
        Set<String> cookieTokens = urlCookie.keySet();
        SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
        for (String cookieToken : cookieTokens) {
            if (cookiePrefs.contains(COOKIE_NAME_PREFIX + cookieToken)) {
                prefsWriter.remove(COOKIE_NAME_PREFIX + cookieToken);
            }
        }
        prefsWriter.remove(url.host());
        prefsWriter.apply();

        return true;
    }

    @Override
    public synchronized boolean removeAllCookie() {
        //内存移除
        cookies.clear();
        //文件移除
        SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
        prefsWriter.clear();
        prefsWriter.apply();
        return true;
    }

    /** 获取所有的cookie */
    @Override
    public synchronized List<Cookie> getAllCookie() {
        List<Cookie> ret = new ArrayList<>();
        for (String key : cookies.keySet()) {
            ret.addAll(cookies.get(key).values());
        }
        return ret;
    }

    @Override
    public synchronized List<Cookie> getCookie(HttpUrl url) {
        List<Cookie> ret = new ArrayList<>();
        Map<String, Cookie> mapCookie = cookies.get(url.host());
        if (mapCookie != null) ret.addAll(mapCookie.values());
        return ret;
    }
}
 //当前cookie是否过期
if (isCookieExpired(cookie)) {
      removeCookie(url, cookie);
 } else {
     saveCookie(url, cookie, getCookieToken(cookie));
 }

 /** 当前cookie是否过期 */
private static boolean isCookieExpired(Cookie cookie) {
     return cookie.expiresAt() < System.currentTimeMillis();
}

【总结】这里保存持久化Cookie的关键看expiresAt与当前时间戳相比是否为过期,而不是看响应头里是否存在expires和max-age字段。

使用与之前类似:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .connectTimeout(10000L, TimeUnit.MILLISECONDS)
                .readTimeout(10000L, TimeUnit.MILLISECONDS)
                .cookieJar(new CookieJarImpl(new SPCookieStore()))
                .addInterceptor(new LoggerInterceptor("TAG"))
                .build();

OkHttpUtils.initClient(okHttpClient);

总结

后台对Cookie返回格式还是要规范一点,否则Cookie持久化保存会出现莫名其妙的错误。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容