热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

OkHttp3(四)——Cookie与拦截器

OkHttp3(四)——Cookie与拦截器标签(空格分隔):OkHttp3版本:1作者:陈小默声明:禁止商业,禁止转载发布于:作业部落、简书、CSDNOkHttp3四Cooki

OkHttp3 (四)——COOKIE与拦截器

标签(空格分隔): OkHttp3

版本:1
作者:陈小默
声明:禁止商业,禁止转载

发布于:作业部落、简书、CSDN


  • OkHttp3 四COOKIE与拦截器
  • COOKIE
    • Android设备中的COOKIE持久化
  • 拦截器
    • 取消重定向


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>()
override fun saveFromResponse(url: HttpUrl, COOKIEs: MutableList) {
map[url.host()] = COOKIEs
}

override fun loadForRequest(url: HttpUrl): MutableList {
return map[url.host()] ?: ArrayList()
}
}
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>()
override fun saveFromResponse(url: HttpUrl, COOKIEs: MutableList) {
map[url.host()] = COOKIEs
}

override fun loadForRequest(url: HttpUrl): MutableList {
return map[url.host()] ?: ArrayList()
}
}

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>()

/**
* 存储 COOKIEs
* 首先将 COOKIEs 存入当前缓存对象 cache 中,然后在将序列化后的数据存入 SharedPreferences 文件。
*
* @author cxm
*
* @param host 站点域名(或IP地址)
* @param COOKIEs COOKIE列表
*/

operator fun set(host: String, COOKIEs: MutableList) {
cache[host] = COOKIEs

val set = HashSet()
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? {
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()
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并持久化到 SharePreferences 中。

获取COOKIEs
首先从缓存中查找COOKIEs,如果没有查找到,就从SharePreferences将保存该站点COOKIEs的HashSet取出,然后依次反序列化为COOKIE对象并保存在List中返回。

然后,我们在使用时只需要修改COOKIEJar即可:

        val COOKIEJar = object : COOKIEJar {
val store = PersistentCOOKIEStore(this@MainActivity) //使用PersistentCOOKIEStore替换之前的HashMap
override fun saveFromResponse(url: HttpUrl, COOKIEs: MutableList) {
store[url.host()] = COOKIEs
}

override fun loadForRequest(url: HttpUrl): MutableList {
return store[url.host()] ?: ArrayList()
}
}

拦截器

如果我们使用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参数。


推荐阅读
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
  • GreenDAO快速入门
    前言之前在自己做项目的时候,用到了GreenDAO数据库,其实对于数据库辅助工具库从OrmLite,到litePal再到GreenDAO,总是在不停的切换,但是没有真正去了解他们的 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 31.项目部署
    目录1一些概念1.1项目部署1.2WSGI1.3uWSGI1.4Nginx2安装环境与迁移项目2.1项目内容2.2项目配置2.2.1DEBUG2.2.2STAT ... [详细]
  • PDO MySQL
    PDOMySQL如果文章有成千上万篇,该怎样保存?数据保存有多种方式,比如单机文件、单机数据库(SQLite)、网络数据库(MySQL、MariaDB)等等。根据项目来选择,做We ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 本文介绍了一个适用于PHP应用快速接入TRX和TRC20数字资产的开发包,该开发包支持使用自有Tron区块链节点的应用场景,也支持基于Tron官方公共API服务的轻量级部署场景。提供的功能包括生成地址、验证地址、查询余额、交易转账、查询最新区块和查询交易信息等。详细信息可参考tron-php的Github地址:https://github.com/Fenguoz/tron-php。 ... [详细]
  • Android实战——jsoup实现网络爬虫,糗事百科项目的起步
    本文介绍了Android实战中使用jsoup实现网络爬虫的方法,以糗事百科项目为例。对于初学者来说,数据源的缺乏是做项目的最大烦恼之一。本文讲述了如何使用网络爬虫获取数据,并以糗事百科作为练手项目。同时,提到了使用jsoup需要结合前端基础知识,以及如果学过JS的话可以更轻松地使用该框架。 ... [详细]
  • Hibernate延迟加载深入分析-集合属性的延迟加载策略
    本文深入分析了Hibernate延迟加载的机制,特别是集合属性的延迟加载策略。通过延迟加载,可以降低系统的内存开销,提高Hibernate的运行性能。对于集合属性,推荐使用延迟加载策略,即在系统需要使用集合属性时才从数据库装载关联的数据,避免一次加载所有集合属性导致性能下降。 ... [详细]
  • LVS实现负载均衡的原理LVS负载均衡负载均衡集群是LoadBalance集群。是一种将网络上的访问流量分布于各个节点,以降低服务器压力,更好的向客户端 ... [详细]
author-avatar
手机用户2602890095
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有