OkHttp3之Cookies管理及持久化 - Akioss
okHttp3正式版刚发布了没几天,正好重构之前的代码,于是第一时间入坑了。对okHttp3的一些改变,会陆续写下来,这是第一篇Cookies管理及持久化。
Cookies管理
OkHttp的源码过于复杂,感兴趣的同学可以自行阅读,这里只针对HttpEngineer
类进行分析,从字面意思即可看出这个类负责http请求的request、response等等操作的处理,而cookies管理也是随着http请求的request、response来处理。
3.0之前
先看networkRequest方法,在里面通过client.getCookieHandler()函数获得了CookieHandler对象,通过该对象拿到cookie并设置到请求头里,请求结束后取得响应后通过networkResponse.headers()函数将请求头获得传入receiveHeaders函数,并将取得的cookie存入getCookieHandler得到的一个CookieHandler对象中去
private Request networkRequest(Request request) throws IOException { Request.Builder result = request.newBuilder(); //例行省略.... CookieHandler cookieHandler = client.getCookieHandler(); if (cookieHandler != null) { // Capture the request headers added so far so that they can be offered to the CookieHandler. // This is mostly to stay close to the RI; it is unlikely any of the headers above would // affect cookie choice besides "Host". Map<String, List<String>> headers = OkHeaders.toMultimap(result.build().headers(), null); Map<String, List<String>> cookies = cookieHandler.get(request.uri(), headers); // Add any new cookies to the request. OkHeaders.addCookies(result, cookies); } //例行省略.... return result.build();}
public void readResponse() throws IOException { //例行省略.... receiveHeaders(networkResponse.headers()); //例行省略....}
public void receiveHeaders(Headers headers) throws IOException { CookieHandler cookieHandler = client.getCookieHandler(); if (cookieHandler != null) { cookieHandler.put(userRequest.uri(), OkHeaders.toMultimap(headers, null)); }}
CookieHandler对象是OkHttpClient类中的一个属性,传入了这个对象,那么OkHttp就会对cookie进行自动管理
private CookieHandler cookieHandler;public OkHttpClient setCookieHandler(CookieHandler cookieHandler) { this.cookieHandler = cookieHandler; return this;}public CookieHandler getCookieHandler() { return cookieHandler;}
OkHttpClient client = new OkHttpClient();client.setCookieHandler(CookieHandler cookieHanlder);
3.0之后
而在OkHttp3中,对cookie而言,新增了两个类Cookiejar
、Cookie
两个类,在了解这两个类之前,先去看一下HttpEngine
关于cookie管理的变化
private Request networkRequest(Request request) throws IOException { Request.Builder result = request.newBuilder(); //例行省略.... List<Cookie> cookies = client.cookieJar().loadForRequest(request.url()); if (!cookies.isEmpty()) { result.header("Cookie", cookieHeader(cookies)); } //例行省略.... return result.build(); }
private String cookieHeader(List<Cookie> cookies) { StringBuilder cookieHeader = new StringBuilder(); for (int i = 0, size = cookies.size(); i < size; i++) { if (i > 0) { cookieHeader.append("; "); } Cookie cookie = cookies.get(i); cookieHeader.append(cookie.name()).append('=').append(cookie.value()); } return cookieHeader.toString(); }
public void receiveHeaders(Headers headers) throws IOException { if (client.cookieJar() == CookieJar.NO_COOKIES) return; List<Cookie> cookies = Cookie.parseAll(userRequest.url(), headers); if (cookies.isEmpty()) return; client.cookieJar().saveFromResponse(userRequest.url(), cookies); }
通过以上几个关键方法,可以很明显的感觉到作者的意图了,为了更加自由定制化的cookie管理。其中loadForRequest()
、saveFromResponse()
这两个方法最为关键,分别是在发送时向request header中加入cookie,在接收时,读取response header中的cookie。现在再去看Cookiejar
这个类,就很好理解了
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);}
so!在OkHttpClient创建时,传入这个CookieJar的实现,就能完成对Cookie的自动管理了
OkHttpClient client = new OkHttpClient.Builder() .cookieJar(new CookieJar() { private final HashMap<HttpUrl, List<Cookie>> cookieStore = new HashMap<>(); @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {cookieStore.put(url, cookies); } @Override public List<Cookie> loadForRequest(HttpUrl url) {List<Cookie> cookies = cookieStore.get(url);return cookies != null ? cookies : new ArrayList<Cookie>(); } }) .build();
Cookies持久化
对Cookies持久化的方案,与之前版本并无很大区别,还是参考android-async-http这个库,主要参考其中两个类:
PersistentCookieStore
SerializableHttpCookie
与之前版本的区别是要将对java.net.HttpCookie
这个类的缓存处理换成对okhttp3.Cookie
的处理,其他方面几乎一样。
废话不多说了,直接上代码
SerializableOkHttpCookies
主要做两件事:
将Cookie对象输出为ObjectStream
将ObjectStream序列化成Cookie对象
public class SerializableOkHttpCookies implements Serializable { private transient final Cookie cookies; private transient Cookie clientCookies; public SerializableOkHttpCookies(Cookie cookies) { this.cookies = cookies; } public Cookie getCookies() { Cookie bestCookies = cookies; if (clientCookies != null) {bestCookies = clientCookies; } return bestCookies; } private void writeObject(ObjectOutputStream out) throws IOException { out.writeObject(cookies.name()); out.writeObject(cookies.value()); out.writeLong(cookies.expiresAt()); out.writeObject(cookies.domain()); out.writeObject(cookies.path()); out.writeBoolean(cookies.secure()); out.writeBoolean(cookies.httpOnly()); out.writeBoolean(cookies.hostOnly()); out.writeBoolean(cookies.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; clientCookies =builder.build(); }}
PersistentCookieStore
根据一定的规则去缓存或者获取Cookie:
public class PersistentCookieStore { private static final String LOG_TAG = "PersistentCookieStore"; private static final String COOKIE_PREFS = "Cookies_Prefs"; private final Map<String, ConcurrentHashMap<String, Cookie>> cookies; private final SharedPreferences cookiePrefs; public PersistentCookieStore(Context context) { cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, 0); cookies = new HashMap<>(); //将持久化的cookies缓存到内存中 即map cookies Map<String, ?> prefsMap = cookiePrefs.getAll(); for (Map.Entry<String, ?> entry : prefsMap.entrySet()) {String[] cookieNames = TextUtils.split((String) entry.getValue(), ",");for (String name : cookieNames) { String encodedCookie = cookiePrefs.getString(name, null); if (encodedCookie != null) { Cookie decodedCookie = 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); } }} } } protected String getCookieToken(Cookie cookie) { return cookie.name() + "@" + cookie.domain(); } 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(); prefsWriter.putString(url.host(), TextUtils.join(",", cookies.get(url.host()).keySet())); prefsWriter.putString(name, encodeCookie(new SerializableOkHttpCookies(cookie))); prefsWriter.apply(); } public List<Cookie> get(HttpUrl url) { ArrayList<Cookie> ret = new ArrayList<>(); if (cookies.containsKey(url.host()))ret.addAll(cookies.get(url.host()).values()); return ret; } public boolean removeAll() { SharedPreferences.Editor prefsWriter = cookiePrefs.edit(); prefsWriter.clear(); prefsWriter.apply(); cookies.clear(); return true; } public boolean remove(HttpUrl url, Cookie cookie) { String name = getCookieToken(cookie); if (cookies.containsKey(url.host()) && cookies.get(url.host()).containsKey(name)) {cookies.get(url.host()).remove(name);SharedPreferences.Editor prefsWriter = cookiePrefs.edit();if (cookiePrefs.contains(name)) { prefsWriter.remove(name);}prefsWriter.putString(url.host(), TextUtils.join(",", cookies.get(url.host()).keySet()));prefsWriter.apply();return true; } else {return false; } } public List<Cookie> getCookies() { ArrayList<Cookie> ret = new ArrayList<>(); for (String key : cookies.keySet())ret.addAll(cookies.get(key).values()); return ret; } /** * cookies 序列化成 string * * @param cookie 要序列化的cookie * @return 序列化之后的string */ protected String encodeCookie(SerializableOkHttpCookies 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()); } /** * 将字符串反序列化成cookies * * @param cookieString cookies string * @return cookie object */ protected Cookie decodeCookie(String cookieString) { byte[] bytes = hexStringToByteArray(cookieString); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); Cookie cookie = null; try {ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);cookie = ((SerializableOkHttpCookies) objectInputStream.readObject()).getCookies(); } catch (IOException e) {Log.d(LOG_TAG, "IOException in decodeCookie", e); } catch (ClassNotFoundException e) {Log.d(LOG_TAG, "ClassNotFoundException in decodeCookie", e); } return cookie; } /** * 二进制数组转十六进制字符串 * * @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); } /** * 十六进制字符串转二进制数组 * * @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; }}
最终效果
完成对Cookie持久化之后,就可以对Cookiejar进行进一步修改了,最终效果:
/** * 自动管理Cookies */ private class CookiesManager implements CookieJar { private final PersistentCookieStore cookieStore = new PersistentCookieStore(getApplicationContext()); @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {if (cookies != null && cookies.size() > 0) { for (Cookie item : cookies) { cookieStore.add(url, item); }} } @Override public List<Cookie> loadForRequest(HttpUrl url) {List<Cookie> cookies = cookieStore.get(url);return cookies; } }
Tips
在这样做之前,尝试了使用Interceptor
和NetWorkInterceptor
在Http请求request和response时,拦截响应链,加入对Cookie的管理。so!接下来可能会详细介绍下Interceptor
这个非常酷的实现。