踩踩鸿洋大神的坑,优化修复他Cookie支持带来的BUG

前言:现在APP内网络框架使用Retrofit应该是很普遍的现象了,但是有时候由于业务的需求,需要维持登录状态,或者服务端需要往客户端写一些数据,然后下一次请求就把这些数据往服务端写回去。完全模拟一个PC端的浏览器机制,所以我这里使用了鸿洋大神的Retrofit对Cookie的支持。

现象

正常使用,发现没什么问题,JSESSIONID也可以正常使用,会话可以保持。昨天后台跟我说,他往客户端写了其他的COOKIE,为什么安卓端没有回传回来??

排查

既然不能传回去,有两个地方有嫌疑

  • 第一个 Response往本地写没有写进去
  • 第二个 Request往服务器传没有传过去

我们看okhttp3.CookieJar.java的源码:

public interface CookieJar {
  /** A cookie jar that never accepts any cookies. */
  CookieJar NO_COOKIES = new CookieJar() {
    @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
    }

    @Override public List<Cookie> loadForRequest(HttpUrl url) {
      return Collections.emptyList();
    }
  };

  /**
   * Saves {@code cookies} from an HTTP response to this store according to this jar's policy.
   *
   * <p>Note that this method may be called a second time for a single HTTP response if the response
   * includes a trailer. For this obscure HTTP feature, {@code cookies} contains only the trailer's
   * cookies.
   */
  void saveFromResponse(HttpUrl url, List<Cookie> cookies);

  /**
   * Load cookies from the jar for an HTTP request to {@code url}. This method returns a possibly
   * empty list of cookies for the network request.
   *
   * <p>Simple implementations will return the accepted cookies that have not yet expired and that
   * {@linkplain Cookie#matches match} {@code url}.
   */
  List<Cookie> loadForRequest(HttpUrl url);
}

这里主要看两个方法:

  • void saveFromResponse(HttpUrl url, List<Cookie> cookies); 从Response往本地写
  • List<Cookie> loadForRequest(HttpUrl url); 从本地往Request请求里加进去

而对于本地化,则使用了一个PersistentCookieStore.java来实现

这里我们先看在 saveFromResponse(HttpUrl url, List<Cookie> cookies) 中,鸿洋大神写了这么一段:

override fun saveFromResponse(httpUrl: HttpUrl, cookies: List<Cookie>?)
{
    if ( cookies != null && cookies.isNotEmpty())
    {
        for (item in cookies)
        {
            cookieStore.add(httpUrl, item)
        }
    }
}

这里就是简单的判断,以及将服务端返回来的cookie列表,一个一个往本地添加进去,那么我们跟踪代码: cookieStore.add(httpUrl, item) 这一行点源码点进去:

PersistentCookieStore.java

public void add(HttpUrl url, Cookie cookie)
{
    String name = getCookieToken(cookie);
    //将cookies缓存到内存中 如果缓存过期 就重置此cookie
    if (!cookie.persistent())
    {
        if (!cookies.containsKey(url.host()))
        {
            cookies.put(url.host(), new ConcurrentHashMap<String, Cookie>());
        }
        cookies.get(url.host()).put(name, cookie);
    } else
    {
        if (cookies.containsKey(url.host()))
        {
            cookies.get(url.host()).remove(name);
        }
    }
    //讲cookies持久化到本地
    SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
    if (url.host() != null && cookies.get(url.host()) != null)
    {
        prefsWriter.putString(url.host(), TextUtils.join(",", cookies.get(url.host()).keySet()));
    }
    prefsWriter.putString(name, encodeCookie(new OkHttpCookies(cookie)));
    prefsWriter.apply();
}

这里的逻辑是,先获取到他的token,token是怎么组成的呢?就是 域名 @ Cookie的Key 比如:127.0.0.1@key1 , 127.0.0.1@key2 , 127.0.0.1@key3

然后判断Cookie是否过期:

  • 如果过期就New一个空的map添加进去。
  • 如果没有过期,则判断在Cookie里面是否存有相同的Key的Cookie,如果有,则remove掉同样的name的旧cookie数据。

其实这里就有问题:当没有过期的时候,应该先remove掉旧的数据,再把新的数据添加进去,说白了就是应该覆盖旧数据,再把新数据持久化到本地。

解决问题

OK,问题已经找到了,所以我们这里把他代码修改一下:

