前言

之前优化过 okhttp 的cookie管理,可以落地,下一次打开app的时候,也能够直接使用上一次的 cookie 进行访问,刚好现在要上插件化,那么在插件中,我就使用 okhttp3 来进行网络访问了,但是他的cookie 管理已经从原来的 cookieManager 转到了 cookieJar,然而网上的文章,很多都是互相抄袭,统一都是使用 url host 作为 cookie 的缓存 key,这样使用 cookie 无法对应域名使用。

问题分析

我为啥说网上的无法跨代码都是无法跨域名使用呢?首选我们先看看 cookie 所带的参数:

  1. key
  2. value
  3. domain
  4. expiresTime

以上4个,我认为是浏览器管理域名比较关键的属性。那么为啥我说现有大部分 okhttp 的cookie管理代码无法跨域名使用呢?

我们来摘抄网上的一段代码。

    @Override
    public void add(URI uri, HttpCookie cookie) {
        String name = getCookieToken(uri, cookie);

        // Save cookie into local store, or remove if expired
        if (!cookie.hasExpired()) {
            if(!cookies.containsKey(uri.getHost()))
                cookies.put(uri.getHost(), new ConcurrentHashMap<String, HttpCookie>());
            cookies.get(uri.getHost()).put(name, cookie);
        } else {
            if(cookies.containsKey(uri.toString()))
                cookies.get(uri.getHost()).remove(name);
        }

        // Save cookie into persistent store
        SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
        prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));
        prefsWriter.putString(COOKIE_NAME_PREFIX + name, encodeCookie(new SerializableHttpCookie(cookie)));
        prefsWriter.commit();
    }

    @Override
    public List<HttpCookie> get(URI uri) {
        ArrayList<HttpCookie> ret = new ArrayList<HttpCookie>();
        if(cookies.containsKey(uri.getHost()))
            ret.addAll(cookies.get(uri.getHost()).values());
        return ret;
    }

以上,是 okhttp 在 cookie 管理缓存的时候使用的代码,其中比较关键的几句,如下

cookies.get(uri.getHost()).put(name, cookie);

if(cookies.containsKey(uri.getHost()))  
            ret.addAll(cookies.get(uri.getHost()).values());

恩,你没看错,这里是以请求URL的host来缓存cookie的。

那么,当你访问 a.abc.com 得到的一个cookie domain 为 abc.com时,也只能为a.abc.com这个域名使用。这是你会想了,如果我的登录接口是 account.abc.com,而我的邮件接口域名是 mail.abc.com,而且你还会有其他域名接口要共用 cookie 怎么办。你说的没错,所以我们应该更优的类似浏览器的去管理 cookie ,才能达到子域名跨域名共享 cookie。

解决方案

上面的问题其实很容易解决,Cookie 缓存下来,应该用在他对应的域名上面,那么我们应该以 Cookie 的 domain 来做缓存 Key 才合适。

关键代码修改为如下:

    @Override
    public void add(URI uri, HttpCookie cookie) {
        if (!cookies.containsKey(cookie.getDomain()))
            cookies.put(cookie.getDomain(), new ConcurrentHashMap<String, HttpCookie>());
        // Save cookie into local store, or remove if expired
        if (!cookie.hasExpired()) {
            cookies.get(cookie.getDomain()).put(cookie.getName(), cookie);
        } else {
            if (cookies.containsKey(cookie.getDomain()))
                cookies.get(cookie.getDomain()).remove(cookie.getDomain());
        }
    }


    @Override
    public List<HttpCookie> get(URI uri) {
        ArrayList<HttpCookie> ret = new ArrayList<HttpCookie>();
        for (String key : cookies.keySet()) {
            if (uri.getHost().contains(key)) {
                ret.addAll(cookies.get(key).values());
            }
        }
        return ret;
    }

这样,当用户使用 okhttp 通过 account.abc.com 登录的时候,可能获取到了 domain 为 abc.com 或者 account.abc.com 的cookie,当再去访问 mail.abc.com 域名的时候,就会将 abc.com 的 cookie 发送到服务器鉴权,我想,这就是 cookie 要留有 domain 字段设定的,跨子域名 cookie 共享。不然,你登录之后,访问 mail.abc.com 依然没有授权好的 cookie 信息到服务器中鉴权。

以上只是样例解析,基于 okhttp 的代码做的分析,okhttp 的对应代码可在okhttp框架的cookie自动管理优化中查看。

OKHTTP3 代码

