本文旨在总结与分享笔者的学习经历,能力有限若有不确之处,望请交流指正.

技术简介

在自动化开发中,经常面临HTTP请求模拟,经常用有如Postman,Burp这样带网络调试能力的工具,当我编写其他语言时有XMLHttpRequestReqwestAxios这类HTTP框架,但如果需要用go来解决这一切呢?今天我要向你介绍。

import "github.com/imroc/req"

Req 是一个简单易用还带黑魔法的 Go HTTP 客户端,写更少的代码,获得更高的效率.

初探请求

一个完备的HTTP客户端请求库具有起码的GET/POST的功能,用req发送请求的几种方式:

GET

发送一次简单的GET

func main() {
	//req.C() => client创建客户端
	client := req.C() //.
	//req.C().R()  => request请求回应
	resp, err := client.R().Get("http://whois.pconline.com.cn/ipJson.jsp")
	if err != nil {
		log.Fatal(err)
	}
 	//IPCallBack({"ip":"58.249....","pro":"广东省
    fmt.Println(resp)
}

自动解码&序列化

req默认会嗅探消息,对接收到的消息主体自动解码,req也提供了消息序列的能力。

source: req.cool/zh/docs/tutorial/auto-decode/

  1. 首先检测 Content-Type 头,如果是文本内容类型(json、xml、html 等),req 会尝试解码,如果不是,则不会解码。
  2. 如果没有 Content-Type 头,会提取响应体首部内容进行嗅探,如果嗅探出来明确不是 UTF-8,则自动解码为 UTF-8,如果字符集不确定,则不会尝试解码。

source: req.cool/zh/docs/tutorial/marshal-unmarshal/ Req 默认使用标准库的 Marshal 和 Unmarshal 实现:

  • 如果将 map 或 struct 传入 Request.SetBody, Request.SetBodyJsonMarshal or Request.SetBodyXmlMarshal,会使用 json.Marshalxml.Marshal 来将请求体 Marshal 成指定编码格式的内容。
  • 如果将 map 或 struct 传入 Request.SetResult, Request.SetErrorResponse.Unmarshal,会根据编码格式使用 json.Unmarshalxml.Unmarshal 来响应体 Unmarshal 到传入的 map 或 struct 中。

DO-GET

req支持DO风格的请求方式,链式调用顺序的写法使得请求代码的可读性更好。

下面是一段展示req自动解码&序列能力的DO风格请求代码:

type info struct {
	Ip   string `json:"ip"`
	Pro  string `json:"pro"`
	City string `json:"city"`
	Addr string `json:"addr"`
	Err  string `json:"err"`
}

func main() {
	client := req.C()
	var resp info
	err := client.
		Get("http://whois.pconline.com.cn/ipJson.jsp").
		AddQueryParams("ip", "114.114.114.114").
		AddQueryParams("json", "true").
		Do().
		Into(&resp)
	if err != nil {
		log.Fatal(err)
	}
	if resp.Err != "" {
		log.Fatal(resp.Err)
	}
	//114.114.114.114 江苏省南京市 电信
	fmt.Println(resp.Ip, resp.Addr)
}

DO-POST

同样的,POST请求也很简单,以百度翻译接口为例

type Word struct {
	Data []struct {
		K string `json:"k"`
		V string `json:"v"`
	} `json:"data"`
	Logid int64 `json:"logid"`
}

func main() {
	client := req.C()
	var resp Word
	err := client.
		Post("https://fanyi.baidu.com/sug").
		SetBody("kw=apple").
		SetHeader("content-type", "application/x-www-form-urlencoded").
		Do().
		Into(&resp)
	if err != nil {
		log.Fatal(err)
	}
    //[{Apple n. 苹果公司,原称苹果电脑公司} {apple n. 苹果树} ...
	fmt.Println(resp.Data)
}

记录与携带本地Cookie

在业务查询时,我们经常需要携带token来保持自己访问权限,典型的有如Jsessionid、ASPsession。Req会自动保存和添加Cookie,简化了这些非常麻烦的请求头设置操作,同时还支持将Cookie持久化存储到本地,让程序启动时调用,结束时自动保存。

source : req.cool/zh/docs/tutorial/cookie/ req 默认启用了 cookie 存储,当 server 返回的 header 中包含 Set-Cookie 时,req 默认会自动存储并在后续需要 cookie 的请求中自动带上 cookie 请求头。

这是一段简单的携带Cookie查询用户哔哩哔哩经验记录的的代码:


	client := req.C()
	client.
		SetTimeout(10 * time.Second)
	type log struct {
		Code    int    `json:"code"`
		Message string `json:"message"`
		TTL     int    `json:"ttl"`
		Data    struct {
			List []struct {
				Delta  int    `json:"delta"`
				Time   string `json:"time"`
				Reason string `json:"reason"`
			} `json:"list"`
			Count string `json:"count"`
		} `json:"data"`
	}
	var loga log
	client.
		Get("https://api.bilibili.com/x/member/web/exp/log").
		SetHeader("cookie", "SESSDATA=....").
		Do().
		Into(&loga)
	if loga.Code == 0 {
		for _, v := range loga.Data.List {
			fmt.Println("经验:" + strconv.Itoa(v.Delta) + "	时间:" + v.Time + "	事件:" + v.Reason)
		}
	}
/*
    经验:5 时间:2023-10-06 00:00:12       事件:观看视频奖励
    经验:5 时间:2023-10-06 00:00:12       事件:登录奖励
    经验:5 时间:2023-10-05 18:59:12       事件:观看视频奖励
....
*/

