docker version:1.0.0

位置:login命令的源码位于/docker/api/client/command.go中

使用方式:docker login [-e|-email=""] [-p|--password=""] [-u|--username=""] [SERVER]

功能:用于登录registry

eg. docker login 127.0.0.1:5000

=================

以下是在client端执行login的过程,通过api的CmdLogin接口,组建POST /auth,将此请求发送给docker deamon的http server

http server将此请求路由到postAuth(api/server/server.go),创建一个名为auth的job。




================================

------------------------dcoker client组建POST auth请求----------------------------

func (cli *DockerCli) CmdLogin(args ...string) error {
cmd := cli.Subcmd("login", "[SERVER]", "Register or log in to a Docker registry server, if no server is specified \""+registry.IndexServerAddress()+"\" is the default.")

var username, password, email string

cmd.StringVar(&username, []string{"u", "-username"}, "", "Username")
cmd.StringVar(&password, []string{"p", "-password"}, "", "Password")
cmd.StringVar(&email, []string{"e", "-email"}, "", "Email")
 //分析输入的参数

err := cmd.Parse(args)
if err != nil {
return nil
}

//如果命令行中没有registry地址,那么获取默认的registry index server的地址:const INDEXSERVER = "https://index.docker.io/v1/"

//如果有将使用命令行中的registry地址
serverAddress := registry.IndexServerAddress()
if len(cmd.Args()) > 0 {
serverAddress = cmd.Arg(0) 
}

//提示函数
promptDefault := func(prompt string, configDefault string) {
if configDefault == "" {
fmt.Fprintf(cli.out, "%s: ", prompt)
} else {
fmt.Fprintf(cli.out, "%s (%s): ", prompt, configDefault)
}
}

// 读取输入
readInput := func(in io.Reader, out io.Writer) string {
reader := bufio.NewReader(in)
line, _, err := reader.ReadLine()
if err != nil {
fmt.Fprintln(out, err.Error())
os.Exit(1)
}
return string(line)
}

//获取配置文件
cli.LoadConfigFile()   //获取“HOME”目录下的registry授权信息文件.dockercfg      const CONFIGFILE = ".dockercfg"

//在授权配置文件中没有找到registry项,新建此registry的配置项
authconfig, ok := cli.configFile.Configs[serverAddress]
if !ok {
authconfig = registry.AuthConfig{}
}

//填充username
if username == "" {
promptDefault("Username", authconfig.Username)
username = readInput(cli.in, cli.out)
if username == "" {
username = authconfig.Username
}
}

//填充passwd和email
if username != authconfig.Username {
if password == "" {
oldState, _ := term.SaveState(cli.terminalFd)
fmt.Fprintf(cli.out, "Password: ")
term.DisableEcho(cli.terminalFd, oldState)

password = readInput(cli.in, cli.out)
fmt.Fprint(cli.out, "\n")

term.RestoreTerminal(cli.terminalFd, oldState)
if password == "" {
return fmt.Errorf("Error : Password Required")
}
}

if email == "" {
promptDefault("Email", authconfig.Email)
email = readInput(cli.in, cli.out)
if email == "" {
email = authconfig.Email
}
}
} else {
password = authconfig.Password
email = authconfig.Email
}

//将新的授权信息添加授权结构体中
authconfig.Username = username
authconfig.Password = password
authconfig.Email = email
authconfig.ServerAddress = serverAddress
cli.configFile.Configs[serverAddress] = authconfig

//向registry发起POST /auth请求,在registry的index中进行授权方面的认证过程
stream, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[serverAddress], false)
if statusCode == 401 {
delete(cli.configFile.Configs, serverAddress)
registry.SaveConfig(cli.configFile)
return err
}
if err != nil {
return err
}
var out2 engine.Env
err = out2.Decode(stream)
if err != nil {
cli.configFile, _ = registry.LoadConfig(os.Getenv("HOME"))
return err
}

//请求成功,保存授权信息到.dockercfg中
registry.SaveConfig(cli.configFile)
if out2.Get("Status") != "" {
fmt.Fprintf(cli.out, "%s\n", out2.Get("Status"))
}
return nil
}

----------------------------------向docker deamon的http server发起POST /auth请求---------------------------------------------------------------

