美文网首页go
【Go Web开发】用户注册

【Go Web开发】用户注册

作者: Go语言由浅入深 | 来源:发表于2022-03-09 23:49 被阅读0次

上一篇文章我们已经为用户添加打下了基础,现在开始使用它,创建一个新的API接口来管理新用户注册的过程。

Method URL模式 Handler 操作
POST /v1/users registerUserHandler 注册一个新用户

当用户调用POST /v1/users接口时,接口需要用户提供如下用户信息作为请求内容:

{
    "name": "Alice Smith", 
    "email": "alice@example.com", 
    "password": "pa55word"
}

当服务端收到这些请求内容时,registerUserHandler处理函数会创建一个User结构体来接收这些信息,并调用ValidateUser()帮助函数对各个字段进行校验,然后传给UserModel.Insert()方法写入数据库。

实际上,registerUserHandler处理函数大部分代码前面已经完成了,现在只需要将其整合起来即可。首先创建cmd/api/users.go文件:

$ touch cmd/api/users.go

添加如下代码到文件当中:

File: cmd/api/user.go


package main

import (
    "errors"
    "net/http"

    "greenlight.alexedwards.net/internal/data"
    "greenlight.alexedwards.net/internal/validator"
)

func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Request) {
    //创建匿名结构体接收客户端发送用户信息
    var input struct {
        Name     string `json:"name"`
        Email    string `json:"email"`
        Password string `json:"password"`
    }
    //解析请求内容到匿名结构体实例中
    err := app.readJSON(w, r, &input)
    if err != nil {
        app.badRequestResponse(w, r, err)
        return
    }
    //将input中的用户信息拷贝到User结构体。注意需要将激活信息设置为false,
    //该操作是非必需的因为默认值就是false,单独设置下可读性更好。
    user := &data.User{
        Name:      input.Name,
        Email:     input.Email,
        Activated: false,
    }
    //使用Password.Set方法处理密码
    err = user.Password.Set(input.Password)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }
    v := validator.New()
    //校验user结构体
    if data.ValidateUser(v, user); !v.Valid() {
        app.failedValidationResponse(w, r, v.Errors)
        return
    }
    //插入用户信息到数据库
    err = app.models.Users.Insert(user)
    if err != nil {
        switch {
        //如果错误是ErrDuplicateEmail,使用v.AddError()方法手动添加校验失败信息
        case errors.Is(err, data.ErrDuplicateEmail):
            v.AddError("email", "a user with this email address already exists")
            app.failedValidationResponse(w, r, v.Errors)
        default:
            app.serverErrorResponse(w, r, err)
        }
        return
    }
    //正常返回201
    err = app.writeJSON(w, http.StatusCreated, envelope{"user": user}, nil)
    if err != nil {
        app.serverErrorResponse(w, r, err)
    }
}

在测试接口之前,需要为注册用户接口添加路由:

File: cmd/api/routes.go


package main

import (
    "github.com/julienschmidt/httprouter"
    "net/http"
)

func (app *application) routes() http.Handler {
    router := httprouter.New()

    router.NotFound = http.HandlerFunc(app.notFoundResponse)
    router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)

    router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)

    router.HandlerFunc(http.MethodGet, "/v1/movies", app.listMoviesHandler)
    router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler)
    router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler)
    router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler)
    router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler)

    // 为/v1/users接口添加路由
    router.HandlerFunc(http.MethodPost, "/v1/users", app.registerUserHandler)

    return app.recoverPanic(app.rateLimit(router))
}

添加完路由后,确保所有到文件保存然后启动服务。向POST /v1/users接口发送请求,email地址为alice@example.com,你应该会看到201 Created返回用户信息:

$ BODY='{"name": "Alice Smith", "email": "alice@example.com", "password": "pa55word"}'
$ curl -i -d "$BODY" localhost:4000/v1/users
HTTP/1.1 201 Created
Content-Type: application/json
Date: Tue, 21 Dec 2021 15:05:44 GMT
Content-Length: 151

{
        "user": {
                "id": 1,
                "create_at": "2021-12-21T23:05:44+08:00",
                "name": "Alice Smith",
                "email": "alice@example.com",
                "activated": false
        }
}

提示:如果您按照上面的请求进行操作,请记住上面的请求中使用的密码——稍后需要使用到!

根据响应结果发现接口正常处理了请求。我们可以从返回状态码中看到,用户信息已经成功创建,在JSON响应中,我们可以看到系统为新用户生成的信息——包括用户的ID和激活状态。可以登录到PostgreSQL数据库中确认下,写入到用户信息:

 psql $GREENLIGHT_DB_DSN  
psql (13.4)
Type "help" for help.

greenlight=> select * from users;
 id |       create_at        |     name    |       email       |      password_hash     | activated | version 
----+------------------------+-------------+-------------------+-------------------------+-----------+---------
  1 | 2021-12-21 23:05:44+08 | Alice Smith | alice@example.com | \x243261243132243256... | f         |       1
(1 row)

注意:psql显示bytea值都是以16进制编码字符串。因此password_hash字段显示就是16进制编码到哈希值。可以执行select *, encode(password_hash, 'escape') from users;查询语句打印出password_hash实际字符串内容。

