最近封装一个SDK时,遇到一个需求就是登录成功之后,APP需要持久保存COOKIE,当APP退出再进入时需要从本地读取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.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Host: www.baidu.com
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3472.3 Safari/537.36
响应头:
Bdpagetype: 1
Bdqid: 0xe1a8fd3600011fd8
Cache-Control: private
Connection: Keep-Alive
Content-Encoding: gzip
Content-Type: text/html
Cxy_all: baidu+c1a146ec227bccffbb8afe4da97bdf3e
Date: Sat, 06 Apr 2019 09:48:35 GMT
Expires: Sat, 06 Apr 2019 09:47:45 GMT
P3p: CP=" OTI DSP COR IVA OUR IND COM "
Server: BWS/1.1
Set-COOKIE: PSTM=1554544115; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-COOKIE: BAIDUID=F7EBDE8F1230A7DDF1DD141A458BD04B:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-COOKIE: BIDUPSID=F7EBDE8F1230A7DDF1DD141A458BD04B; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-COOKIE: delPer=0; path=/; domain=.baidu.com
Set-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.com
Strict-Transport-Security: max-age=172800
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-Ua-Compatible: IE=Edge,chrome=1
请求头(第二次请求):
里面携带COOKIE信息
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: max-age=0
Connection: keep-alive
COOKIE: 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=12314353
Host: www.baidu.com
Upgrade-Insecure-Requests: 1
User-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上也有类似的问题https://github.com/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,
从源码上可以看出,当请求头中存在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);
从源码上可以看出,当请求头中不存在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);
}
});
查看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 ();
COOKIEs.add(COOKIE);
}
当请求头中存在domain时,这个时候主地址为ip与domian不等,COOKIE解析失败为null,导致保存COOKIE失败,这个浏览器也是存在问题的,这个得后台注意格式。
这里为了安全可以对COOKIE进行加密存储,可以使用这个SharedPreferences加密库,https://github.com/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);
这里可以参考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> COOKIEs;
private final SharedPreferences COOKIEPrefs;
public SPCOOKIEStore(Context context) {
COOKIEPrefs = context.getSharedPreferences(COOKIE_PREFS, Context.MODE_PRIVATE);
COOKIEs = new HashMap();
//将持久化的COOKIEs缓存到内存中,数据结构为 Map>
Map prefsMap = COOKIEPrefs.getAll();
for (Map.Entry 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());
}
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() 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());
}
//当前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 loadCOOKIE(HttpUrl url) {
List ret = new ArrayList();
if (!COOKIEs.containsKey(url.host())) return ret;
Collection 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 urlCOOKIE = COOKIEs.remove(url.host());
//文件移除
Set 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 getAllCOOKIE() {
List ret = new ArrayList();
for (String key : COOKIEs.keySet()) {
ret.addAll(COOKIEs.get(key).values());
}
return ret;
}
@Override
public synchronized List getCOOKIE(HttpUrl url) {
List ret = new ArrayList();
Map 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()
【总结】这里保存持久化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持久化保存会出现莫名其妙的错误。
转载请注明出处
作者:戎码虫
链接:https://www.jianshu.com/p/3c3da18e369e