位置:cil.call位于/api/client/utils.go


stream, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[serverAddress], false)

func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo bool) (io.ReadCloser, int, error) {

//解析registry地址
params, err := cli.encodeData(data)
if err != nil {
return nil, -1, err
}

//新建方法为POST路径为/auth的http请求
req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", api.APIVERSION, path), params)
if err != nil {
return nil, -1, err
}

//若registry地址存在,获取index地址信息
if passAuthInfo {
cli.LoadConfigFile()
// Resolve the Auth config relevant for this server
authConfig := cli.configFile.ResolveAuthConfig(registry.IndexServerAddress()) //获取index地址信息 const INDEXSERVER = "https://index.docker.io/v1/"

//组装http请求的头部
getHeaders := func(authConfig registry.AuthConfig) (map[string][]string, error) {
buf, err := json.Marshal(authConfig)
if err != nil {
return nil, err
}
registryAuthHeader := []string{
base64.URLEncoding.EncodeToString(buf),
}
return map[string][]string{"X-Registry-Auth": registryAuthHeader}, nil
}


if headers, err := getHeaders(authConfig); err == nil && headers != nil {
for k, v := range headers {
req.Header[k] = v
}
}
}
req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION)
req.URL.Host = cli.addr
req.URL.Scheme = cli.scheme
if data != nil {
req.Header.Set("Content-Type", "application/json")
} else if method == "POST" {
req.Header.Set("Content-Type", "plain/text")
}

//发起POST /auth请求, 在真正发起请求钱,做了个ping的动作
resp, err := cli.HTTPClient().Do(req)
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return nil, -1, ErrConnectionRefused
}
return nil, -1, err
}
//请求返回错误码处理
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, -1, err
}
if len(body) == 0 {
return nil, resp.StatusCode, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(resp.StatusCode), req.URL)
}
return nil, resp.StatusCode, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body))
}
//返回index服务响应的包体和状态码
return resp.Body, resp.StatusCode, nil
}

------------------------------------docker deamon的http server处理client端的POST auth请求---------------------------

代码位置 api/server/server.go

