背景介绍
使用 chrome.webRequest API 观察和分析流量并拦截、阻止或修改正在进行的请求
HTTP请求的生命周期
?
事件 | 解释 | onBeforeRequest | 当请求即将发生时触发。此事件在建立任何 TCP 连接之前发送,可用于取消或重定向请求。 | onBeforeSendHeaders | 当请求即将发生并且初始标头已准备好时触发。 该事件旨在允许扩展添加、修改和删除请求标头 (*)。 onBeforeSendHeaders 事件传递给所有订阅者,因此不同的订阅者可能会尝试修改请求; 有关如何处理的信息,请参见实施细节部分。 此事件可用于取消请求。 | onSendHeaders | 在所有扩展都有机会修改请求标头后触发,并呈现最终 (*) 版本。 该事件在标头发送到网络之前触发。 此事件是信息性的并且是异步处理的。 它不允许修改或取消请求。 | onHeadersReceived | 每次收到 HTTP(S) 响应标头时触发。 由于重定向和身份验证请求,每个请求可能会发生多次。 此事件旨在允许扩展添加、修改和删除响应标头,例如传入的 Content-Type 标头。 在触发此事件之前会处理缓存指令,因此修改 Cache-Control 等标头对浏览器的缓存没有影响。 它还允许您取消或重定向请求。 | onAuthRequired | 当请求需要对用户进行身份验证时触发。 可以同步处理此事件以提供身份验证凭据。 请注意,扩展可能会提供无效的凭据。 注意不要通过重复提供无效凭据进入无限循环。 这也可以用来取消请求。 | onBeforeRedirect | 在即将执行重定向时触发。重定向可以由 HTTP 响应代码或扩展触发。此事件是信息性的并且是异步处理的。它不允许您修改或取消请求。 | onResponseStarted | 当收到响应body的第一个字节时触发。 对于 HTTP 请求,这意味着状态行和响应标头可用。 此事件是信息性的并且是异步处理的。 它不允许修改或取消请求。 | onCompleted | 成功处理请求时触发。 | onErrorOccurred | 当请求无法成功处理时触发。 |
认证凭证
webRequest.onAuthRequired
参考文档:
webRequest.onAuthRequired - Mozilla | MDN
这个事件如何触发的?是第一次请求中了 response header 中带有了WWW-Authenticate 与 Proxy-Authenticate 两个 header 标签,此时被识别为需要服务端认证 和 代理认证
补充知识点
认证和授权,英文分别为:authentication 和 permission 这两个词非常重要,认证的意思是你需要带什么凭证我才相信你能进来,授权是我看着你的证件,应该给予你什么权利,举个很好理解的例子:
当你去一个企事业单位,你带着工牌,工牌上的信息,就是认证信息,你拿着这个认证信息是可以进门的,但进门后,需要认证人员来赋予你出入那些地方的凭证,这个新的凭证就是权限,带着这些权限,你才能范围机构内的那些部门,所有的权限都是这样设计的,而一个个授权再一个个比对的话,比较浪费时间。最后就造了角色,比如你是局长,带着局长的牌子就可以出入局长的权利范围内的各个区域
都有那些认证方式?
Basic (查看 RFC 7617, base64编码凭证. 详情请参阅下文.),
Bearer (查看 RFC 6750, bearer 令牌通过OAuth 2.0保护资源)
Digest (查看 RFC 7616, 只有 md5 散列 在Firefox中支持, 查看 bug 472823 用于SHA加密支持),
HOBA (查看 RFC 7486 (草案), HTTP Origin-Bound 认证, 基于数字签名),
Mutual (查看 draft-ietf-httpauth-mutual),
AWS4-HMAC-SHA256 (查看 AWS docs).
Basic 的认证是什么样子的?
-
当你刚要进机构的时候,此时会有人说:“请出示你的证件”,他的表现形式就是在 Response 的 header 中给出 WWW-Authenticate: Basic realm="工作人员",realm 是字符串,英文翻译领域,意思就是告诉你,我要的是工作人员的认证信息,如果你上来就进局长门,那么 realm 应该就是局长。 -
于是你从口袋里拿出证件,再次交给守卫们,并告诉他,Authorization: Base base64(username:password) ,守卫拿到后,反 base64 后得到你的账号密码,验证你的身份,然后告诉你可以进去了,记得这已经是第二次了 -
常规的 http 协议传输,给你感觉是一次调用后就结束了的链接,实际上 http 请求是一个 tcp/ip 长连接下的一种协议传输,平时给你单次连接后断开是是内部程序事件循环调度的结果,浏览器线程一直都开着,tcp/ip 可能一直连接着,所以第一次不成功后返回,但是浏览器并没有直接 response 返回给你,而是问你要凭证,你输入凭证后,程序将带有认证信息头的 http 协议的请求又发了一次,这次就成功了。
详细参考
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Authentication#www-authenticate_%E4%B8%8E_proxy-authenticate_%E9%A6%96%E9%83%A8https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Authentication#www-authenticate_与_proxy-authenticate_首部
Basic 碰到代理时
Response header: Proxy-Authenticate: <type> realm=<realm>
Request header: Proxy-Authorization: <type> <credentials>
webRequest 的 blocking
blocking 的意思是指,上述的 http 传输过程中,一般程序是不停下来的,比如通知事件是不需要与主线程交互的,主线程就是通知一下子线程,至于你怎么干它不关心。
但是,像要权限这种东西,主线程自己搞不定,他需要你回传认证信息给它,它再重新打包 header 再次发送请求出去,此时你需要让他等一会,这个等待就是关键词 blocking
所以,认证这个环节需要阻塞权限 webRequestBlocking,这个权限类似于开关,你加上这个权限意味着,程序在运行到权限校验时,会判断是否需要等待你返回认证信息,如果你不开启,它就继续走下去,不等待你的返回信息。这个标识在 extraInfoSpec 中填写。
browser.webRequest.onAuthRequired.addListener(
listener, // function
filter, // object
extraInfoSpec // optional array of strings
)
browser.webRequest.onAuthRequired.removeListener(listener)
browser.webRequest.onAuthRequired.hasListener(listener)
?
其他事件,请自行阅读谷歌文档,说实话,当自己理解了这个概念时,再看谷歌文档,是发现真的比国内的技术文档好太多,一看就是搞技术的人写的,这份认真态度值得学习。
额外注意事项
-
webrequest 拿到不到 responseBody 怎么办,是的,你没看错,onCompleted 事件中并没有这个东西,此外你如果想要拿到 requestBody 时,在onBeforeRequest 事件中,extraInfoSpec 要增加["requestBody"]返回,此时你知道 extraInfoSpec 的用途了吗,就是一个可变的开关参数,根本不同的事件可能需要返回不同的值或者开关,这个就很好用。 -
用 chrome.devtools.network.onRequestFinished 来获取 responseBody 细节请自行谷歌,讲这个有啥用?你的模拟抓取时有用
const target = "https://httpbin.org/basic-auth/*";
const pendingRequests = [];
/*
* A request has completed. We can stop worrying about it.
*/
function completed(requestDetails) {
console.log(`completed: ${requestDetails.requestId}`);
let index = pendingRequests.indexOf(requestDetails.requestId);
if (index > -1) {
pendingRequests.splice(index, 1);
}
}
function provideCredentialsAsync(requestDetails) {
// If we have seen this request before,
// then assume our credentials were bad,
// and give up.
if (pendingRequests.indexOf(requestDetails.requestId) != -1) {
console.log(`bad credentials for: ${requestDetails.requestId}`);
return {cancel: true};
} else {
pendingRequests.push(requestDetails.requestId);
console.log(`providing credentials for: ${requestDetails.requestId}`);
// we can return a promise that will be resolved
// with the stored credentials
return browser.storage.local.get(null);
}
}
browser.webRequest.onAuthRequired.addListener(
provideCredentialsAsync,
{urls: [target]},
["blocking"]
);
browser.webRequest.onCompleted.addListener(
completed,
{urls: [target]}
);
browser.webRequest.onErrorOccurred.addListener(
completed,
{urls: [target]}
);
|