Android OkHttp Cookie持久化问题总结

作者 : 开心源码 本文共11600个字,预计阅读时间需要29分钟 发布时间: 2022-05-12 共231人阅读

说明

最近封装一个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.8Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9Connection: keep-aliveHost: www.baidu.comUpgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3472.3 Safari/537.36

响应头:

Bdpagetype: 1Bdqid: 0xe1a8fd3600011fd8Cache-Control: privateConnection: Keep-AliveContent-Encoding: gzipContent-Type: text/htmlCxy_all: baidu+c1a146ec227bccffbb8afe4da97bdf3eDate: Sat, 06 Apr 2019 09:48:35 GMTExpires: Sat, 06 Apr 2019 09:47:45 GMTP3p: CP=" OTI DSP COR IVA OUR IND COM "Server: BWS/1.1Set-Cookie: PSTM=1554544115; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.comSet-Cookie: BAIDUID=F7EBDE8F1230A7DDF1DD141A458BD04B:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.comSet-Cookie: BIDUPSID=F7EBDE8F1230A7DDF1DD141A458BD04B; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.comSet-Cookie: delPer=0; path=/; domain=.baidu.comSet-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.comStrict-Transport-Security: max-age=172800Transfer-Encoding: chunkedVary: Accept-EncodingX-Ua-Compatible: IE=Edge,chrome=1

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

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9Cache-Control: max-age=0Connection: keep-aliveCookie: 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=12314353Host: www.baidu.comUpgrade-Insecure-Requests: 1User-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上也有相似的问题 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.pngPersistentCookieStore代码实现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过滤条件persistentCookie判断

从源码上可以看出,当请求头中不存在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加密库, 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持久化保存会出现莫名其妙的错误。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Android OkHttp Cookie持久化问题总结

发表回复