好了,下面我们尝试向API发出一个请求,带有无效的用户信息。我们的校验功能会生效,并返回错误响应:

$ BODY='{"name": "", "email": "bob@invalid.", "password": "pass"}'
$ curl -i -d "$BODY" localhost:4000/v1/users              
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
Date: Tue, 21 Dec 2021 15:21:51 GMT
Content-Length: 139

{
        "error": {
                "email": "must be a valid email address",
                "name": "must be provided",
                "password": "must be at least 8 bytes long"
        }
}

最后,尝试用alice@example.com注册第二个帐户。你应该会得到一个验证错误,其中包含一个“用户与此电子邮件地址已经存在”的消息,像这样:

$ BODY='{"name": "Alice Jones", "email": "alice@example.com", "password": "pa55word"}'
$ curl -i -d "$BODY" localhost:4000/v1/users
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
Date: Tue, 21 Dec 2021 15:26:06 GMT
Content-Length: 78

{
        "error": {
                "email": "a user with this email address already exists"
        }
}

如果你愿意,可以尝试使用大小写不同的alice@example.com发送一些请求,比如ALICE@example.comAlice@Example.com。因为数据库中的电子邮件列是citext类型,所以这些替代版本也会被识别为重复邮箱地址。

附加内容

Email大小写敏感

我们快速详细地讨论一下邮件地址的大小写敏感性:

  • 根据RRC 2821,电子邮件地址(username@domain)的域名部分不区分大小写。这意味着我们可以很确信的说在实际使用中alice@example.comalice@EXAMPLE.COM是同一个人的邮箱。
  • 邮箱地址的用户名部分可能是大小写敏感的,这需要根据不同的邮箱服务提供商来确定。几乎所有的主流邮箱服务提供商都对用户名也不区分大小写,但不是绝对的。我们只能说实际使用中alice@example.com很可能和ALICE@example.com是同一个邮箱地址。

因此这对于我们应用来说意味着什么呢?

从安全角度来看,我们应该始终使用用户在注册时提供的确切格式来存储邮件地址,并且我们应该只使用确切的格式向他们发送电子邮件。如果不这样做,邮件有可能被发送给错误的用户。尤其是在使用邮件进行身份验证时(如密码重置过程)中,特别需要注意这一点。

因此,在对比alice@example.comALICE@example.com时有可能是同一个邮箱地址,因此在应用开发时需要不区分大小写对比。

在用户注册流程中,通过不区分大小写对比邮件地址,防止同一个邮箱注册多个账号。从用户体验角度来看,在登录、激活或密码重置等流程中,如果不要求用户提交请求时使用与注册时完全相同的电子邮件格式,用户就会更容易接受。

用户枚举

需要注意的是用户注册接口很容易受到用户枚举攻击。例如,如果攻击者想知道alice@example.com是否在我们这里有账户,他们会发送这样的请求:

$ BODY='{"name": "Alice Jones", "email": "alice@example.com", "password": "pa55word"}'
$ curl -d "$BODY" localhost:4000/v1/users                                       
{
        "error": {
                "email": "a user with this email address already exists"
        }
}

问题就在这里。响应明确地告诉攻击者alice@example.com已经是一个用户。因此泄漏这些信息有什么风险呢?

第一个也是最明显的风险,与用户隐私有关。对于敏感或机密的服务,您可能不希望显示谁有帐户。第二个风险是,它使攻击者更容易侵入用户的帐户。一旦他们知道用户的电子邮件地址,他们可以:

  • 通过社会工程或其他类型的定制攻击来锁定用户。
  • 在泄露的密码库中搜索电子邮件地址,并在我们的服务中尝试使用相同的密码。

防止枚举攻击通常需要两个条件:

1、确保发送给客户端的响应总是相同的,不管用户是否存在。一般来说,这意味着需要将响应措辞修改得模棱两可,并在其他渠道通知用户任何问题(例如发送电子邮件通知用户已经有一个帐户)。

2、确保发送响应所花费的时间总是相同的,不管用户是否存在。在Go中,这通常意味着将工作转移到一个后台程序。

不幸的是,这些缓解措施往往会增加应用程序的复杂性。对于那些不是攻击者的普通用户来说,从用户体验的角度来看,是不好的。你不得不问:这样的代价值得吗?

在回答这个问题时,有一些事情需要考虑。用户隐私在应用程序中有多重要?对于攻击者来说,一个被攻破的账户有多大的吸引力(高价值)?减少用户使用中的不便有多重要?这些问题的答案因项目的不同而不同,这将有助于形成您的决策因素。

值得注意的是,许多常用的服务,包括Twitter、GitHub和Amazon,都没有防止用户枚举(至少在他们的注册页面上没有)。我并不是说这样做就可以了——只是这些公司已经决定,在他们的特定情况下,对用户来说额外的冲突比隐私和安全风险更糟糕。

相关文章

网友评论

    本文标题:【Go Web开发】用户注册

    本文链接:https://www.haomeiwen.com/subject/pegxdrtx.html