熟悉的陌生人cookie,这些你了解吗

96
mandypig
0.6 2019.04.13 21:24* 字数 3024

拖了好长一段时间,今天利用周末在家更新一篇文章,写这篇文章的起因还是在看公司代码的时候存在一些疑惑,在加上之前也比较少接触cookie相关的东西,就利用这次机会去挖一下这方面的东西,其实文章本身就是通过自己demo测试去发现cookie存储的一些规律,为什么是通过demo测试代码而不是直接去看源码,主要的原因就在于关于webview cookie存储这块的源码最终涉及到jni调用,jni这块确实没找到对应源码,如果大家有兴趣可以试试跟踪下,入口点就是

cookieManager.setCookie(xxx)

至少自己是没跟到正确的源码,虽然没找到对应的源码,但是通过demo代码也一样可以得出cookie存储相关的规律。

疑惑代码

之所以会想着去摸索下cookie的存储规律,主要还是自己在公司代码中看到了有点不太明白的地方,也就是写这篇文章的最初目的,为了解惑,这样的代码是否真的没问题,来看下这段代码,其实公司这段代码也是直接从网上拷贝过来的进行适当修改得到的,如果处理过cookie应该对类似代码比较熟悉才对

 private void syncCookie(Context context, String url,String value) {
    CookieSyncManager.createInstance(context);
    CookieManager cookieManager = CookieManager.getInstance();
    cookieManager.setAcceptCookie(true);
    cookieManager.removeSessionCookie();
    cookieManager.setCookie(url, value);
    CookieSyncManager.getInstance().sync();// To get instant sync instead of waiting for the timer to trigger, the host can call this.
  }

比较有疑问的地方就在于removeSessionCookie这句代码,具体该代码什么意思可以直接在api说明上找到官方说明

Removes all session cookies, which are cookies without an expiration

写得好像挺明白的,移除掉没有过期时间的session cookies。
那么问题来了,什么是session cookies,关于cookie分类一般可以在网上找到类似说法cookie分类,这里就直接贴出文章中的分类

1,临时Cookie(会话Cookie)
2,永久Cookie

不设置过期时间,则表示这个cookie生命周期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。
这种生命期为浏览会话期的cookie被称为会话cookie。会话cookie一般不保存在硬盘上而是保存在内存里。

设置了过期时间,浏览器就会把cookie保存到硬盘上,
关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。

网上其他说法类似上述,参照上面定义也就是说removeSessionCookie是移除掉临时Cookie,按照这种理解,每次setcookie之前移除一下这些没用的临时cookie无可厚非,但结合android cookiemanage设置cookie会发现和上述文章说法存在一定的偏差。

问题点

实际上android中的cookiemanager会把通过setcookie设置的cookie保存到一个名为Cookies的数据库中,具体的文件路径就在
data/data/包名/app_webview/cookies中,完全可以通过数据库工具打开看看cookie的具体存储形式。具体格式如下


QQ截图20190413172500.png

被保存到cookie数据库中的cookie按照存储时间可以分为两类,一类就是有设置过过期时间的cookie,另一类就是没有过期时间的cookie,按照官方说明就是which are cookies without an expiration,每当webview发起一个url请求时,cookiemanager就会根据一定的匹配规则从cookie数据库中找到cookie一起发送出去。

实际上我们在setcookie时完全可以给保存的cookie设置一个过期时间,可以通过max-age字段或者expires进行设置,max-age设置的是相对时间而expires设置的是绝对时间,他们的对应关系就是

currentTime+max-age=expires

不过expires目前来说不推荐使用了,使用max-age即可,一般情况下我们通过调用

cookieManager.setCookie(url, "key=value");

来保存cookie,这种cookie是没有过期时间的,而保存有过期时间的cookie应该是这种形式

cookieManager.setCookie(url, "key=value;max-age=60");

max-age的单位是秒。这两种不同的保存形式对应的正是网上所说的临时cookie和永久cookie,但是这两种cookie实际上都是存储在cookie数据库中的,并不存在临时cookie只要关闭浏览器窗口,cookie就消失了这种情况,应该说临时cookie的定义在android端不同于浏览器吧。那会不会存在一种情况我搞错定义了,设置了max-age的才是临时cookie,而没有设置过的才是永久cookie,这个应该不太可能,理由还是上面那句话###Removes all session cookies, which are cookies without an expiration
这可是官方自己说的,没有过期时间的才是session cookie

永久cookie

这么来看的话永久cookie似乎不太永久,因为它有一个过期时间限制它的存在时期,如果设置一个cookie的max-age=60,超过这个时间之后这个cookie会不会从数据库消失,我写过demo测试,如果在cookie过期后没有再次去获取这个cookie,你会发现这个cookie还是会一直存在数据库当中,直到你想主动去获取这个cookie,那么cookiemanager才会自动把这个cookie给移除掉,所以对于设置过过期时间的cookie来说它会根据时间判断是否删除该cookie

临时cookie

