当前位置:首页 > 技术文章 > 正文内容

被源网站风控的应对方案之请求重放(三)

写在开篇

当我们 采集的数据来源于源网站的API & 请求接口与用户地址栏有对应的唯一关键值 & 短时间内无法逐个突破风控关键点 时,“请求重放”就出场了。

PS. 如果说上一章讲述的 请求头设置 是采集过程中的后置部分的话,那么本章就是数据采集过程的前置篇。因为,只有探索到了真正的请求及请求参数,才能落实到请求里去,所以,这一章的内容讲完,你应该就可以将采集流程串起来&实践数据采集了哦~


数据重放与插件

正如《 浏览器插件数据采集时,被源网站风控的应对方案(一) 》一文中所阐述的,“ 请求重放 ”的现实依据是:用户正常打开源网站时,源网站上的请求可以正常通过源网站的风控系统。据此,我们只需将合规的请求及参数存储下来并重新请求一次,就可以获取到目标数据了。

结合Chrome Extension,这个过程就涉及到了两个问题:

  • 请求及请求参数从哪里来?
  • 请求及请求参数又如何重放?


首先,我们来讲讲“从哪里获取请求内容”。

现在,请你再由“上面阐述的、用户正常操作的交互流程”发散思维:用户是 自行 打开源网页进行浏览时,可以看到完整的数据。那么,如果,Chrome Extension能为我们 自动的、默默的 打开页面&等我们拿到请求信息后再关掉TAB的话就好了(静默打开 是为了用户侧没有那么强烈的交互感官刺激)

而对此,Chrome Extension当然是拥有操控TAB的能力的,所以第一个问题“光靠人为操作的流程太过繁琐&不便捷”从理论上已经解决。那,又如何获取到请求内容呢?这就涉及到了Service Worker层的
chrome.webRequest.onBeforeSendHeaders.addListener
监听设置技术点啦。本章就不对这块内容做详述介绍了,基本原理就是为某一类请求设置一种监听:当某种请求出现时,获取到这种请求及该请求的参数。

// replayRequestUtil.js文件,该文件主要用来处理重放请求的监听设置+请求重放

// 全局变量,在service worker休眠时会被清除哦~
const _tempRequestStorage = {};

export function addOnBeforeSendHeaderListener() {
  /**
   * 以下这段代码是用检测是否有监听函数的;
   *
   * 如果addOnBeforeSendHeaderListener该函数只是在service worker启动或休眠启动时执行的话,这段代码就不需要(本次示例采用的是这种方案,所以注释了代码)
   * 如果addOnBeforeSendHeaderListener该函数是由content层等入口触发(即,可能在同一段service worker被激活的时间段内执行多次的),则需要这段代码
   *
   * const _existListeners = chrome.webRequest.onBeforeSendHeaders.hasListeners();
   * if (_existListeners) {
   *   return;
   * }
   *
   * */

  /**
   * 以下设置监听回调函数时,我们模拟的场景是:
   *
   * 用户浏览器地址栏中的地址是:https://www.fun8.top/detail.html?itemId=10001
   * 页面请求数据的API地址是(为了区分主域名,也可以让读者区分):https://www.fun8.top/api/get-detail?id=10001
   *
   * */
  const _apiUrlReg = /\/\/www\.fun8\.top\/api\/get\-detail/; // 这个变量可以作为系统级别的常量,我放在这里是为了较好的展示demo

  chrome.webRequest.onBeforeSendHeaders.addListener(
    (details) => {
      // 因为一般会指定多种类别的地址,所以必须更细粒度的验证、获取请求参数 & 存储
      if (details.url && _apiUrlReg.test(details.url)) {
        console.log('进入监听回调函数&命中目标地址', details);
        const _url = new URL(details.url);


        // 需要从请求链接中找到唯一性指征参数,这是使用“请求重放”的必要条件,否则会造成数据混乱
        const _id = _url.searchParams.get('id');
        if (_id) {
          const _hKey = JSON.stringify({
            tabId: details.tabId,
            url: _url.origin + _url.pathname,
            id: _id,
          });


          // 这里我们使用的是全局变量,因为设计方案“打开TAB-请求获得/存储-请求重放-关闭TAB”是一气呵成的,理论上不会涉及到Service Worker的休眠;
          // 如果你的技术方案是需要先存储再经过较长时间(比如5分钟)再使用的,请使用chrome.storage或IndexDb这种持久性存储介质,因为全局变量在service worker的休眠时会被清除;
          _tempRequestStorage[_hKey] = {
            url: details.url,
            headers: details.requestHeaders || [],
          };
        }
      }
    },
    {
      urls: [
        'https://www.fun8.top/api/*', // 因为这里指定的是一系列符合通配符的API地址(注意不是用户地址栏的地址),所以在回调函数中需要再细颗粒度的验证&获取数据
        'https://api.fun8.top/item/*', // 可以指定多种地址
      ],
    },
    [
      'requestHeaders', // 指定需要在拦截时将requestHeaders返回给回调函数,如果不设置的话,就不能在回调函数中获取到哦~
      'extraHeaders', // 一定要设置这个,不然就取不到origin这种关键请求头了哦~
    ],
  );

  console.log('设置请求头监听函数完成');
}


