go 自定义http.Client - 动态修改请求Body-编程思维

go 自定义http.Client - 动态修改请求Body

前言

在对接Alexa Smart Home时,有的请求Payload中需要传入Access Token,但是这个Token是由OAuth2 Client管理的,封装Payload时并不知道Access Token。

所以使用自定义RoundTripper,在请求前取出Header里的token,修改body,实现动态修改payload。

原理

go中可以使用http.DefaultClient进行http请求,也可以自己创建http.Client,传入自定义Transport可以实现对request的处理。

http.Client

// A Client is an HTTP client. Its zero value (DefaultClient) is a
// usable client that uses DefaultTransport.
//
// The Client's Transport typically has internal state (cached TCP
// connections), so Clients should be reused instead of created as
// needed. Clients are safe for concurrent use by multiple goroutines.
//
// A Client is higher-level than a RoundTripper (such as Transport)
// and additionally handles HTTP details such as cookies and
// redirects.
//
// When following redirects, the Client will forward all headers set on the
// initial Request except:
//
// • when forwarding sensitive headers like "Authorization",
// "WWW-Authenticate", and "Cookie" to untrusted targets.
// These headers will be ignored when following a redirect to a domain
// that is not a subdomain match or exact match of the initial domain.
// For example, a redirect from "foo.com" to either "foo.com" or "sub.foo.com"
// will forward the sensitive headers, but a redirect to "bar.com" will not.
//
// • when forwarding the "Cookie" header with a non-nil cookie Jar.
// Since each redirect may mutate the state of the cookie jar,
// a redirect may possibly alter a cookie set in the initial request.
// When forwarding the "Cookie" header, any mutated cookies will be omitted,
// with the expectation that the Jar will insert those mutated cookies
// with the updated values (assuming the origin matches).
// If Jar is nil, the initial cookies are forwarded without change.
//
type Client struct {
	// Transport specifies the mechanism by which individual
	// HTTP requests are made.
	// If nil, DefaultTransport is used.
	Transport RoundTripper

	// CheckRedirect specifies the policy for handling redirects.
	// If CheckRedirect is not nil, the client calls it before
	// following an HTTP redirect. The arguments req and via are
	// the upcoming request and the requests made already, oldest
	// first. If CheckRedirect returns an error, the Client's Get
	// method returns both the previous Response (with its Body
	// closed) and CheckRedirect's error (wrapped in a url.Error)
	// instead of issuing the Request req.
	// As a special case, if CheckRedirect returns ErrUseLastResponse,
	// then the most recent response is returned with its body
	// unclosed, along with a nil error.
	//
	// If CheckRedirect is nil, the Client uses its default policy,
	// which is to stop after 10 consecutive requests.
	CheckRedirect func(req *Request, via []*Request) error

	// Jar specifies the cookie jar.
	//
	// The Jar is used to insert relevant cookies into every
	// outbound Request and is updated with the cookie values
	// of every inbound Response. The Jar is consulted for every
	// redirect that the Client follows.
	//
	// If Jar is nil, cookies are only sent if they are explicitly
	// set on the Request.
	Jar CookieJar

	// Timeout specifies a time limit for requests made by this
	// Client. The timeout includes connection time, any
	// redirects, and reading the response body. The timer remains
	// running after Get, Head, Post, or Do return and will
	// interrupt reading of the Response.Body.
	//
	// A Timeout of zero means no timeout.
	//
	// The Client cancels requests to the underlying Transport
	// as if the Request's Context ended.
	//
	// For compatibility, the Client will also use the deprecated
	// CancelRequest method on Transport if found. New
	// RoundTripper implementations should use the Request's Context
	// for cancellation instead of implementing CancelRequest.
	Timeout time.Duration
}

http.RoundTripper

// RoundTripper is an interface representing the ability to execute a
// single HTTP transaction, obtaining the Response for a given Request.
//
// A RoundTripper must be safe for concurrent use by multiple
// goroutines.
type RoundTripper interface {
	// RoundTrip executes a single HTTP transaction, returning
	// a Response for the provided Request.
	//
	// RoundTrip should not attempt to interpret the response. In
	// particular, RoundTrip must return err == nil if it obtained
	// a response, regardless of the response's HTTP status code.
	// A non-nil err should be reserved for failure to obtain a
	// response. Similarly, RoundTrip should not attempt to
	// handle higher-level protocol details such as redirects,
	// authentication, or cookies.
	//
	// RoundTrip should not modify the request, except for
	// consuming and closing the Request's Body. RoundTrip may
	// read fields of the request in a separate goroutine. Callers
	// should not mutate or reuse the request until the Response's
	// Body has been closed.
	//
	// RoundTrip must always close the body, including on errors,
	// but depending on the implementation may do so in a separate
	// goroutine even after RoundTrip returns. This means that
	// callers wanting to reuse the body for subsequent requests
	// must arrange to wait for the Close call before doing so.
	//
	// The Request's URL and Header fields must be initialized.
	RoundTrip(*Request) (*Response, error)
}

实现

我们先写一个server,打印出访问的payload信息。

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
		req, err := ioutil.ReadAll(r.Body)
		if err != nil {
			rw.WriteHeader(500)
			rw.Write([]byte(err.Error()))
			return
		}
		fmt.Println(string(req))
	})
	if err := http.ListenAndServe(":8000", mux); err != nil {
		panic(err)
	}
}

如果使用默认的DefaultClient,只会打印出我们传入的payload。

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"

	"github.com/google/uuid"
)

func main()  {
	id := uuid.NewString()
	req, _ := http.NewRequest("GET", "http://localhost:8000", strings.NewReader(fmt.Sprintf(`{"id":"%s"}`, id)))
	req.Header.Add("Authorization", fmt.Sprintf("Bearer token%s", id))
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	fmt.Println(resp)
}()