public void add(HttpUrl url, Cookie cookie)
{
    String name = getCookieToken(cookie);
    //将cookies缓存到内存中 如果缓存过期 就重置此cookie
    if (!cookie.persistent())
    {
        if (!cookies.containsKey(url.host()))
        {
            cookies.put(url.host(), new ConcurrentHashMap<String, Cookie>());
        }
        cookies.get(url.host()).put(name, cookie);
    } else
    {
        // 这里是修改后的代码
        Map<String, Cookie> cookieMap = cookies.get(url.host());
        if (cookieMap != null)
        {
            cookieMap.put(name, cookie);
        }
    }
    //讲cookies持久化到本地
    SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
    if (url.host() != null && cookies.get(url.host()) != null)
    {
        prefsWriter.putString(url.host(), TextUtils.join(",", cookies.get(url.host()).keySet()));
    }
    prefsWriter.putString(name, encodeCookie(new OkHttpCookies(cookie)));
    prefsWriter.apply();
}

这里我做了优化处理,先获取到对应host的cookieMap,如果不为空,则直接put进去。 为什么要这么做呢?我们这里先了解一下 cookies.containsKey(url.host()) 这行代码的意思是什么??意思是,在map中是否存在这个key,如果key存在,则返回true。但是如果刚好map中保存的是一个 null 呢??map中,key-value的形式,value是允许为null的。

所以我这里先获取到对应的value,判断不为空,再进行对应的处理,从逻辑上来讲,是比较安全的一个做法。然后就直接put进去,覆盖如果key一样,就覆盖旧数据了。再将cookies持久化的本地,这些就没什么可说的。

结尾

这里贴一下,我修改后的GitHub地址,以及Gradle直接使用地址: GitHub : https://github.com/xiaolei123/OkHttpHelper Gradle : implementation 'com.xiaolei:OkHttpUtil:1.0.6' 使用:

com.xiaolei.okhttputil.Cookie.CookieJar cookiejar = new CookieJar(context,null);
new OkHttpClient.Builder()
        .cookieJar(cookiejar)
        .build();

The End

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏奔跑的蛙牛技术博客

ElementRef & TemplateRef & ViewContainerRef

今天在做ng项目,看着前人的代码有 viewChild() 等关键字。新手有点疑惑,索性查查资料总结一下和ng相关的几个和DOM相关的几个概念

1052
来自专栏java闲聊

Netty(二) 创建简单的服务器

本篇文章是延续上一篇Netty文章,因此推荐先去看上一篇文章Netty(一),当然对Netty有一定认识略过。开始利用Netty创建一个简单的服务器

922
来自专栏面朝大海春暖花开

js代码小优化

好不容易把嵌套小窗口登陆注册功能,做完了,直接调之前写好的登陆注册功能,也就是页面跳转

992
来自专栏developerHaoz 的安卓之旅

Android Volley 源码解析(一),网络请求的执行流程

花了好几天,重新研究了 Volley 的源码实现,比起之前又有了一番新的体会,啃源码真的是一件让人纠结的事情,阅读优秀的源码,特别是难度相对较大的源码,一旦陷入...

1014
来自专栏从零开始学自动化测试

Selenium2+python自动化19-单选和复选框

最近发生了一些不愉快的事,其中缘由就不多说了,小编以后在这个公众号继续给大家更新,在过去的一年里感谢大家的一路支持,当然最感动的是能留下来的小伙伴,是你...

3418
来自专栏编程之旅

iOS开发——多线程完成短信获取按钮倒计时

现在的APP应用中,用手机获取短信验证码是非常常见的一个功能,而往往要求的效果就是在按下获取验证码之后,验证码的按钮开始倒计时,例如30秒后重新获取。而我们如何...

1154
来自专栏非著名程序员

基于 RxJava2+Retrofit2 精心打造的 Android 基础框架 XSnow

XSnow ? 基于RxJava2+Retrofit2精心打造的Android基础框架,包含网络、上传、下载、缓存、事件总线、权限管理、数据库、图片加载、UI模...

2347
来自专栏朱慕之的博客

UIWebView与JS的交互

要实现这样一个需求:按照本地的CSS文件展示一串网络获取的带HTML格式的只有body部分的文本,需要自己拼写完整的HTML。除此之外,还需要禁用获取的HTML...

721
来自专栏我杨某人的青春满是悔恨

POP 实现 Template Method

模板方法是一种古老的设计模式,它使用一些抽象的操作定义一套算法或者流程,父类决定步骤,子类决定具体实现,当然父类可以提供一个默认实现。

711
来自专栏大数据钻研

前端面试那些坑

HTML Doctype作用?严格模式与混杂模式如何区分?它们有何意义? HTML5 为什么只需要写 ? 行内元素有哪些?块级元素有哪些? 空(void)元素有...

3126

扫码关注云+社区