对比永久cookie来说,临时cookie就显得没有那么临时了,它会一定存在数据库当中,这种行为看起来倒是更像永久cookie,要想删除掉这种cookie,只有我们主动调用api来操作,可以使用removeSessionCookie或者removeAllCookies,removeSessionCookie之前就已经说过,removeAllCookies也很好理解就是会把cookie数据库中的所有cookie全部删除掉,不管是临时的还是永久的,一个不留。

回到上面的问题

如果理解了我上面说的android当中的cookie分类之后再来看一下文章最开始的那段代码会不会就会觉得有点问题了。我们通过setcookie去保存一个值,但是我们却没有设置max-age,然后在setcookie之前我们还调用removeSessionCookie把之前保存的临时cookie全部给删掉了,这种做法就好像我们的手机有256G内存,但是每次要安装一个app之前我们就把手机上其他app全部先给卸载掉是一个道理的,其实造成上面代码的主要原因还是没有理解android中cookie的存储规则,解决方法一般有两种,一种就是注释掉removeSessionCookie,另一种就是setcookie时带上max-age。

多个值存储问题

可能一个url我们会保存多个key-value键值对,多个普通键值对保存时一定要分开写才行。比如

cookieManager.setCookie(url, "key=value;key2=value2");

这种写法会有问题,cookie数据库只会保存key=value,后面的key2将会被忽略掉,必须写成

cookieManager.setCookie(url, "key=value;");
cookieManager.setCookie(url, "key2=value2;");

注意我说的是普通键值对,实际上cookie还提供了一些特殊的字段,比如manx-age就是其中一个,还有domain,path等其他字段
对于这些字段是完全可以合在一起写的

cookieManager.setCookie(url, "key=value;max-age=60;path=\path");

我个人理解就是max-age,path这些字段就是用来修饰key=value这个键值对的,这也是为什么cookieManager.setCookie(url, "key=value;key2=value2");这种写法不行,因为cookie数据库并没有预先定义好key2来修饰key这个键值对。

cookie数据库特殊字段解析

这些字段就是用来匹配cookie的,cookie数据库中一般会存储很多的cookie,一个url到底匹配哪些cookie就是通过这些字段来确定的,默认情况下不进行这些字段设置则保存到cookie数据库时会有相应的值

1 domain字段