func postAuth(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {

//获取认证配置,新建名为auth的job
var (
authConfig, err = ioutil.ReadAll(r.Body)
job = eng.Job("auth")
stdoutBuffer = bytes.NewBuffer(nil)
)
if err != nil {
return err
}
job.Setenv("authConfig", string(authConfig))
job.Stdout.Add(stdoutBuffer)

//运行auth job
if err = job.Run(); err != nil {
return err
}
if status := engine.Tail(stdoutBuffer, 1); status != "" {
var env engine.Env
env.Set("Status", status)
return writeJSON(w, http.StatusOK, env)
}
w.WriteHeader(http.StatusNoContent)
return nil
}

--------------------------------------auth job调用registry包中的auth.go中的Login方法向registry发起用户认证-------------------------

registry包中向引擎eng注册的handler,使用s.Auth处理。

//代码位置registry/service.go

// Install installs registry capabilities to eng.
func (s *Service) Install(eng *engine.Engine) error {
eng.Register("auth", s.Auth)
eng.Register("search", s.Search)
return nil
}

func (s *Service) Auth(job *engine.Job) engine.Status {
var (
err error
authConfig = &AuthConfig{}
)

job.GetenvJson("authConfig", authConfig)
// TODO: this is only done here because auth and registry need to be merged into one pkg
if addr := authConfig.ServerAddress; addr != "" && addr != IndexServerAddress() {
addr, err = ExpandAndVerifyRegistryUrl(addr)
if err != nil {
return job.Error(err)
}
authConfig.ServerAddress = addr
}
//使用registry/auth.go中的Login处理

status, err := Login(authConfig, HTTPRequestFactory(nil))
if err != nil {
return job.Error(err)
}
job.Printf("%s\n", status)
return engine.StatusOK
}

----------------------registry/auth.go---------------------

docker客户端向docker-registry发起认证请求使用POST方法 /v1/users路由,

docker-registry收到请求后,进行注册,若注册成功返回给docker客户端201,并提示激活用户

若返回400,则用户已经注册


// try to register/login to the registry server
func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, error) {
var (
status string
reqBody []byte
err error
client = &http.Client{
Transport: &http.Transport{
DisableKeepAlives: true,
Proxy: http.ProxyFromEnvironment,
},
CheckRedirect: AddRequiredHeadersToRedirectedRequests,
}
reqStatusCode = 0
serverAddress = authConfig.ServerAddress //使用命令中提供的registry地址
)

if serverAddress == "" {
serverAddress = IndexServerAddress() //如果命令行中没有提供registry地址,使用官方的registry地址
}

loginAgainstOfficialIndex := serverAddress == IndexServerAddress()

// to avoid sending the server address to the server it should be removed before being marshalled
authCopy := *authConfig
authCopy.ServerAddress = ""

jsonBody, err := json.Marshal(authCopy)
if err != nil {
return "", fmt.Errorf("Config Error: %s", err)
}

// using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status.
b := strings.NewReader(string(jsonBody))
req1, err := http.Post(serverAddress+"users/", "application/json; charset=utf-8", b)//向registry发起POST方法,路由为/v1/users的请求
if err != nil {
return "", fmt.Errorf("Server Error: %s", err)
}
reqStatusCode = req1.StatusCode
defer req1.Body.Close()
reqBody, err = ioutil.ReadAll(req1.Body)
if err != nil {
return "", fmt.Errorf("Server Error: [%#v] %s", reqStatusCode, err)
}
//下面为分析registry返回的状态码的情况
if reqStatusCode == 201 {//收到201状态码,代码用户注册成功,提示用户激活,退出,不做登录操作
if loginAgainstOfficialIndex { //如果是docker官方的registry,提示信息
status = "Account created. Please use the confirmation link we sent" +
" to your e-mail to activate it."
} else { //如果是私有的registry,提示信息
status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it."
}
} else if reqStatusCode == 400 { //收到400,代表用户已存在,发起GET方法,路由为/v1/users的请求,进行用户登录(这个也许是docker官方的registry的状态码,私有的返回401)
if string(reqBody) == "\"Username or email already exists\"" {
req, err := factory.NewRequest("GET", serverAddress+"users/", nil)
req.SetBasicAuth(authConfig.Username, authConfig.Password)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}

//发起登录请求后,若收到200,代表用户登录成功
if resp.StatusCode == 200 {
status = "Login Succeeded"
} else if resp.StatusCode == 401 { //发起登录请求后,收到401,代表用户名或密码错误
return "", fmt.Errorf("Wrong login/password, please try again")
} else if resp.StatusCode == 403 { //发起登录请求后,收到403,代表账户没有激活
if loginAgainstOfficialIndex { //如果是docker官方的registry,提示信息
return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.")
}
return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)
} else { //收到其他状态码的提示信息
return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header)
}
} else { //注册时收到其他状态信息时
return "", fmt.Errorf("Registration: %s", reqBody)
}
} else if reqStatusCode == 401 {//注册时收到401,代表登录私有registry的用户登录
// This case would happen with private registries where /v1/users is
// protected, so people can use `docker login` as an auth check.
req, err := factory.NewRequest("GET", serverAddress+"users/", nil)


req.SetBasicAuth(authConfig.Username, authConfig.Password)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode == 200 { //登录私有registry,若收到200,代表登录成功
status = "Login Succeeded"
} else if resp.StatusCode == 401 {//登录私有registry,若收到401,代表用户名或密码错误
return "", fmt.Errorf("Wrong login/password, please try again")
} else {
return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
resp.StatusCode, resp.Header)
}
} else {
return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody)
}
return status, nil
}

registry使用standalone方式,从docker client访问registry协议流程图


1.client向registry发起ping操作,使用GET方法,协议版本为V1,试探是否能够和registry通信

包体内容为

2.registry向client返回200 OK,代码已经收到client的ping操作请求

包体为

3.client将登录的用户信息交给registry端,适应POST方法代码要求registry创建此用户,若此用户存在,则返回401

4.registry返回401错误码,代表此用户已经存在

5.client收到401,知道了registry端已有此用户,client使用GET方法,携带basic authorization验证用户信息

6、registry给client返回200 OK,代表此用户通过验证

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