✨作者简介:00后,22年刚刚毕业,一枚在鹅厂搬砖的程序员。
✨前置任务:在阅读本篇文章之前希望读者已经阅读了上篇文章OkHttp原理第三篇—RetryAndFollowUpInterceptor,本篇文章详细对
BridgeInterceptor
进行解析,也希望读者在阅读之前已经对其进行了简单研究。
✨学习目标:弄懂
BridgeInterceptor
如何处理请求头和数据压缩。
✨创作初衷:学习
OkHttp
的原理,阅读Kotlin
框架源码,提高自己对Kotlin
代码的阅读能力。为了读代码而读代码,笔者知道这是不对的,但作为应届生,提高阅读源码的能力笔者认为还是很重要的。
OkHttp原理第四篇-BridgeInterceptor
主要处理处理请求头和压缩
BridgeInterceptor#intercept
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val userRequest = chain.request()
val requestBuilder = userRequest.newBuilder()
val body = userRequest.body
if (body != null) {
val contentType = body.contentType()
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString())
}
val contentLength = body.contentLength()
// 上篇文章的HTTP协议总结中对Content-Length和Transfer-Encoding的作用进行了说明,这里也印证了两者不可同时出现
if (contentLength != -1L) {
requestBuilder.header("Content-Length", contentLength.toString())
requestBuilder.removeHeader("Transfer-Encoding")
} else {
requestBuilder.header("Transfer-Encoding", "chunked")
requestBuilder.removeHeader("Content-Length")
}
}
// host,请求的服务器地址或者域名
if (userRequest.header("Host") == null) {
requestBuilder.header("Host", userRequest.url.toHostHeader())
}
// 连接方式,默认为Keep-Alive,客户端希望保持连接,若服务端返回close则也不能维持长连接,维持长连接是两端头都需要同意的事
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive")
}
// Accept-Encoding用于提供客户端支持的压缩方式,与响应头的Content-Encoding对应
// Range表示客户端想要此次请求资源的某一部分如:Range: bytes=0-50, 100-150,表示只要0-50和100-150字节两个部分
// 若没有指定上述两个请求头属性,则压缩方式指定为gzip
var transparentGzip = false
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true
requestBuilder.header("Accept-Encoding", "gzip")
}
// 加载Cookie,loadForRequest()是抽象方法需要自己实现,关于Cookie的知识读者可以看Cookie小节的分析
val cookies = cookieJar.loadForRequest(userRequest.url)
// 若Cookie不为空,则给请求头Cookie字段赋值
if (cookies.isNotEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies))
}
// User-Agent表示此次请求的发起的平台和工具,如下:
// Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.29 Safari/537.36
// 由OkHttp发起则发起工具为OkHttp
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", userAgent)
}
val networkResponse = chain.proceed(requestBuilder.build())
// 保存Cookie,看下1.CookieJar#receiveHeaders分析
cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)
val responseBuilder = networkResponse.newBuilder()
.request(userRequest)
// 若上述使用gzip,则需要进行解压
// Content-Encoding代表此次响应体的编码
if (transparentGzip &&
"gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
networkResponse.promisesBody()) {
val responseBody = networkResponse.body
if (responseBody != null) {
val gzipSource = GzipSource(responseBody.source()) //装饰者模式OKIO,将输入流包装为Gzip输入流
val strippedHeaders = networkResponse.headers.newBuilder()
.removeAll("Content-Encoding") //解压后,请求体的编码发生变化,清楚此属性
.removeAll("Content-Length") //挤压后,数据大小也发生变化,清楚此属性
.build()
responseBuilder.headers(strippedHeaders) //重置响应头
val contentType = networkResponse.header("Content-Type")
responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer())) //Body的三个参数分别是内容类型(String),长度(Long),数据源(BufferedSource)
}
}
return responseBuilder.build()
}
1.CookieJar#receiveHeaders
fun CookieJar.receiveHeaders(url: HttpUrl, headers: Headers) {
// 未设置CookieJar则结束此次调用
if (this === CookieJar.NO_COOKIES) return
// 解析Cookie,拿到cookie数组,下面2.Cookie#parseAll分析
val cookies = Cookie.parseAll(url, headers)
if (cookies.isEmpty()) return
// 回调创建Client时绑定的CookieJar的saveFromResponse()方法,用于保存Cookie
saveFromResponse(url, cookies)
}
2.Cookie#parseAll
fun parseAll(url: HttpUrl, headers: Headers): List<Cookie> {
// 去除响应头中的Set-Cookie字段
val cookieStrings = headers.values("Set-Cookie")
var cookies: MutableList<Cookie>? = null
// 逐个进行解析
for (i in 0 until cookieStrings.size) {
val cookie = parse(url, cookieStrings[i]) ?: continue
if (cookies == null) cookies = mutableListOf()
cookies.add(cookie)
}
return if (cookies != null) {
Collections.unmodifiableList(cookies)
} else {
emptyList()
}
}
Cookie
HTTP
是无状态协议,何为无状态呢?用户登录了一个网站,当用户再访问此域名下的另一个资源时,服务器并不知道你已经登录过了,前后两次请求没有联系。Cookie
就是为了解决此问题而诞生的。
Cookie
的过程如下:
由上图可知,服务端在响应头中加入Set-Cookie
字段,客户端解析此字段在本地保存下来,在本地保存则意味着不太安全,因此cookie
中不应存储密码等敏感数据,等下次请求时带上此Cookie
,服务端验证此Cookie
就知道客户端是否登陆过了。
Cookie
中存储键值对,具体的属性定义rfc6265,Cookie
类中也对规范进行了实现。
class Cookie private constructor(
@get:JvmName("name") val name: String, //名称
@get:JvmName("value") val value: String, //cookie的值
@get:JvmName("expiresAt") val expiresAt: Long, //字段为此cookie超时时间。若设置其值为一个时间,那么当到达此时间后,此cookie失效。不设置的话默认值是Session,意思是cookie会和session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,此cookie失效。
@get:JvmName("domain") val domain: String, //为可以访问此cookie的域名,下级域名可以设置自己或者上级域名,比如二级域名设置为顶级域名是允许的,反之则Cookie无法生成,同级域名共享则只能设置为共享域名的上级域名
@get:JvmName("path") val path: String, //为可以访问此cookie的页面路径。 比如domain是abc.com,path是/test,那么只有/test路径下的页面可以读取此cookie。
@get:JvmName("secure") val secure: Boolean, //设置是否只能通过https来传递此条cookie
@get:JvmName("httpOnly") val httpOnly: Boolean, //若此属性为true,则只有在http请求头中会带有此cookie的信息,而不能通过document.cookie来访问此cookie。
//Returns true if this cookie does not expire at the end of the current session.
@get:JvmName("persistent") val persistent: Boolean, // True if 'expires' or 'max-age' is present.
@get:JvmName("hostOnly") val hostOnly: Boolean // True unless 'domain' is present.
) {...}
OkHttp
中Cookie
使用如下:
实现CookieJar
接口,并在创建OkHttpClient
时设置即可
具体使用何种策略缓存Cookie
看业务需求,内存,SP
,SQLlite
,Serializable
…都可以,下面实现只是示范。
class MainActivity : AppCompatActivity() {
//使用内存缓存Cookie
val cookieCache = HashMap<String, List<Cookie>>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val okHttpClient = OkHttpClient().newBuilder()
.cookieJar(object: CookieJar{ //cookieJar是接口,需要实现加载和保存Cookie的两个方法,两个方法会在BridgeInterceptor#intercept()中进行调用
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val cookies = cookieCache.get(getCacheKey(url))
return cookies ?: ArrayList()
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
if (cookies.size > 0) {
cookieCache.put(getCacheKey(url), cookies)
}
}
})
.build()
}
}
下面为拓展内容,说到Cookie
则避不开Session
和Token
Session
Session
是在服务端存储客户数据,是服务端保持连接状态的实现。
Cookie
机制如果说是通过检查客户身上的“通行证”来确定客户身份的话,那么Session
机制就是通过检查服务器上的“客户明细表”来确认客户身份。Session
相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要根据sessionid
查询客户档案表就可以了。
Session和HTTP协议并没有关系,其也并不是协议内容,而Cookie是协议规范中具体定义的。
Session
的实现可以借助Cookie
,有一种实现方案则是Cookie
存储一个SessionID
,等下次发起请求时携带此ID
,服务端收到后解析此SessionID
从而辨识用户,服务端是存储在数据库还是内存是服务器的选择,网上很多博客说只在内存是不准确的,Session
只是服务端维持状态技术的名称,而具体怎么维持由服务端具体的业务决定。/
若浏览器禁用Cookie
,还有一种Session
的实现则是放在URL
中,当然放到表单中也是可以实现的。
Session
也存在缺点,若服务器存在负载均衡,如何在多个服务器之间共享Session
则成为问题,跨机的资源消耗是非常大,势必会造成效率下降。
Token
Token
就完美的解决了上述的问题,Token
是面向无状态的。
Token
是如何验证用户身份的呢,过程如下:
- 客户端发起请求,服务端生成一段可以标志客户端身份的文本,并将文本用自己的密钥求得摘要算法,将信息和摘要合并下发客户端
- 客户端收到此段信息,等后续发起其他请求时携带此文本
- 客户端发起新的请求,服务端收到请求,拿到此文本中的身份信息,并用自己的密钥再次做摘要算法,若和摘要相同则认证成功
如此实现服务端只需要做加密算法对比验证即可验证用户合法性,避免了查询数据库,而且Token
还可以解决Session
负载均衡的问题,只需要多个服务器共享密钥即可。
Token
和Cookie
的区别:
Token
可以跨域,Cookie
不可跨域,协议规范中定义每个域名只能管理自己的Cookie
Token
存在哪⼉都⾏,localstorage
或者cookie
Token
可以避免CSRF
攻击Token
完全由应用管理,所以它可以避开同源策略Token
多用于移动客户端,APP
一般不支持Cookie
,但是OkHttp
是支持Cookie
的
JWT
Token
有一种流行的实现,JWT(JSON Web Token)
,在RFC 7519
中定义
关于JWT
的章节来自于知乎大佬java技术爱好者的文章
JWT定义的数据格式如下:
xxxxx.yyyyy.zzzzz
以点分割为三个部分,第一部分是head
,第二部分是Payload
,第三部分是Signature
head
{
"alg": "HS256",
"typ": "JWT"
}
alg
属性表示签名使用的算法,默认为HMAC SHA256
(写为HS256
),type
属性表示令牌的类型,JWT
令牌统一写为JWT
。
最后,使用Base64 URL
算法将上述JSON
对象转换为字符串保存。
Payload
JWT
第二部分是Payload
,也是一个Json
对象,除了包含需要传递的数据,还有七个默认的字段供选择。
分别是,iss
:发行人、exp
:到期时间、sub
:主题、aud
:用户、nbf
:在此之前不可用、iat
:发布时间、jti
:JWT ID
用于标识该JWT
。
如果自定义字段,可以这样定义:
{
//默认字段
"sub":"主题123",
//自定义字段
"name":"java技术爱好者",
"isAdmin":"true",
"loginTime":"2021-12-05 12:00:03"
}
需要注意的是,默认情况下JWT
是未加密的,任何人都可以解读其内容,因此如果一些敏感信息不要存放在此,以防信息泄露。
JSON
对象也使用Base64 URL
算法转换为字符串保存。
Signature
前两部分的摘要,不再过多叙述。
验证过程如Token
小节中的过程,不再赘述。
HTTP协议相关知识总结
BridgeInterceptor
主要处理请求头和压缩,此小节主要总结出现过的请求头和响应头属性。
请求头
Content-Length
和Transfer-Encoding
存在冲突,不可同时存在,其作用在RetryAndFollowUpInterceptor
的HTTP
总结小节已经叙述,此处不再分析。Host
请求的服务器地址或者域名Connection
客户端希望的连接方式,Keep-Alive
代表希望长连接,若返回的响应头中Connection
字段为close
则也不能维持长连接,维持长连接是两端头都需要同意的事Accept-Encoding
客户端支持的压缩编码方式,与响应头中的Content-Encoding
对应,客户端把自己的支持列表发过去,服务端选择一个返回回来Range
表示客户端想要此次请求资源的某一部分如:Range: bytes=0-50, 100-150
,表示只要0-50
和100-150
字节两个部分Cookie
在Cookie小节进行了详细分析,此处不在分析User-Agent
表示此次请求的发起的平台和工具
响应头
Content-Encoding
服务端选择的压缩方式Content-Length
响应体的内容长度Content-Type
响应的MIME
类型Set-Cookie
服务端传给客户端的Cookie
总结
BridgeInterceptor
总体来说比较简单,关于协议的内容也不复杂,主要是用于封装头,了解头属性的处理和作用其实对于理解BridgeInterceptor
是非常有帮助的,相对复杂的则是Cookie
也是面试问的比较多的问题,笔者在上文也对其进行了详细分析。
✨ 原创不易,还希望各位大佬支持一下 \textcolor{blue}{原创不易,还希望各位大佬支持一下} 原创不易,还希望各位大佬支持一下
👍 点赞,你的认可是我创作的动力! \textcolor{green}{点赞,你的认可是我创作的动力!} 点赞,你的认可是我创作的动力!
⭐️ 收藏,你的青睐是我努力的方向! \textcolor{green}{收藏,你的青睐是我努力的方向!} 收藏,你的青睐是我努力的方向!
✏️ 评论,你的意见是我进步的财富! \textcolor{green}{评论,你的意见是我进步的财富!} 评论,你的意见是我进步的财富!
下篇预告:分析OkHttp的Cache类如何运行,其缓存策略是什么,如何进行的缓存?
下篇文章已更新OkHttp原理第五篇-Cache缓存类详解