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.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.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加密库, 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持久化问题总结