OkHttp3 (四)——Cookie与拦截器
标签(空格分隔): OkHttp3
版本:1
作者:陈小默
声明:禁止商业,禁止转载
[toc]
Cookie
在介绍如何使用Cookie之前,我们应该对后台的数据处理有一定的认识。由于HTTP协议无状态的特性,后台是无法保存用户的信息的,在此情形下,Cookie就诞生了。
Cookie的作用是在客户端保存数据,然后在每一次对该站点进行访问的时候都会携带此Cookie中的数据,于是后台就可以通过客户端Cookie中的数据来识别用户。早期很多网站甚至将用户名和密码保存在Cookie中。
在Web应用开发中有一句真理:任何的客户端行为都是不可信赖的。Cookie作为客户端技术,也有着同样的困境。Cookie会被攻击、被篡改,黑客可以从Cookie中查看到用户的用户名和密码,甚至是信用卡的密码。在此情形下,Session的概念被提出。
Session是一种服务端技术。服务端将数据保存在Session中,仅仅将此Session的ID发送给客户端,客户端在请求该站点的时候,只需要将Cookie中的SESSIONID这个数据发送给服务端即可。这样一来就避免了用户信息泄露的尴尬。
接下来我们通过一个具体的例子介绍OkHttp中Cookie的基本使用。
fun login() {
val form = FormBody.Builder()
.add("username", "cxm")
.add("password", "123456")
.build()
val request = Request.Builder()
.url(POST_LOGIN)
.post(form)
.build()
val call = client.newCall(request)
val response = call.execute()
println(response.body().string())
response.close()
}
在上面的登录方法中,我们向服务器发送了用户名和密码,此时在后台的实现是,将用户名和密码保存在服务端的Session中,然后将Session的ID保存在客户端的Cookie中。
fun info() {
val request = Request.Builder()
.url(GET_INFO)
.build()
val call = client.newCall(request)
val response = call.execute()
println(response.body().string())
response.close()
}
在info方法中,后台所做的处理是查询Session中保存的数据,并且返回用户名和密码,如果没有就提示未登录。
OkHttp默认是不保存Cookie的,如果我们需要OkHttp管理Cookie的话,需要给OkHttpClient设置CookieJar对象。
val cookie = object : CookieJar {
private val map = HashMap<String, MutableList<Cookie>>()
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
map[url.host()] = cookies
}
override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
return map[url.host()] ?: ArrayList<Cookie>()
}
}
val client = OkHttpClient.Builder().cookieJar(cookie).build()
saveFromResponse
:方法会在服务端给客户端发送Cookie时调用。此时需要我们自己实现保存Cookie的方式。这里使用了最简单的Map来保存域名与Cookie的关系。
loadForRequest
:每当这个client访问到某一个域名时,就会通过此方法获取保存的Cookie,并且发送给服务器。
接下来我们运行程序
fun main(args: Array<String>) {
login()
info()
}
可以看到如下内容
{"success":true,"message":"login","data":"cxm 登录成功"}
{"success":false,"message":"info","data":"cxm 您好!您的密码是:123456"}
那么当我们没有登录而直接获取信息时
fun main(args: Array<String>) {
//login()
info()
}
就会看到如下的内容
{"success":false,"message":"info","data":"当前未登录,请登陆后再试"}
Android设备中的Cookie持久化
在上面的例子中,我们会发现,由于map并没有被持久化到文件中,每次程序结束时我们存储在map中的Cookie就会消失。如果我们需要在程序每次启动的时候能够使用上次的Cookie,就需要将它序列化到本地文件中。
在Android应用中,我们先创建一个用于网络访问的工具类:
/**
* OkHttp请求工具类
* @author cxm
*/
class HttpUtil(val client: OkHttpClient) {
/**
* 用于回调的Handler
*/
private val handler = Handler()
/**
* 执行请求
* @param request 请求参数
* @param onCompleted 请求完成时回调
*/
fun execute(request: Request,
onCompleted: (ByteArray) -> Unit) {
execute(request, Exception::printStackTrace, onCompleted)
}
/**
* 执行请求
* @param request 请求参数
* @param onError 发生错误时回调
* @param onCompleted 请求完成时回调
*/
fun execute(request: Request,
onError: (Exception) -> Unit,
onCompleted: (ByteArray) -> Unit) {
execute(request, { current: Long, max: Long, speed: Long -> }, onError, onCompleted)
}
/**
* 执行请求
* @param request 请求参数
* @param progress 每秒回调一次当前下载进度
* @param onError 发生错误时回调
* @param onCompleted 请求完成时回调
*/
fun execute(request: Request,
progress: (current: Long, max: Long, speed: Long) -> Unit,
onError: (Exception) -> Unit,
onCompleted: (result: ByteArray) -> Unit) {
client.newCall(request).enqueue(object : okhttp3.Callback {
override fun onFailure(call: okhttp3.Call, e: IOException) {
handler.post { onError(e) }
}
override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
var out: OutputStream? = null
var input: InputStream? = null
try {
out = ByteArrayOutputStream()
input = response.body().byteStream()
val bytes = ByteArray(8 * 1024)
var len = 0
var current = 0L
val max = response.body().contentLength()
var timer = System.currentTimeMillis()
var speed = 0L
do {
current += len
speed += len
val currentTimer = System.currentTimeMillis()
if (currentTimer - timer > 1000) {
handler.post { progress(current, max, speed) }
timer = currentTimer
speed = 0L
}
out.write(bytes, 0, len)
len = input.read(bytes)
} while (len > 0)
val result = out.toByteArray()
handler.post { onCompleted(result) }
} catch (e: IOException) {
onFailure(call, e)
} finally {
if (input != null)
try {
input.close()
} catch (e: IOException) {
}
if (out != null)
try {
out.close()
} catch (e: IOException) {
}
response.close()
}
}
})
}
}
接下来,我们修改MainActivity的代码,使用普通的CookieJar实现,如下:
class MainActivity : AppCompatActivity() {
val HOST: String = "http://192.168.1.112:8080/smart"
val LOGIN: String = "$HOST/okhttp/cookie/login"
val INFO: String = "$HOST/okhttp/cookie/info"
val cookieJar: CookieJar = object : CookieJar {
private val map = HashMap<String, MutableList<Cookie>>()
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
map[url.host()] = cookies
}
override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
return map[url.host()] ?: ArrayList<Cookie>()
}
}
val client: OkHttpClient = OkHttpClient.Builder()
.cookieJar(cookieJar)
.build()
val util: HttpUtil = HttpUtil(client)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById(R.id.login).setOnClickListener {
val form: FormBody = FormBody.Builder()
.add("username", "cxm")
.add("password", "123456")
.build()
val request: Request = Request.Builder()
.url(LOGIN)
.post(form)
.build()
util.execute(request, {
Log.e("login", String(it))
})
}
findViewById(R.id.info).setOnClickListener {
val request = Request.Builder()
.url(INFO)
.build()
util.execute(request, {
Log.e("login", String(it))
})
}
}
}
这时,当INFO按钮被点击的时候,控制台会输出如下内容:
E/login: {"success":false,"message":"info","data":"当前未登录,请登陆后再试"}
然后,点击LOGIN按钮
E/login: {"success":true,"message":"login","data":"cxm 登录成功"}
当LOGIN按钮被点击之后,无论点击INFO按钮多少次,都会输出下列内容
E/login: {"success":false,"message":"info","data":"cxm 您好!您的密码是:123456"}
只要此程序正常运行,此Cookie就一直可用。但是在Android中,如果一个应用没有在前台显示,那么它就可能被销毁。对小内存的设备就更是如此。
为了解决这个问题,我们就需要将数据持久化到本地。通常,在Android设备中我们使用SharedPreferences保存。为了将Cookie写入文件,我们就要序列化这个对象。但是,我们发现Cookie并没有实现序列化的接口,那么我们就必须实现一个作为中介的对象:
/**
* 序列化的Cookie对象
* @author cxm
*/
class SerializableCookie(cookie: Cookie) : Serializable {
private val name: String?
private val value: String?
private val expiresAt: Long?
private val domain: String?
private val path: String?
private val secure: Boolean?
private val httpOnly: Boolean?
private val hostOnly: Boolean?
init {
name = cookie.name()
value = cookie.value()
expiresAt = cookie.expiresAt()
domain = cookie.domain()
path = cookie.path()
secure = cookie.secure()
httpOnly = cookie.httpOnly()
hostOnly = cookie.hostOnly()
}
/**
* 从当前对象中参数生成一个Cookie
* @author cxm
*/
fun cookie(): Cookie {
return Cookie.Builder()
.name(name)
.value(value)
.expiresAt(expiresAt ?: 0L)
.path(path)
.let {
if (secure ?: false) it.secure()
if (httpOnly ?: false) it.httpOnly()
if (hostOnly ?: false)
it.hostOnlyDomain(domain)
else
it.domain(domain)
it
}
.build()
}
}
有了这个类之后,我么就可以序列化Cookie中的数据了。
/**
* 序列化Cookie的工具类
* @author cxm
*/
class PersistentCookieStore(val context: Context) {
private val COOKIE_PREFS = "cookie_prefs"
private val cache = HashMap<String, MutableList<Cookie>>()
/**
* 存储 Cookies
* 首先将 Cookies 存入当前缓存对象 cache 中,然后在将序列化后的数据存入 SharedPreferences 文件。
*
* @author cxm
*
* @param host 站点域名(或IP地址)
* @param cookies Cookie列表
*/
operator fun set(host: String, cookies: MutableList<Cookie>) {
cache[host] = cookies
val set = HashSet<String>()
cookies.map { encodeBase64(it) }
.forEach { set.add(it) }
val prefs = context.getSharedPreferences(COOKIE_PREFS, Context.MODE_PRIVATE)
val edit = prefs.edit()
edit.putStringSet(host, set)
edit.apply()
}
/**
* 获取 Cookies
* 首先,从缓存中查询是否有可用的 Cookies ,如果没有再从 SharedPreferences 文件中查找。
*
* @author
*
* @param host 站点域名(或IP地址)
* @return Cookies
*/
operator fun get(host: String): MutableList<Cookie>? {
val cookies = cache[host]
if (cookies != null && cookies.isNotEmpty()) {
return cookies
} else {
val prefs = context.getSharedPreferences(COOKIE_PREFS, Context.MODE_PRIVATE)
val set = prefs.getStringSet(host, null)
if (set == null) {
return null
} else {
val list = ArrayList<Cookie>()
set.map { decodeBase64(it) }
.forEach { list.add(it) }
cache[host] = list
return list
}
}
}
/**
* 移除某一个站点的 Cookies
* 将其从缓存和 SharedPreferences 文件中删除
*
* @param host 站点域名(或IP地址)
*/
fun remove(host: String) {
cache.remove(host)
val prefs = context.getSharedPreferences(COOKIE_PREFS, Context.MODE_PRIVATE)
prefs.edit().remove(host).apply()
}
/**
* 清空全部站点的 Cookies
* 清空缓存和 SharedPreferences 。
*
*/
fun clear() {
cache.clear()
val prefs = context.getSharedPreferences(COOKIE_PREFS, Context.MODE_PRIVATE)
prefs.edit().clear().apply()
}
/**
* 将一个 Cookie 对象序列化为字符串
*
* 1,将 Cookie 对象转换为可序列化的 SerializableCookie 对象
* 2,将 SerializableCookie 序列化为 ByteArray
* 3,将 ByteArray 使用 Base64 编码并生成字符串
*
* @author cxm
*
* @param cookie 需要序列化的 Cookie 对象
* @return 序列化之后的字符串
*/
private fun encodeBase64(cookie: Cookie): String {
var objectBuffer: ObjectOutputStream? = null
try {
val buffer = ByteArrayOutputStream()
objectBuffer = ObjectOutputStream(buffer)
objectBuffer.writeObject(SerializableCookie(cookie))
val bytes = buffer.toByteArray()
val code = Base64.encode(bytes, Base64.DEFAULT)
return String(code)
} catch (e: Exception) {
throw e
} finally {
if (objectBuffer != null)
try {
objectBuffer.close()
} catch (e: Exception) {
}
}
}
/**
* 将一个编码后的字符串反序列化为 Cookie 对象
*
* 1,将该字符串使用 Base64 解码为字节数组
* 2,将字节数据反序列化为 SerializableCookie 对象
* 3,从 SerializableCookie 对象中获取 Cookie 对象并返回。
*
* @author cxm
*
* @param code 被编码后的序列化数据
* @return 解码后的 Cookie 对象
*/
private fun decodeBase64(code: String): Cookie {
var objectBuffer: ObjectInputStream? = null
try {
val bytes = Base64.decode(code, Base64.DEFAULT)
val buffer = ByteArrayInputStream(bytes)
objectBuffer = ObjectInputStream(buffer)
return (objectBuffer.readObject() as SerializableCookie).cookie()
} catch (e: Exception) {
e.printStackTrace()
throw e
} finally {
if (objectBuffer != null)
try {
objectBuffer.close()
} catch (e: Exception) {
}
}
}
}
具体实现在代码中的注释已经说明,这里叙述一下流程
设置Cookies
将Cookies保存到缓存中,然后将Cookies依次使用encodeBase64
方法序列化为字符串。最后将这些字符串保存到HashSet<String>
并持久化到 SharePreferences
中。
获取Cookies
首先从缓存中查找Cookies,如果没有查找到,就从SharePreferences
将保存该站点Cookies的HashSet<String>
取出,然后依次反序列化为Cookie对象并保存在List<Cookie>
中返回。
然后,我们在使用时只需要修改CookieJar即可:
val cookieJar = object : CookieJar {
val store = PersistentCookieStore(this@MainActivity) //使用PersistentCookieStore替换之前的HashMap
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
store[url.host()] = cookies
}
override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
return store[url.host()] ?: ArrayList<Cookie>()
}
}
拦截器
如果我们使用OkHttp对某一个站点进行访问,每一次访问的时候我们可能都需要设置请求头,而对于同一个站点,这些请求头很可能都是一样的,比如
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8,en;q=0.6
Cache-Control:max-age=0
Connection:keep-alive
Upgrade-Insecure-Requests:1
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36
这些数据如果我们在每一次请求的时候都去主动携带的话,不仅影响美观,而且不利于修改。那么这是拦截器的就派上用场了。我们可以让拦截器在每次访问网络的时候重新设置请求头,并且将相应的数据添加上。
/**
* Observes, modifies, and potentially short-circuits requests going out and the corresponding
* responses coming back in. Typically interceptors add, remove, or transform headers on the request
* or response.
*/
public interface Interceptor {
Response intercept(Chain chain) throws IOException;
interface Chain {
Request request();
Response proceed(Request request) throws IOException;
Connection connection();
}
}
这是拦截器的接口的全部代码。
对于上述场景,实现如下:
val interceptor = Interceptor {
//获取原始Request对象,并在原始Request对象的基础上增加请求头信息
val request = it.request().newBuilder()
.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
.addHeader("Accept-Language", "zh-CN,zh;q=0.8,en;q=0.6")
.addHeader("Connection", "keep-alive")
.build()
//执行请求并返回响应对象
it.proceed(request)
}
val client = OkHttpClient.Builder()
.cookieJar(cookie)
.addInterceptor(interceptor)
.build()
取消重定向
访问某些站点时我们会发现最终看到的页面是初始连接经过多次跳转后达到的,在某些时候我们需要获取到每一次跳转的数据,那么我们就应该对访问过程进行拦截。
val client = OkHttpClient.Builder()
.followSslRedirects(false)
.followRedirects(false)
.build()
拦截方式就是在初始化client的时候设置不追随重定向。如果我们想要继续访问,就需要从响应头中获取Location
参数。