OkHttp原理第四篇-BridgeInterceptor

作者简介: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中存储键值对,具体的属性定义rfc6265Cookie类中也对规范进行了实现。

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.
) {...}

OkHttpCookie使用如下:

实现CookieJar接口,并在创建OkHttpClient时设置即可

具体使用何种策略缓存Cookie看业务需求,内存,SPSQLliteSerializable…都可以,下面实现只是示范。

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则避不开SessionToken

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是如何验证用户身份的呢,过程如下:

  1. 客户端发起请求,服务端生成一段可以标志客户端身份的文本,并将文本用自己的密钥求得摘要算法,将信息和摘要合并下发客户端
  2. 客户端收到此段信息,等后续发起其他请求时携带此文本
  3. 客户端发起新的请求,服务端收到请求,拿到此文本中的身份信息,并用自己的密钥再次做摘要算法,若和摘要相同则认证成功

如此实现服务端只需要做加密算法对比验证即可验证用户合法性,避免了查询数据库,而且Token还可以解决Session负载均衡的问题,只需要多个服务器共享密钥即可。

TokenCookie的区别:

  • 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:发布时间、jtiJWT ID用于标识该JWT

如果自定义字段,可以这样定义:

{
    //默认字段
    "sub":"主题123",
    //自定义字段
    "name":"java技术爱好者",
    "isAdmin":"true",
    "loginTime":"2021-12-05 12:00:03"
}

需要注意的是,默认情况下JWT是未加密的,任何人都可以解读其内容,因此如果一些敏感信息不要存放在此,以防信息泄露。

JSON对象也使用Base64 URL算法转换为字符串保存。

Signature

前两部分的摘要,不再过多叙述。

验证过程如Token小节中的过程,不再赘述。

HTTP协议相关知识总结

BridgeInterceptor主要处理请求头和压缩,此小节主要总结出现过的请求头和响应头属性。

请求头

  • Content-LengthTransfer-Encoding存在冲突,不可同时存在,其作用在RetryAndFollowUpInterceptorHTTP总结小节已经叙述,此处不再分析。
  • Host 请求的服务器地址或者域名
  • Connection 客户端希望的连接方式,Keep-Alive代表希望长连接,若返回的响应头中Connection字段为close则也不能维持长连接,维持长连接是两端头都需要同意的事
  • Accept-Encoding 客户端支持的压缩编码方式,与响应头中的Content-Encoding对应,客户端把自己的支持列表发过去,服务端选择一个返回回来
  • Range 表示客户端想要此次请求资源的某一部分如:Range: bytes=0-50, 100-150,表示只要0-50100-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缓存类详解

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值