// service-worker.js文件 入口处,添加设置监听代码
import { addOnBeforeSendHeaderListener } from '@/utils/replayRequestUtil';

addOnBeforeSendHeaderListener();


说完了“获取请求”相关内容,接下来的“请求重放”就简单了:不过就是从存储介质中将数据提取出来,再用fetch等浏览器API执行一下请求一下就可以了~

// replayRequestUtil.js文件
import { delay } from 'lodash';
import { _fetchApi } from '@/utils/chromeDeclarativeNetRequestUtil'; // 这是上一章中的函数,本节中就直接拿过来用了


export function sleep(millisecond) {
  return new Promise((resolve) => {
    delay(() => {
      resolve({});
    }, millisecond);
  });
}


// 这个函数一般由content层触发
// url: https://www.fun8.top/detail.html?itemId=10001
export async function autoOpenTabAndGetData(url) {
  if (!/\/\/www\.fun8\.top\/detail\.html/.test(url)) {
    return { success: false, message: '请求链接出错' };
  }


  const _url = new URL(url);
  const _itemId = _url.searchParams.get('itemId');
  if (!_itemId) {
    return { success: false, message: '请求链接出错-2' };
  }


  console.log('新TAB准备打开');
  const _tab = await chrome.tabs.create({ active: false, url }); // 自动打开页面,active:false 代表的是静默打开tab页
  console.log(`新TAB已打开,TAB ID - ${_tab.id}`);


  let _tempRequestInfo;


  let _checkLoopIndex = 0;
  while (_checkLoopIndex < 20) {
    // 在等待的这2秒中,因为tab页的打开,所以chrome.webRequest.onBeforeSendHeaders的监听函数会被执行到
    // 每2秒检测一次是否已经拿到头部信息,一般在第一个2秒的时候就拿到了的
    // 另外,如果40秒还拿不到请求头的,就可以放弃了
    await sleep(2000);


    console.log(`第${_checkLoopIndex}验证`);


    // 这里利用唯一性键值的取值与上面的设置形成了闭环
    const _hKey = JSON.stringify({
      tabId: _tab.id,
      url: 'https://www.fun8.top/api/get-detail',
      id: _itemId,
    });


    ++_checkLoopIndex;


    if (_tempRequestStorage[_hKey]) {
      _tempRequestInfo = _tempRequestStorage[_hKey];
      console.log(`获取到请求信息 - 第${_checkLoopIndex}验证`);


      break; // 拿到请求信息就可以了
    }
  }


  console.log(`TAB-${_tab.id}准备关闭`);


  // 这里异步执行就行~
  chrome.tabs.remove([_tab.id], () => {
    console.log(`TAB-${_tab.id}已关闭`);
  });


  if (!_tempRequestInfo) {
    return { success: false, message: '请求信息获取失败' };
  }


  // 从监听函数中获取到的headers信息是[{name:'xxx',value:'xxxx'}]格式的,但是_fetchApi需要得是对象格式的,所以处理一下
  // 这块代码可以去上一篇中的_fetchApi兼容,我这里为了少贴代码所以在这里处理了一下
  const _headers = {};
  for (const _item of _tempRequestInfo?.headers || []) {
    _headers[_item.name.toLowerCase()] = _item.value;
  }


  console.log(`重放请求开始`);
  const _response = await _fetchApi({ url: _tempRequestInfo.url, headers: _headers });
  const _result = await _response.json();
  console.log(`重放请求结束,请求结果:${_result}`);


  return { success: true, message: '请求成功', result: _result };
}


// content.js文件 - 向页面上嵌入一个按钮,用于执行代码
const _container = document.getElementById('head');
const _btnDom = document.createElement('button');
_btnDom.innerHTML = '点击';
_container.appendChild(_btnDom);


