Android OkHttp Cookie持久化问题总结

作者: 戎码虫 | 来源:发表于2019-04-08 00:22 被阅读16次

    说明

    最近封装一个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持久化保存会出现莫名其妙的错误。

    相关文章

      网友评论

        本文标题:Android OkHttp Cookie持久化问题总结

        本文链接:https://www.haomeiwen.com/subject/ppbkiqtx.html