恩,上面对 cookie 管理的代码基于 okhttp,本篇说 okhttp3,当然少不了 okhttp3 的代码了,okhttp3 对比 okhttp 的 cookie 管理,将 cookieManager 转变为了自己的 CookieJar,下面是代码:

CookieManager:

import android.content.Context;  
import android.text.TextUtils;

import java.util.List;

import okhttp3.Cookie;  
import okhttp3.CookieJar;  
import okhttp3.HttpUrl;

/**
 * Created by jiechic on 6/21/16.
 */
public class CookieManager implements CookieJar {

    private static CookieManager cookieManager;

    private final PersistentCookieStore persistentCookieStore;

    public static CookieManager getInstance(Context context) {
        if (cookieManager == null) {
            synchronized (CookieManager.class) {
                if (cookieManager == null) {
                    cookieManager = new CookieManager(context.getApplicationContext());
                }
            }
        }
        return cookieManager;
    }

    private CookieManager(Context context) {
        persistentCookieStore = new PersistentCookieStore(context);
    }

    @Override
    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
        persistentCookieStore.add(url, cookies);
    }

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

PersistentCookieStore:

import android.content.Context;  
import android.content.SharedPreferences;  
import android.text.TextUtils;  
import android.util.Log;

import java.io.ByteArrayInputStream;  
import java.io.ByteArrayOutputStream;  
import java.io.IOException;  
import java.io.ObjectInputStream;  
import java.io.ObjectOutputStream;  
import java.util.ArrayList;  
import java.util.HashMap;  
import java.util.List;  
import java.util.Locale;  
import java.util.Map;  
import java.util.concurrent.ConcurrentHashMap;

import okhttp3.Cookie;  
import okhttp3.HttpUrl;

/**
 * Created by jiechic on 6/21/16.
 * A persistent cookie store which implements the Apache HttpClient CookieStore interface. * Cookies are stored and will persist on the user's device between application sessions since they * are serialized and stored in SharedPreferences. Instances of this class are * designed to be used with AsyncHttpClient#setCookieStore, but can also be used with a * regular old apache HttpClient/HttpContext if you prefer.
 */
public class PersistentCookieStore {  
    private static final String LOG_TAG = "PersistentCookieStore";
    private static final String COOKIE_PREFS = "CookiePrefsFile";
    private static final String COOKIE_NAME_PREFIX = "cookie_";
    private final HashMap<String, ConcurrentHashMap<String, Cookie>> mCookies;
    private final SharedPreferences mCookiePrefs;

    /**
     * Construct a persistent cookie store. * * @param context Context to attach cookie store to
     */
    public PersistentCookieStore(Context context) {
        mCookiePrefs = context.getSharedPreferences(COOKIE_PREFS, 0);
        mCookies = new HashMap<>();//Load any previously stored cookies into the store

        Map<String, ?> prefsMap = mCookiePrefs.getAll();
        for (Map.Entry<String, ?> entry : prefsMap.entrySet()) {
            if (null != entry.getValue() && !((String) entry.getValue()).startsWith(COOKIE_NAME_PREFIX)) {
                String[] cookieNames = TextUtils.split((String) entry.getValue(), ",");
                for (String name : cookieNames) {
                    String encodedCookie = mCookiePrefs.getString(COOKIE_NAME_PREFIX + name, null);
                    if (encodedCookie != null) {
                        Cookie decodedCookie = decodeCookie(encodedCookie);
                        if (decodedCookie != null) {
                            if (!mCookies.containsKey(entry.getKey()))
                                mCookies.put(entry.getKey(), new ConcurrentHashMap<String, Cookie>());
                            mCookies.get(entry.getKey()).put(name, decodedCookie);
                        }
                    }
                }
            }
        }
    }

    protected String getCookieToken(Cookie cookie) {
        return cookie.name() + cookie.domain();
    }

    public void add(HttpUrl httpUrl, List<Cookie> cookies) {
        if (null != cookies && cookies.size() > 0) {
            for (Cookie item : cookies) {
                add(item);
            }
        }
    }