_btnDom.addEventListener('click', async function () {
  // contentToServiceWorkerByLongConnection这个函数就是使用长链接通信通道向service worker发送消息
  contentToServiceWorkerByLongConnection({
    command: 'batchCollectItemData',
    payload: {
      urlList: [
        'https://www.fun8.top/detail.html?itemId=10001',
        'https://www.fun8.top/detail.html?itemId=10002',
        'https://www.fun8.top/detail.html?itemId=10003',
      ],
    },
  });
});


// service-worker.js文件


async function _batchCollectItemData(payload, port) {
  const { urlList } = payload;


  for (const _url of urlList) {
    const _result = await autoOpenTabAndGetData(_url);


    console.log('请求完毕', _result.success, _result.result);


    // 一般情况下,获取到数据之后会直接抛给服务端或第三方存储系统;


    // 另外,或为了交互、或为了保持content与service长链接通信通道,一般,我们会每次请求完给content层回复一条消息;
    // 这块逻辑和本章的内容没有很直接的关系,不过是从技术方案的完整性考虑才加上的代码
    port.postMessage({
      command: 'resolveBatchCollectItemData',
      payload: {
        url: _url,
      },
    });


    // 一定要注意设置请求间隔,不要给源网站造成困扰哦~
    await sleep(10000);
  }
}


chrome.runtime.onConnect.addListener(function (port) {
  port.onMessage.addListener(function (message) {
    if(message.command === 'batchCollectItemData'){
      _batchCollectItemData(payload, port);
    }
  });
});


好啦,到此,从Chrome Extension出发,实践“请求重放”方案就到此为止啦~接下来,我来展示下我本地的交互Demo~(PS.你可以对照上面的代码与视频中的日志理解)

点击这里查看测试流程交互视频


写在结尾

除去代码,本章其实很简短,但是涵盖了“Chrome Extension自动操控TAB与请求拦截”等实践性较强的内容,所以,如果想较好的理解本章知识点,请务必实践一遍或至少对照视频与代码理解一遍(如有问题,请留言哦~)

另外,最后着重说明两点,也是上述技术方案中并未涉及持久化存储,而是设计成简单的“ 自动打开TAB-获取请求及参数-重放请求-关闭TAB ”流程的两个原因:

  1. 很多时候,源网站的风控系统都会检测请求时间,如果你将一个请求存储下来&很长一段时间之后再去使用的话,很有可能这个请求会失效;
  2. 因为“请求重放时,请求参数与之前的一次请求完全一致”,如果源网站有意要拦截的话是十分简单的,所以大家一定要控制好重放次数。一般,一个请求重放一次就可以丢弃了;


最后的最后,打个小广告:本章内容只适用于“源网站数据来源于API的数据获取,而非来源于页面dom结构的数据获取”,如果想知道如何快速&尽量少被风控的同学可以期待一下我的下一篇文章哦~;

相关文章

Ubuntu 25.04发行版登场:Linux 6.14内核,带来多项技术革新

IT之家 4 月 18 日消息,科技媒体 linuxiac 昨日(4 月 17 日)发布博文,报道称代号为 Plucky Puffin 的 Ubuntu 25.04 发行版正式上线,搭载最新 Linu...

细数5款国外热门Linux发行版(linux发行版排名网站)

Linux系统已经与我们的生活息息相关,当你用Android手机浏览这篇文章时,你就已经在使用Linux系统。当然作为编程开发最热门的系统,他还有很多专注于开发使用的版本。Fedora热门入门推荐,一...

Win+Ubuntu缝合怪:第三方开发者推出“Wubuntu”Linux发行版

IT之家 2 月 26 日消息,一位第三方开发者推出了一款名为“Wubuntu”的缝合怪 Linux 发行版,系统本身基于 Ubuntu,但界面为微软 Windows 11 风格,甚至存在微软 Win...

2023 年 10 个最佳 Linux 桌面发行版

Linux 操作系统在桌面领域的发展已经不再被忽视,越来越多的用户正在考虑切换到 Linux 上。在 2023 年,我们可以期待更多的 Linux 桌面发行版的推出和发展。这里列举了 10 个最佳的...

「 VUE3 + TS + Vite 」父子组件间如何通信?

组件之间传值,大家都很熟悉,涉及到 VUE3 +TS 好多同学就无从下手了,所以分享这篇文章,希望看完后提起 VUE3+TS 能够不慌不忙。平时使用的函数如:ref、reactive、watch、co...

前端React面试基础系列(React基础篇)

本文阅读8分钟,喜欢的小伙伴可以持续关系小编哦1. 什么是受控组件和非受控组件?受控组件像表单元素在用户输入时,像<input> <select>等元素需要绑定一个 chang...