Android OkHttp Cookie持久化問題總結(jié)

說明

最近封裝一個SDK時百框,遇到一個需求就是登錄成功之后,APP需要持久保存Cookie,當(dāng)APP退出再進入時需要從本地讀取Cookie值晃听,類似于瀏覽器,一個網(wǎng)站登錄成功之后砰识,關(guān)閉瀏覽器再打開能扒,還能繼續(xù)訪問這個網(wǎng)站網(wǎng)頁。

Cookie

圖片來源:https://www.cnblogs.com/zhuanzhuanfe/p/8010854.html

分析

首先我們清除谷歌瀏覽器里面緩存的Cookie辫狼,當(dāng)首次訪問百度https://www.baidu.com/初斑,請求體中還沒有攜帶Cookie,響應(yīng)體中會出現(xiàn)Set-Cookie字段予借,要求瀏覽器保存Cookie越平,當(dāng)?shù)诙握埱髸r會攜帶這個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

響應(yīng)頭:

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

現(xiàn)象

使用的是鴻洋的 okhttputils網(wǎng)絡(luò)框架灵迫,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";
// 配置基本網(wǎng)絡(luò)請求
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);
            }
        });

當(dāng)設(shè)置內(nèi)存保存Cookie時(MemoryCookieStore)瀑粥,第二次訪問攜帶上Cookie挣跋,但是退出APP之后就丟失了。

當(dāng)設(shè)置永久保存Cookie時(PersistentCookieStore)狞换,第二次訪問還是沒有攜帶上Cookie避咆,

image.png
PersistentCookieStore代碼實現(xiàn)
persistent值

從源碼上可以看出舟肉,當(dāng)請求頭中存在expires和max-age時,返回為True查库,這個時候PersistentCookieStore是不對Cookie進行磁盤路媚、內(nèi)存存儲的,這里只是設(shè)置一個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判斷

從源碼上可以看出,當(dāng)請求頭中不存在expires和max-age時剂府,返回為False拧揽,這個時候PersistentCookieJar是不對Cookie進行磁盤存儲的。

另外一種情況

okttp3訪問IP地址Cookie丟失的現(xiàn)象腺占,這里使用百度的IP地址:http://220.181.112.244:80/淤袜,

//這里使用百度IP地址
String baidu = "http://220.181.112.244:80/";
// 配置基本網(wǎng)絡(luò)請求
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實現(xiàn)湾笛,可以看到這一部分代碼:

...
  } 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);
    }

當(dāng)請求頭中存在domain時饮怯,這個時候主地址為ip與domian不等,Cookie解析失敗為null嚎研,導(dǎo)致保存Cookie失敗蓖墅,這個瀏覽器也是存在問題的,這個得后臺注意格式临扮。

瀏覽器情況

代碼實現(xiàn)

第一種實現(xiàn)方式(攔截器實現(xiàn))

這里為了安全可以對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)
        //網(wǎng)絡(luò)攔截器
        .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解密放到內(nèi)存中
                        cookies.put(hostString, spCookie);
                    }
                }

                //獲取內(nèi)存中的Cookie
                String memoryCookie = cookies.get(hostString);
                //攔截網(wǎng)絡(luò)請求數(shù)據(jù)
                Request request = originalRequest.newBuilder()
                        //設(shè)置請求頭Cookie值
                        .addHeader("Cookie", memoryCookie == null ? "" : memoryCookie)
                        .build();

                //攔截返回數(shù)據(jù)
                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();

                    //更新內(nèi)存中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);

第二種實現(xiàn)方式(繼承CookieJar實現(xiàn))

這里可以參考OKGO里面實現(xiàn)的庫杆勇,Cookie贪壳,實現(xiàn)
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持久化的統(tǒng)一前綴

    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緩存到內(nèi)存中,數(shù)據(jù)結(jié)構(gòu)為 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對應(yīng)的所有cookie的key,用","分割
                String[] cookieNames = TextUtils.split((String) entry.getValue(), ",");
                for (String name : cookieNames) {
                    //根據(jù)對應(yīng)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();
    }

    /** 當(dāng)前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>());
        }
        //當(dāng)前cookie是否過期
        if (isCookieExpired(cookie)) {
            removeCookie(url, cookie);
        } else {
            saveCookie(url, cookie, getCookieToken(cookie));
        }
    }

    /** 保存cookie蚜退,并將cookies持久化到本地 */
    private void saveCookie(HttpUrl url, Cookie cookie, String cookieToken) {
        //內(nèi)存緩存
        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();
    }

    /** 根據(jù)當(dāng)前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;
    }

    /** 根據(jù)url移除當(dāng)前的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;

        //內(nèi)存移除
        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;

        //內(nèi)存移除
        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() {
        //內(nèi)存移除
        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;
    }
}
 //當(dāng)前cookie是否過期
if (isCookieExpired(cookie)) {
      removeCookie(url, cookie);
 } else {
     saveCookie(url, cookie, getCookieToken(cookie));
 }

 /** 當(dāng)前cookie是否過期 */
private static boolean isCookieExpired(Cookie cookie) {
     return cookie.expiresAt() < System.currentTimeMillis();
}

【總結(jié)】這里保存持久化Cookie的關(guān)鍵看expiresAt與當(dāng)前時間戳相比是否為過期闰靴,而不是看響應(yīng)頭里是否存在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);

總結(jié)

后臺對Cookie返回格式還是要規(guī)范一點钻注,否則Cookie持久化保存會出現(xiàn)莫名其妙的錯誤蚂且。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市幅恋,隨后出現(xiàn)的幾起案子杏死,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件淑翼,死亡現(xiàn)場離奇詭異腐巢,居然都是意外死亡,警方通過查閱死者的電腦和手機玄括,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門冯丙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人遭京,你說我怎么就攤上這事银还。” “怎么了洁墙?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長戒财。 經(jīng)常有香客問我热监,道長,這世上最難降的妖魔是什么饮寞? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任孝扛,我火速辦了婚禮,結(jié)果婚禮上幽崩,老公的妹妹穿的比我還像新娘苦始。我一直安慰自己,他們只是感情好慌申,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布陌选。 她就那樣靜靜地躺著,像睡著了一般蹄溉。 火紅的嫁衣襯著肌膚如雪咨油。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天柒爵,我揣著相機與錄音役电,去河邊找鬼。 笑死棉胀,一個胖子當(dāng)著我的面吹牛法瑟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播唁奢,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼霎挟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了驮瞧?” 一聲冷哼從身側(cè)響起氓扛,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后采郎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體千所,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年蒜埋,在試婚紗的時候發(fā)現(xiàn)自己被綠了淫痰。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡整份,死狀恐怖待错,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情烈评,我是刑警寧澤火俄,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站讲冠,受9級特大地震影響瓜客,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜竿开,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一谱仪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧否彩,春花似錦疯攒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至贴浙,卻和暖如春筷转,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背悬而。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工呜舒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人笨奠。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓袭蝗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親般婆。 傳聞我的和親對象是個殘疾皇子到腥,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容