    private void add(Cookie cookie) {
//        String name = getCookieToken(cookie);

        //Save cookie into local store, or remove if expired
        if (!mCookies.containsKey(cookie.domain()))
            mCookies.put(cookie.domain(), new ConcurrentHashMap<String, Cookie>());

        if (cookie.expiresAt() > System.currentTimeMillis()) {
            mCookies.get(cookie.domain()).put(cookie.name(), cookie);
        } else {
            if (mCookies.containsKey(cookie.domain()))
                mCookies.get(cookie.domain()).remove(cookie.domain());
        }
        //Save cookie into persistent store
        SharedPreferences.Editor prefsWriter = mCookiePrefs.edit();
        prefsWriter.putString(cookie.domain(), TextUtils.join(",", mCookies.get(cookie.domain()).keySet()));
        prefsWriter.putString(COOKIE_NAME_PREFIX + cookie.name(), encodeCookie(new SerializableHttpCookie(cookie)));
        prefsWriter.apply();
    }


    public List<Cookie> get(HttpUrl uri) {
        ArrayList<Cookie> ret = new ArrayList<>();
        for (String key : mCookies.keySet()) {
            if (uri.host().contains(key)) {
                ret.addAll(mCookies.get(key).values());
            }
        }
        return ret;
    }

    /**
     * Serializes Cookie object into String * * @param cookie cookie to be encoded, can be null * @return cookie encoded as String
     */
    protected String encodeCookie(SerializableHttpCookie cookie) {
        if (cookie == null) return null;
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(os);
            outputStream.writeObject(cookie);
        } catch (IOException e) {
            Log.d(LOG_TAG, "IOException in encodeCookie", e);
            return null;
        }
        return byteArrayToHexString(os.toByteArray());
    }

    /**
     * Returns cookie decoded from cookie string * * @param cookieString string of cookie as returned from http request * @return decoded cookie or null if exception occured
     */
    protected Cookie decodeCookie(String cookieString) {
        byte[] bytes = hexStringToByteArray(cookieString);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        Cookie cookie = null;
        try {
            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
            cookie = ((SerializableHttpCookie) objectInputStream.readObject()).getCookie();
        } catch (IOException e) {
            Log.d(LOG_TAG, "IOException in decodeCookie", e);
        } catch (ClassNotFoundException e) {
            Log.d(LOG_TAG, "ClassNotFoundException in decodeCookie", e);
        }
        return cookie;
    }

    /**
     * Using some super basic byte array <-> hex conversions so we don't have to rely on any * large Base64 libraries. Can be overridden if you like! * * @param bytes byte array to be converted * @return string containing hex values
     */
    protected String byteArrayToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte element : bytes) {
            int v = element & 0xff;
            if (v < 16) {
                sb.append('0');
            }
            sb.append(Integer.toHexString(v));
        }
        return sb.toString().toUpperCase(Locale.US);
    }

    /**
     * Converts hex values from strings to byte arra * * @param hexString string of hex-encoded values * @return decoded byte array
     */
    protected byte[] hexStringToByteArray(String hexString) {
        int len = hexString.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i + 1), 16));
        }
        return data;
    }
}

SerializableHttpCookie:

SerializableHttpCookie implements Serializable {  
    private static final long serialVersionUID = 6374381323722046732L;
    private transient final Cookie cookie;
    private transient Cookie clientCookie;

    public SerializableHttpCookie(Cookie cookie) {
        this.cookie = cookie;
    }

    public Cookie getCookie() {
        Cookie bestCookie = cookie;
        if (clientCookie != null) {
            bestCookie = clientCookie;
        }
        return bestCookie;
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeObject(cookie.name());
        out.writeObject(cookie.value());
        out.writeLong(cookie.expiresAt());
        out.writeObject(cookie.domain());
        out.writeObject(cookie.path());
        out.writeBoolean(cookie.secure());
        out.writeBoolean(cookie.httpOnly());
        out.writeBoolean(cookie.hostOnly());
        out.writeBoolean(cookie.persistent());
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        String name = (String) in.readObject();
        String value = (String) in.readObject();
        long expiresAt = in.readLong();
        String domain = (String) in.readObject();
        String path = (String) in.readObject();
        boolean secure = in.readBoolean();
        boolean httpOnly = in.readBoolean();
        boolean hostOnly = in.readBoolean();
        boolean persistent = in.readBoolean();
        Cookie.Builder builder = new Cookie.Builder();
        builder = builder.name(name);
        builder = builder.value(value);
        builder = builder.expiresAt(expiresAt);
        builder = hostOnly ? builder.hostOnlyDomain(domain) : builder.domain(domain);
        builder = builder.path(path);
        builder = secure ? builder.secure() : builder;
        builder = httpOnly ? builder.httpOnly() : builder;
        clientCookie = builder.build();
    }
}

使用方法:

OkHttpClient client = new OkHttpClient.Builder()  
                .cookieJar(CookieManager.getInstance(context))
                .build();