不知道有多少人遇到我这个问题。当Docker的Registry服务器返回301、302 Redirect时,特定条件下,Docker客户端跟随返回的新地址时,认证信息会失效。导致后续请求都被视作匿名的未授权请求处理。

这是什么原因呢?这个特定条件又是指什么呢?

原来Docker客户端在做HTTP请求时,使用的是golang的默认http客户端。在golang的http客户端文档中,明确指出,golang在跟随重定向响应的新地址时,针对像AuthorizationWWW-AuthenticateCookie这类HTTP Header,会有一个域安全性校验。如果新地址的域名和老地址不一样,并且不是老地址的子域名时,跟随请求会丢掉这些HTTP头,以保障请求安全性。

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.

这里提到了域和子域。那么针对域名满足条件,但是端口不满足条件的情况(比如从foo.com重定向到foo.com:8080),Go怎么处理的呢?我们跟进Go的源代码看看。相关代码位于net/http/client.go这个文件中,从Client.Do()开始,依次调用copyHeaders() => makeHeadersCopier() => shouldCopyHeaderOnRedirect(),我们来看看最后这个函数的源代码:

func shouldCopyHeaderOnRedirect(headerKey string, initial, dest *url.URL) bool {
  	switch CanonicalHeaderKey(headerKey) {
  	case "Authorization", "Www-Authenticate", "Cookie", "Cookie2":
  		// Permit sending auth/cookie headers from "foo.com"
  		// to "sub.foo.com".
  
  		// Note that we don't send all cookies to subdomains
  		// automatically. This function is only used for
  		// Cookies set explicitly on the initial outgoing
  		// client request. Cookies automatically added via the
  		// CookieJar mechanism continue to follow each
  		// cookie's scope as set by Set-Cookie. But for
  		// outgoing requests with the Cookie header set
  		// directly, we don't know their scope, so we assume
  		// it's for *.domain.com.
  
  		// TODO(bradfitz): once issue 16142 is fixed, make
  		// this code use those URL accessors, and consider
  		// "http://foo.com" and "http://foo.com:80" as
  		// equivalent?
  
  		// TODO(bradfitz): better hostname canonicalization,
  		// at least once we figure out IDNA/Punycode (issue
  		// 13835).
  		ihost := strings.ToLower(initial.Host)
  		dhost := strings.ToLower(dest.Host)
  		return isDomainOrSubdomain(dhost, ihost)
  	}
  	// All other headers are copied:
  	return true
  }
  
  // isDomainOrSubdomain reports whether sub is a subdomain (or exact
  // match) of the parent domain.
  //
  // Both domains must already be in canonical form.
  func isDomainOrSubdomain(sub, parent string) bool {
  	if sub == parent {
  		return true
  	}
  	// If sub is "foo.example.com" and parent is "example.com",
  	// that means sub must end in "."+parent.
  	// Do it without allocating.
  	if !strings.HasSuffix(sub, parent) {
  		return false
  	}
  	return sub[len(sub)-len(parent)-1] == '.'
  }

从代码里可以看出,判断重定向时特殊的Header是否复制上,直接是通过Host字段字符串匹配进行的。而且代码中的TODO说的很清楚,针对带端口的请求,这里暂时没作处理。等#16142这个issue修复后再做处理。但是这里的处理,应该仅仅是针对默认的80端口,将带80端口和不带80端口的域视作同一个域。而其他端口则不作处理。

也就是说,如果你原始请求是test.com,新请求是test.com:8080,那么这个时候是不会作为同一个域处理,Header会丢弃。

如果原始请求是test.com,新请求是test.com:80。现阶段是会作为不同域处理,后续等issue修复后,这里会修复,修复后作为同域处理。

这里回到最开始我们说的Docker Registry的问题。如果你Registry地址返回的是301或者302重定向,而且重定向的新地址和Registry的地址不是同域名、同端口的话,那么Docker客户端在跟随新地址时,就会丢弃认证用的HTTP头,导致最终请求被视作匿名用户。

针对这个问题,我已经在Docker上提了Issue #865,不知Docker何时以何种方式处理。