结果:

{"id":"912733ce-4e17-4209-ad9e-71159fd37845"}
&{200 OK 200 HTTP/1.1 1 1 map[Content-Length:[0] Date:[Sun, 28 Nov 2021 06:48:50 GMT]] {} 0 [] false false map[] 0xc000194000 <nil>}

使用自定义Transport

package main

import (
	"bytes"
	"encoding/json"
	"io/ioutil"
	"net/http"
	"strings"
)

type customTransport struct {
}

func (t *customTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	token := req.Header.Get("Authorization")
	if len(token) != 0 && strings.HasPrefix(token, "Bearer ") {
		token = token[7:]
		var bodyBytes []byte
		if req.Body != nil {
			bodyBytes, _ = ioutil.ReadAll(req.Body)
		}
		var payload map[string]interface{}
		if err := json.Unmarshal(bodyBytes, &payload); err != nil {
			return nil, err
		} else {
			payload["token"] = token
			if bodyBytes, err := json.Marshal(payload); err != nil {
				return nil, err
			} else {
				req.ContentLength = int64(len(bodyBytes))
				req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
			}
		}
	}
	resp, err := http.DefaultTransport.RoundTrip(req)
	if err != nil {
		return nil, err
	}
	return resp, nil
}

使用自定义Client

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"

	"github.com/google/uuid"
)

func main()  {
	id := uuid.NewString()
	req, _ := http.NewRequest("GET", "http://localhost:8000", strings.NewReader(fmt.Sprintf(`{"id":"%s"}`, id)))
	req.Header.Add("Authorization", fmt.Sprintf("Bearer token%s", id))
	client := &http.Client{
		Transport: &customTransport{},
	}
	resp, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	fmt.Println(resp)
}()

最终结果:

{"id":"ebcceb4b-1979-457b-bf49-9255ceb77322","token":"tokenebcceb4b-1979-457b-bf49-9255ceb77322"}
&{200 OK 200 HTTP/1.1 1 1 map[Content-Length:[0] Date:[Sun, 28 Nov 2021 06:49:25 GMT]] {} 0 [] false false map[] 0xc000140000 <nil>}

总结

我们可以使用http.DefaultClient完成大部分http请求,但是如果我们需要实现一些自定义逻辑时,可以传入http.Client中对应自定义的部分,实现自定义逻辑。

本文中通过修改Transport,读取请求Header,并修改请求Body,动态修改请求Payload。

 

第十六章:接口-编程思维

本篇翻译自《Practical Go Lessons》 Chapter 16: Interfaces 1 你将在本章学到什么? 什么是类型接口? 如何定义接口。 “实现一个接口”是什么意思? 接口的优点 2 涵盖的技术概念 接口 interface 具体实现 concrete implementation 实现一个接

Go 交叉编译 (跨平台编译)-编程思维

Golang 支持交叉编译,在一个平台上生成另一个平台的可执行程序 一、Windows 下编译 Linux 64位 和 Mac 可执行程序 SET CGO_ENABLED=0 SET GOOS=linux SET GOARCH=amd64 go build main.go SET CGO_ENABLED=0 SET

第十五章:指针类型-编程思维

本篇翻译自《Practical Go Lessons》 Chapter 15: Pointer type 1 你将在本章将学到什么? 什么是指针? 什么时指针类型? 如何去创建并使用一个指针类型的变量。 指正类型变量的零值是什么? 什么是解除引用? slices, maps, 和 channels 有什么特殊的地方?

第八章:变量、常量和基础类型-编程思维

本篇翻译自《Practical Go Lessons》 Chapter 8: Variables, constants and basic types 1 你将在本章中学到什么? 什么是变量?我们为什么需要它们? 什么是类型? 如何创建变量? 如何给变量赋值? 什么是常量?常量和变量有什么区别? 如何定义常量? 如何

node.js 递归复制文件夹(附带文件过滤功能)-编程思维

 1、简介:   很简单,写了一个node操作文件的小脚本,主要实现对目标文件夹中内容的复制。还顺带一个按照文件夹或者文件名过滤的功能。 2、应用场景   适合基于 node 环境的项目,项目打包的时候,配合 webpack 配置,生成需要的线上项目目录,方便快捷。 3、 使用说明   代码我检验过,应该是没bug的

手机端上传照片实现 压缩、拖放、缩放、裁剪、合成拼图等功能-编程思维

一、序   如题,最近工作中遇到一个移动端用户上传照片,然后在线编辑,添加一些别的图片合成的功能,类似于超级简化版美图秀秀。总结了一下,大致操作包含 上传图片,图片压缩、触摸拖动图片、放大/缩小、添加别的图片进行合成,最后生成一张新图片。功能比较多,问遍了度娘,也没什么系统的有用信息。蛋疼。。。于是挽起袖子自己撸代码。

编写一个接口压测工具-编程思维

前言 前段时间有个项目即将上线,需要对其中的核心接口进行压测;由于我们的接口是 gRPC 协议,找了一圈发现压测工具并不像 HTTP 那么多。 最终发现了 ghz 这个工具,功能也非常齐全。 事后我在想为啥做 gRPC 压测的工具这么少,是有什么难点嘛?为了验证这个问题于是我准备自己写一个工具。 特性 前前后后大概花

DNS CAA 记录及创建方法-编程思维

请访问原文链接:https://sysin.cn/blog/dns-caa/,查看最新版。原创作品,转载请保留出处。 作者:gc(at)sysin.org,主页:www.sysin.cn 关于 CAA 有一百多个被称为证书颁发机构的组织,可以颁发 SSL 证书来保证您的域的身份。如果您和大多数域所有者一样,您可能只从