通常在go网络编程里,我们会常用类似CookieJar来存储与管理Cookie,req框架也支持自定义的Cookie存储能力,以Cookiejar为例,读取与保存到本地json。

import (
	"fmt"
	"log"
	"strconv"
	"time"

	"github.com/imroc/req/v3"
	cookiejar "github.com/juju/persistent-cookiejar"
)

func client() (*req.Client, *cookiejar.Jar) {
	jar, err := cookiejar.New(&cookiejar.Options{
		Filename: "cookies.json",
	})
	if err != nil {
		log.Fatalf("failed to create persistent cookiejar: %s\n", err.Error())
	}
	client := req.C()
	client.
		SetTimeout(10 * time.Second).
		SetCookieJar(jar) //自定义存储
	return client, jar
}

func main() {
	type log struct {
		Code    int    `json:"code"`
		Message string `json:"message"`
		TTL     int    `json:"ttl"`
		Data    struct {
			List []struct {
				Delta  int    `json:"delta"`
				Time   string `json:"time"`
				Reason string `json:"reason"`
			} `json:"list"`
			Count string `json:"count"`
		} `json:"data"`
	}
	var loga log
	client, jar := client()
	defer jar.Save() //保存
	client.
		Get("https://api.bilibili.com/x/member/web/exp/log").
		Do().
		Into(&loga)
	if loga.Code == 0 {
		for _, v := range loga.Data.List {
			fmt.Println("经验:" + strconv.Itoa(v.Delta) + "	时间:" + v.Time + "	事件:" + v.Reason)
		}
	} else {
		fmt.Println(loga.Message) //账号未登录
	}
}

代理请求

有时候受环境限制,需要通过代理网络才能完成请求,req也支持了客户端的代理设置,以请求ip地址为例。

func client() *req.Client {
	client := req.C()
	client.
		SetProxyURL("socks5://localhost:7890")
	return client
}

func main() {
	type info struct {
		Ip   string `json:"ip"`
		Pro  string `json:"pro"`
		City string `json:"city"`
		Addr string `json:"addr"`
		Err  string `json:"err"`
	}
	var resp info
	client().
		Get("http://whois.pconline.com.cn/ipJson.jsp").
		AddQueryParams("json", "true").
		Do().
		Into(&resp)
	/*
		PS ...> go run A.go
		广东省广州市 广东工贸职业技术学院
		PS ...> go run B.go
		新加坡....数据中心
	*/
	fmt.Println(resp.Addr)
}

HTTP指纹模拟

我们知道,传统的HTTP请求特征比较多,而现在依赖来源IP,来源UA的检测信任正在降低,与之取代的是HTTP指纹HTTP指纹是利用了如TLS握手特征,HTTP2属性帧的值和排列顺序,HTTP特殊请求头等等进行验证,当服务端在检测某些特征来源在高频次的请求后予以处理。

在传统的Request等请求库中缺失了针对这些特征检查的模拟能力,而req最新版本声称支持了对于HTTP的指纹特征的自定义伪装。

source : req.cool/zh/blog/已支持-http-指纹伪装轻松绕过反爬虫检测

……

HTTP 指纹是由一系列特征构成的,服务端检查的特征越详细(反爬级别越高),伪装的难度就越高,当然如果我们把所有特征全都伪装了,那就一定就能骗过服务端了。

常见的一些特征:

  1. User-Agent 的值。
  2. Header 及其排列顺序。
  3. TLS 指纹,也就是TLS 握手时,客户端发送 ClientHello 的特征。包含客户端所支持的加密套件列表及其排列顺序、客户端支持的TLS版本、压缩方式、TLS extensions 列表及其排列顺序等。

对于 HTTP2,还有更多额外可以检查的特征:

  1. HTTP2 settings 帧中的值列表及其排列顺序。
  2. HTTP2 WINDOW_UPDATE 帧的值。
  3. HTTP2 Priority 帧列表及其排列顺序。
  4. 伪头(Pseudo-Header) 的顺序。
  5. 请求头及其排列顺序,以及 header 帧中 flag 与 priority 选项的值。

以上所有特征,在 req 中都可以轻松模拟,实现伪装成任何想要的浏览器客户端。

ja3检测为例,传统的客户端请求,会生成固定的ja3指纹:

func client() *req.Client {
	client := req.C()
	return client
}

func main() {
	type info struct {
		Hash string `json:"ja3_hash"`
		Text string `json:"ja3_text"`
	}
	var resp info
	client().
		Get("https://tls.browserleaks.com/json").
		Do().
		Into(&resp)

	/*
		PS ...> go run C.go
		7a15285d4efc355608b304698cd7f9ab 771,49195-49199-49196-49200-52393....
		PS ...> go run C.go
		7a15285d4efc355608b304698cd7f9ab 771,49195-49199-49196-49200-52393....
	*/
	fmt.Println(resp.Hash, resp.Text)
}

设置客户端伪装后:

package main

import (
	"fmt"

	"github.com/imroc/req/v3"
)

func client() *req.Client {
	client := req.C()
	client.
		ImpersonateChrome() //伪装Chrome
	return client
}

func main() {
	type info struct {
		Hash string `json:"ja3_hash"`
		Text string `json:"ja3_text"`
	}
	var resp info
	client().
		Get("https://tls.browserleaks.com/json").
		Do().
		Into(&resp)
	/*
		PS ...> go run C.go
		c310f0fbecd660195ed31f273f938e05 771,4865-4866-4867-49195-49199-49196-49200-52393
		PS ...> go run C.go
		0d6c3d08a9204af8a40f24b9168cd090 771,4865-4866-4867-49195-49199-49196-49200-52393
	*/
	fmt.Println(resp.Hash, resp.Text)
}