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 — самый быстрый способ определить процесс