1. Причина
анализироватьhttp.Client
Причина реализации исходного кода заключается в том, что при использовании следующих шагов для имитации входа на веб-сайт возникает проблема, см.Зная - go net/http.Client обрабатывает перенаправление:
-
POST
пароль от учетной записи и другие параметры для входа -
Изданный
token
, этоtoken
пройти черезcookie
Изданный -
Перенаправить на домашнюю страницу
/
проходя черезhttp.Post
Сделайте запрос, не ждите перенаправления, можете получить его напрямуюcookie
значение, но на самом деле go обрабатывает перенаправление для нас, потерянноеcookie
ценность
Проанализировав исходный код, эту проблему можно легко решить:
// 请求http.calabash.top将被301重定向到https
myClient := http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
respWithNoRedirect, _ := myClient.Get("http://blog.calabash.top")
respWithRedirect, _ := http.Get("http://blog.calabash.top")
fmt.Println(respWithNoRedirect.StatusCode) // 301
fmt.Println(respWithRedirect.StatusCode) // 200
2. Client
HTTP-клиент, нулевое значение которого равноDefaultClient
, ядро анализа здесь лежит в способе обработки перенаправления
type Client struct {
// 分析范围之外
Transport RoundTripper
// 重定向策略
CheckRedirect func(req *Request, via []*Request) error
// Cookie的存储, 单纯的get/set方法
Jar CookieJar
// 超时
Timeout time.Duration
}
var DefaultClient = &Client{}
При обнаружении перенаправления, за исключением следующих случаев,Client
Все начальные заголовки запроса будут перенаправлены:
-
Когда адрес перенаправления и начальный адрес
domain
разные и неsub domain
, в заголовке запросаcookie
,authorization
и другие чувствительные поля будут игнорироваться -
перенаправления могут изменить
cookie
значение, так впередcookie
Заголовки запросов будут игнорироваться при любых измененияхcookie
2.1 Правила переадресации:redirectBehavior
func redirectBehavior(reqMethod string, resp *Response, ireq *Request) (redirectMethod string, shouldRedirect, includeBody bool) {
switch resp.StatusCode {
case 301, 302, 303:
redirectMethod = reqMethod
shouldRedirect = true
includeBody = false
if reqMethod != "GET" && reqMethod != "HEAD" {
redirectMethod = "GET"
}
case 307, 308:
redirectMethod = reqMethod
shouldRedirect = true
includeBody = true
if resp.Header.Get("Location") == "" {
shouldRedirect = false
break
}
if ireq.GetBody == nil && ireq.outgoingLength() != 0 {
shouldRedirect = false
}
}
return redirectMethod, shouldRedirect, includeBody
}
Из исходного кода можно получить следующую информацию:
-
Для кодов статуса 301, 302, 303 перенаправление не идет с телом запроса, а для не-
GET/HEAD
Метод запроса будет вынужден измениться наGET
Метод запроса -
Для кода состояния 307, 308 перенаправление не меняет метод запроса на изменение и повторно использует тело запроса.
Location
пустой илиBody
Если пусто, не следует перенаправлять
Ссылаться наMDN - Http Status Code, который имеет описание
301: Хотя стандарт требует, чтобы браузер не модифицировал метод и тело http при получении ответа и перенаправлении, у некоторых браузеров могут возникнуть проблемы. Поэтому лучше использовать 301 при работе с методами GET или HEAD и использовать 308 вместо 301 в других случаях.
302: даже если регуляторные требования к обеспечению того, чтобы метод запроса браузера и запрашивать тот же предмет во время перенаправления, но не все пользовательские агенты последуют этому Рекомендуется использовать только код состояния 302 в ответ на методы получения или главы, другие альтернативы с использованием 307 301
303: Обычно в качестве возвращаемого результата операции PUT или POST указывает, что ссылка перенаправления указывает не на вновь загруженный ресурс, а на другую страницу. Метод запроса перенаправленной страницы всегда должен использовать GET
307: Единственное различие между кодами состояния 307 и 302 заключается в том, что при отправке запроса на перенаправление код состояния 307 гарантирует, что метод запроса и тело сообщения не изменятся.
308: В процессе перенаправления метод запроса и тело сообщения не меняются.Однако, когда возвращается код состояния 301, метод запроса иногда неправильно изменяется клиентом на метод GET.
в состоянии обнаружитьMDN
описание иredirectBehavior
Дизайн исходного кода очень последователен
2.2 Стратегия проверки для перенаправления:checkRedirect
когда не указаноCheckRedirect
функция,Client
Будет использоватьсяdefaultCheckRedirect
Стратегия:По умолчанию до десяти перенаправлений
func (c *Client) checkRedirect(req *Request, via []*Request) error {
fn := c.CheckRedirect
if fn == nil {
fn = defaultCheckRedirect
}
return fn(req, via)
}
func defaultCheckRedirect(req *Request, via []*Request) error {
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
}
2.3 Обработка заголовков запроса на перенаправление:makeHeadersCopier
Этот метод проходитЗакрытиеспособ завершения обработки упомянутого выше заголовка запроса:
-
Когда адрес перенаправления и исходный адрес имеют разные доменные имена и не являются поддоменами, заголовок запроса
cookie
,authorization
и другие чувствительные поля будут игнорироваться -
перенаправления могут изменить
cookie
значение, так впередcookie
Заголовки запросов будут игнорироваться при любых измененияхcookie
func (c *Client) makeHeadersCopier(ireq *Request) func(*Request) {
// 克隆一份header
var (
ireqhdr = ireq.Header.Clone()
icookies map[string][]*Cookie
)
// 再维护一个cookie哈希表
if c.Jar != nil && ireq.Header.Get("Cookie") != "" {
icookies = make(map[string][]*Cookie)
for _, c := range ireq.Cookies() {
icookies[c.Name] = append(icookies[c.Name], c)
}
}
// The previous request
preq := ireq
// 调用返回一个接受req的函数, 用于对拷贝header进行处理
return func(req *Request) {
if c.Jar != nil && icookies != nil {
var changed bool
resp := req.Response
// 如果响应中的set-cookie操作设定的cookie名称存在于cookie哈希表中, 从哈希表中删除它
for _, c := range resp.Cookies() {
if _, ok := icookies[c.Name]; ok {
delete(icookies, c.Name)
changed = true
}
}
// 忽略所有变化的cookie, 重新组装cookie请求头字段
if changed {
ireqhdr.Del("Cookie")
var ss []string
for _, cs := range icookies {
for _, c := range cs {
ss = append(ss, c.Name+"="+c.Value)
}
}
sort.Strings(ss) // Ensure deterministic headers
ireqhdr.Set("Cookie", strings.Join(ss, "; "))
}
}
for k, vv := range ireqhdr {
// 对于非同域或子域, 敏感请求头的处理
if shouldCopyHeaderOnRedirect(k, preq.URL, req.URL) {
req.Header[k] = vv
}
}
// Update previous Request with the current request
preq = req
}
}
Кстати, для разных доменов или поддоменов обработка конфиденциальных заголовков запроса относительно проста и понятна.
func shouldCopyHeaderOnRedirect(headerKey string, initial, dest *url.URL) bool {
switch CanonicalHeaderKey(headerKey) {
case "Authorization", "Www-Authenticate", "Cookie", "Cookie2":
ihost := canonicalAddr(initial)
dhost := canonicalAddr(dest)
return isDomainOrSubdomain(dhost, ihost)
}
// All other headers are copied:
return true
}
func isDomainOrSubdomain(sub, parent string) bool {
if sub == parent {
return true
}
if !strings.HasSuffix(sub, parent) {
return false
}
return sub[len(sub)-len(parent)-1] == '.'
}
2.4 http.Get
за методом
можно найти в исходном коде,http.Get
и т.д. методыDefaultClient
слой после упаковки
var DefaultClient = &Client{}
func Get(url string) (resp *Response, err error) {
return DefaultClient.Get(url)
}
func Post(url, contentType string, body io.Reader) (resp *Response, err error) {
return DefaultClient.Post(url, contentType, body)
}
func PostForm(url string, data url.Values) (resp *Response, err error) {
return DefaultClient.PostForm(url, data)
}
func Head(url string) (resp *Response, err error) {
return DefaultClient.Head(url)
}
И эти методы в конечном итоге называютсяClient.Do
Способ, эту часть можно сказать очень аппетитный
func (c *Client) Get(url string) (resp *Response, err error) {
req, err := NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {
req, err := NewRequest("POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
return c.Do(req)
}
func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error) {
return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
}
func (c *Client) Head(url string) (resp *Response, err error) {
req, err := NewRequest("HEAD", url, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
2.5 Client.Do
метод
func (c *Client) Do(req *Request) (*Response, error) {
return c.do(req)
}
начать анализClient.do
метод, этот метод имеет около 200 строк, чтобы избежать влияния на анализ процесса, часть кода обработки ошибок будет удалена, а упрощенный код выглядит следующим образом.
func (c *Client) do(req *Request) (retres *Response, reterr error) {
var (
deadline = c.deadline()
reqs []*Request
resp *Response
copyHeaders = c.makeHeadersCopier(req)
reqBodyClosed = false
redirectMethod string
includeBody bool
)
for {
if len(reqs) > 0 {
loc := resp.Header.Get("Location")
ireq := reqs[0]
req = &Request{
Method: redirectMethod,
Response: resp,
URL: u,
Header: make(Header),
Host: host,
Cancel: ireq.Cancel,
ctx: ireq.ctx,
}
if includeBody && ireq.GetBody != nil {
req.Body, err = ireq.GetBody()
req.ContentLength = ireq.ContentLength
}
copyHeaders(req)
err = c.checkRedirect(req, reqs)
if err == ErrUseLastResponse {
return resp, nil
}
resp.Body.Close()
}
reqs = append(reqs, req)
var err error
var didTimeout func() bool
resp, didTimeout, err = c.send(req, deadline)
var shouldRedirect bool
redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
if !shouldRedirect {
return resp, nil
}
req.closeBody()
}
}
-
копировать
req
все вheaders
, возвращает функциюcopyHeaders
, при перенаправлении некоторые поля заголовка запроса обрабатываются по правилам -
reqs = append(reqs, req)
, зарегистрируйте запрос наreqs
в массиве первыйreq
Должен быть самый оригинальный запрос, последнийreq
Должен быть перенаправлен -
resp = c.send(req)
, Серьезно инициируйте запрос, получитеresp
-
redirectBehavior(req.Method, resp, reqs[0])
, по ответу определить требуется ли перенаправление, если нет, то процесс завершается, при необходимости продолжить вниз -
Входить
if len(reqs) > 0
ветка, начать обработку редиректа -
от
resp
получено вLocation
поля в сочетании с исходным запросомreqs[0]
Соберите новый запрос на перенаправление и назначьте егоreq
-
copyHeaders(req)
, Сравнениеreqs[0]
иreq
, удалить определенные поля в соответствии с двумя правилами, упомянутыми выше -
c.checkRedirect(req, reqs)
Определить, соблюдается ли политика перенаправления, если нет, вернуться к последнему ответу и не продолжать перенаправление -
Выполните шаги 1-4 еще раз
3. Резюме
Несмотря на то, что это интерфейс, я впервые читаю исходный код Go, опыт все равно очень классный, 800 строк кода, 400 строк комментариев и не очень большой объем QAQ.
Обобщить опыт:
-
Посмотрите исходник с вопросом, легко определить направление
-
Проанализируйте основную ветку, удалите/пропустите некоторые боковые ветки и код обработки ошибок.
-
DEBUG — самый быстрый способ определить процесс