cookieManager.setCookie(“http://www.baidu.com”, "key=value;key2=value2");

对应的domain为www.baidu.com,但是domian字段不可以随意设置否则会出现无法存储到数据库的情况。比如上述url为http://www.baidu.com,如果强行设置domain为http://www.jianshu.com会发现对应的cookie无法存储进数据库,关于domain的设置有一些限制,必须是url对应的一级域名,二级域名...这种形式才可以,需要注意一点的是www.baidu.com的一级域名是baidu.com而不是com

2 path字段
比如http://www.baidu.com/path1/path2这样的url,默认不设置的时候对应的path为"\path1",注意并不是“\path1\path2”,可以修改path为任意路径,最终存储到cookie数据库中时会把不是以\开头的path自动补上\。当我们通过getcookie从数据库中获取cookie时会发现和setcookie不太一样的地方在于如getcookie("http://www.baidu.com/path1/path2"),按照setcookie里面的path设置规则此时getcookie时对应的path应该是\path1才对,然而实际情况却是对应的路径为\path1\path2,不必太纠结这个点了记住就行。还有一点需要注意的就是cookie数据库会把父路径以及父父路径对应的cookie也给拿出来,“\”,“\path1”中的cookie只要domain和getcookie的url中一致也会给全部拿出来。

3 max-age,expires字段
在文章前面已经说过

4 httponly,secure字段
没使用过这两个字段,secure : 表示cookie只能被发送到http服务器。httponly : 表示cookie不能被客户端脚本获取到。这两个字段不需要带上value值,直接使用即可比较特殊

cookiemanager的flush

用来进行同步操作的,cookiemanager存储cookie在android版本最开始的时候是通过CookieSyncManager来实现的,为了提高效率在loadurl获取到的cookie会首先存储在内存当中,如果需要保存到数据库就需要调用CookieSyncManager的sync方法来实现同步,在android21版本之后cookiemanager已经实现了自动同步功能,不在需要使用CookieSyncManager来实现同步功能。但是虽然能够自动同步,但是这个同步的时间点我们无法控制,如果需要及时同步到数据库该如何操作,答案就是可以调用flush方法实现立即同步功能,但是这个方法会阻塞住ui线程,所以没有及时同步需求的话建议不要调用。

何时删除cookie

cookie不能光存不删除吧,和cookie删除相关的操作上面已将说过了,可以调用removeAllCookies来实现,调用这个方法的时机可以考虑放到清除缓存的操作中,一般app都会有一个设置页面,里面会有一项清除缓存的功能,可以在那里对cookie进行删除操作我觉得是一个比较好的时机,实现也比较简单。另一个可以执行删除cookie的时机可以放到用户升级app完成后可以考虑将原有的cookie数据库进行删除

cookie工具类

综上所说,自己写了一个cookie工具类,主要是对setcookie进行了一定程度的封装,直接拷贝就可以使用了,使用方法也很简单看下api应该就会使用了,就不做过多介绍

/**
 * @author stupid pig mandy
 * 存储WebView Cookie工具类
 */
public class WebViewCookie {

    public static void clear(Context context) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            CookieSyncManager.createInstance(context);
            CookieManager.getInstance().removeAllCookie();
            CookieSyncManager.getInstance().sync();
        } else {
            CookieManager.getInstance().removeAllCookies(null);
            CookieManager.getInstance().flush();
        }
    }

    public static void setCookie(Context context, boolean immediately, String url, Map<String, String> values) {
        Set<Map.Entry<String, String>> entries = values.entrySet();
        List<String> list = new ArrayList<>();
        for (Map.Entry<String, String> entry : entries) {
            String key = entry.getKey();
            String value = entry.getValue();
            list.add(key + "=" + value + ";");
        }
        setCookie(context, immediately, url, list);
    }

    public static void setCookieMaxAge(Context context, boolean immediately, String url, String key, String value, int maxAge) {
        setCookie(context, immediately, url, key, value, maxAge, "", "", false, false);
    }

    public static void setCookieDomain(Context context, boolean immediately, String url, String key, String value, String domain) {
        setCookie(context, immediately, url, key, value, 0, domain, "", false, false);
    }

    public static void setCookiePath(Context context, boolean immediately, String url, String key, String value, String path) {
        setCookie(context, immediately, url, key, value, 0, "", path, false, false);
    }

    public static void setCookieSecure(Context context, boolean immediately, String url, String key, String value, boolean secure) {
        setCookie(context, immediately, url, key, value, 0, "", "", secure, false);
    }

    public static void setCookieHttpOnly(Context context, boolean immediately, String url, String key, String value, boolean HttpOnly) {
        setCookie(context, immediately, url, key, value, 0, "", "", false, HttpOnly);
    }

    public static void setCookie(Context context, boolean immediately, String url, String key, String value, int maxAge,
                                 @Nullable String domain, String path, boolean secure, boolean httponly) {
        List<String> list = new ArrayList<>();
        StringBuilder sb = new StringBuilder();
        sb.append(key).append("=").append(value).append(";");
        if (maxAge > 0) {
            sb.append("max-age=").append(maxAge).append(";");
        }
        if (!TextUtils.isEmpty(domain)) {
            sb.append("domain=").append(domain).append(";");
        }
        if (!TextUtils.isEmpty(path)) {
            sb.append("path=").append(path).append(";");
        }
        if (secure) {
            sb.append("secure;");
        }
        if (httponly) {
            sb.append("httponly;");
        }
        list.add(sb.toString());
        setCookie(context, immediately, url, list);
    }

    private static void setCookie(Context context, boolean immediately, String url, List<String> list) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            CookieSyncManager.createInstance(context);
            setCookie(immediately, url, list);
            CookieSyncManager.getInstance().sync();
        } else {
            setCookie(immediately, url, list);
        }
    }

    private static void setCookie(boolean immediately, String url, List<String> list) {
        final CookieManager cookieManager = CookieManager.getInstance();
//        cookieManager.setAcceptCookie(true);// 允许接受 Cookie,默认开启 无视
        for (String entry : list) {
            if (entry == null) {
                continue;
            }
            String[] split = entry.split(";");
            if (check(split)) {
                cookieManager.setCookie(url, entry);
            }
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (immediately) {
                AsyncTask.execute(new Runnable() {
                    @Override
                    public void run() {
                        cookieManager.flush();
                    }
                });
            }
        }
    }

    private static boolean check(String[] split) {
        int normalValue = 0;
        int length = split.length;
        for (String s : split) {
            String[] result = s.split("=");
            if (result.length == 2 && (result[0].equalsIgnoreCase("max-age")
                    || result[0].equalsIgnoreCase("expires")
                    || result[0].equalsIgnoreCase("domain"))) {
                if (length > 1) {
                    continue;
                } else {
                    return false;
                }
            }
            if (result.length == 2 && result[0].equalsIgnoreCase("path") && result[1].startsWith("/")) {
                if (length > 1) {
                    continue;
                } else {
                    return false;
                }
            }
            if (result.length == 1 && (result[0].equalsIgnoreCase("httponly")
                    || result[0].equalsIgnoreCase("secure"))) {
                if (length > 1) {
                    continue;
                } else {
                    return false;
                }
            }
            normalValue++;
            if (normalValue > 1) {
                return false;
            }
        }
        return true;
    }

    public static String getCookie(String url) {
        CookieManager instance = CookieManager.getInstance();
        return instance.getCookie(url);
    }
}

总结

上面所说的关于cookie中的问题,都是自己通过测试代码一点一点总结出来的,可能会在一些地方存在理解偏差,如果有什么错误的地方还望指出,其实setcookie本身来说并不难,主要就是cookie当中一些特殊字段的处理需要留心下,自己在开发的过程中倒是没遇到复杂的cookie设置问题,但是系统的总结下还是有必要的

文章不易,支持的老铁可以点个赞

随笔