当前注册流程还存在几个显著的问题需要优化:
- 接口参数没有校验,即使全部留空或随意填写也能入库成功。为此,用户名、密码、邮箱都应该必传,且需要有一定的安全验证。例如,密码应该在
6-12位之间,邮箱应该是xx@xx.xx格式; - 禁止注册相同的用户;
 - 密码不应该明文入库,应加密后入库;
 logic/Register函数参数过多,一来显示不优雅,二来不利于维护,应该将用户信息定义到一个结构体中,将其作为函数入参。
参数校检
GoFrame内置了强大的接口参数校检功能,只需要在g.Meta的tag上加上v即可启用。
api/users/v1/users.go
package v1  
  
import "github.com/gogf/gf/v2/frame/g"  
  
type RegisterReq struct {  
    g.Meta   `path:"users/register" method:"post"`  
    Username string `json:"username" v:"required|length:3,12"`  
    Password string `json:"password" v:"required|length:6,16"`  
    Email    string `json:"email" v:"required|email"`  
}  
  
type RegisterRes struct {  
}
多个验证规则使用|隔开,required表示此字段必填,length表示位数在3-12之间,email表示只接受合法的邮箱地址。所有可用的验证规则可在开发手册中查阅。
发起一个空用户名请求测试:
$ curl -X POST http://127.0.0.1:8000/v1/users/register -H "Content-Type: application/json" -d "{\"password\":\"123456\", \"email\":\"tyyn1022@gmail.com\"}"
{
    "code":51,
    "message":"The Username field is required",
    "data":null
}
The Username field is required提示我们用户名不能为空。
如果您对英文提示不满意,还可以使用框架提供的i18n组件改成中文提示。
参数校检i18n
从Github下载文件并且存放到manifest/i18n目录,直接从下文复制也行。
manifest/i18n/zh-CN/validation.toml
"gf.gvalid.rule.required"             = "{field}字段不能为空"
"gf.gvalid.rule.required-if"          = "{field}字段不能为空"
"gf.gvalid.rule.required-unless"      = "{field}字段不能为空"
"gf.gvalid.rule.required-with"        = "{field}字段不能为空"
"gf.gvalid.rule.required-with-all"    = "{field}字段不能为空"
"gf.gvalid.rule.required-without"     = "{field}字段不能为空"
"gf.gvalid.rule.required-without-all" = "{field}字段不能为空"
"gf.gvalid.rule.date"                 = "{field}字段值`{value}`日期格式不满足Y-m-d格式,例如: 2001-02-03"
"gf.gvalid.rule.datetime"             = "{field}字段值`{value}`日期格式不满足Y-m-d H:i:s格式,例如: 2001-02-03 12:00:00"
"gf.gvalid.rule.date-format"          = "{field}字段值`{value}`日期格式不满足{format}"
"gf.gvalid.rule.email"                = "{field}字段值`{value}`邮箱地址格式不正确"
"gf.gvalid.rule.phone"                = "{field}字段值`{value}`手机号码格式不正确"
"gf.gvalid.rule.phone-loose"          = "{field}字段值`{value}`手机号码格式不正确"
"gf.gvalid.rule.telephone"            = "{field}字段值`{value}`电话号码格式不正确"
"gf.gvalid.rule.passport"             = "{field}字段值`{value}`账号格式不合法,必需以字母开头,只能包含字母、数字和下划线,长度在6~18之间"
"gf.gvalid.rule.password"             = "{field}字段值`{value}`密码格式不合法,密码格式为任意6-18位的可见字符"
"gf.gvalid.rule.password2"            = "{field}字段值`{value}`密码格式不合法,密码格式为任意6-18位的可见字符,必须包含大小写字母和数字"
"gf.gvalid.rule.password3"            = "{field}字段值`{value}`密码格式不合法,密码格式为任意6-18位的可见字符,必须包含大小写字母、数字和特殊字符"
"gf.gvalid.rule.postcode"             = "{field}字段值`{value}`邮政编码不正确"
"gf.gvalid.rule.resident-id"          = "{field}字段值`{value}`身份证号码格式不正确"
"gf.gvalid.rule.bank-card"            = "{field}字段值`{value}`银行卡号格式不正确"
"gf.gvalid.rule.qq"                   = "{field}字段值`{value}`QQ号码格式不正确"
"gf.gvalid.rule.ip"                   = "{field}字段值`{value}`IP地址格式不正确"
"gf.gvalid.rule.ipv4"                 = "{field}字段值`{value}`IPv4地址格式不正确"
"gf.gvalid.rule.ipv6"                 = "{field}字段值`{value}`IPv6地址格式不正确"
"gf.gvalid.rule.mac"                  = "{field}字段值`{value}`MAC地址格式不正确"
"gf.gvalid.rule.url"                  = "{field}字段值`{value}`URL地址格式不正确"
"gf.gvalid.rule.domain"               = "{field}字段值`{value}`域名格式不正确"
"gf.gvalid.rule.length"               = "{field}字段值`{value}`字段长度应当为{min}到{max}个字符"
"gf.gvalid.rule.min-length"           = "{field}字段值`{value}`字段最小长度应当为{min}"
"gf.gvalid.rule.max-length"           = "{field}字段值`{value}`字段最大长度应当为{max}"
"gf.gvalid.rule.size"                 = "{field}字段值`{value}`字段长度必须应当为{size}"
"gf.gvalid.rule.between"              = "{field}字段值`{value}`字段大小应当为{min}到{max}"
"gf.gvalid.rule.min"                  = "{field}字段值`{value}`字段最小值应当为{min}"
"gf.gvalid.rule.max"                  = "{field}字段值`{value}`字段最大值应当为{max}"
"gf.gvalid.rule.json"                 = "{field}字段值`{value}`字段应当为JSON格式"
"gf.gvalid.rule.xml"                  = "{field}字段值`{value}`字段应当为XML格式"
"gf.gvalid.rule.array"                = "{field}字段值`{value}`字段应当为数组"
"gf.gvalid.rule.integer"              = "{field}字段值`{value}`字段应当为整数"
"gf.gvalid.rule.float"                = "{field}字段值`{value}`字段应当为浮点数"
"gf.gvalid.rule.boolean"              = "{field}字段值`{value}`字段应当为布尔值"
"gf.gvalid.rule.same"                 = "{field}字段值`{value}`字段值必须和{field}相同"
"gf.gvalid.rule.different"            = "{field}字段值`{value}`字段值不能与{field}相同"
"gf.gvalid.rule.in"                   = "{field}字段值`{value}`字段值应当满足取值范围:{pattern}"
"gf.gvalid.rule.not-in"               = "{field}字段值`{value}`字段值不应当满足取值范围:{pattern}"
"gf.gvalid.rule.regex"                = "{field}字段值`{value}`字段值不满足规则:{pattern}"
"gf.gvalid.rule.__default__"          = "{field}字段值`{value}`字段值不合法"
"CustomMessage"                       = "自定义错误"
"project id must between {min}, {max}"  = "项目ID必须大于等于{min}并且要小于等于{max}"
修改主函数,启用i18n:
main.go
package main  
  
···
func main() {  
    var err error  
  
    // 全局设置i18n  
    g.I18n().SetLanguage("zh-CN")  
  
    // 检查数据库是否能连接  
    err = connDb()  
    if err != nil {  
       panic(err)  
    }  
  
    cmd.Main.Run(gctx.GetInitCtx())  
}  
···
再次发起请求:
$ curl -X POST http://127.0.0.1:8000/v1/users/register -H "Content-Type: application/json" -d "{\"password\":\"123456\", \"email\":\"tyyn1022@gmail.com\"}"
{
    "code":51,
    "message":"Username字段不能为空",
    "data":null
}
可以看到message已经变成中文提示了。
禁止重复用户名
用户名是登录的重要依据,如果碰巧系统中有两个同名用户,则会出现重大的逻辑混乱。所以我们需要在数据入库前查询该用户是否存在,如果存在,则返回错误信息,提示用户已经存在。
internal/logic/users/users_register.go
package users  
  
...
  
func (u *Users) Register(ctx context.Context, username, password, email string) error {  
    if err := u.checkUser(ctx, username); err != nil {  
       return err  
    }  
  
    _, err := dao.Users.Ctx(ctx).Data(do.Users{  
       Username: username,  
       Password: password,  
       Email:    email,  
    }).Insert()  
    if err != nil {  
       return err  
    }  
    return nil  
}  
  
func (u *Users) checkUser(ctx context.Context, username string) error {  
    count, err := dao.Users.Ctx(ctx).Where("username", username).Count()  
    if err != nil {  
       return err  
    }  
    if count > 0 {  
       return gerror.New("用户已存在")  
    }  
    return nil  
}
发起请求测试结果:
$ curl -X POST http://127.0.0.1:8000/v1/users/register -H "Content-Type: application/json" -d "{\"username\":\"oldme\", \"password\":\"123456\", \"email\":\"tyyn1022@gmail.com\"}"
{
    "code":50,
    "message":"用户已存在",
    "data":null
}
只有代码检测还不够安全,我们在数据表中加上唯一索引,强制限制用户唯一。
ALTER TABLE users ADD UNIQUE (username);
密码加密
密码明文保存是一种非常不安全的行为,通常的做法是对其hash计算后存入数据库,例如md5、SHA-1等。
新增一个方法encryptPassword实现密码加密功能。
internal/logic/users/users.go
package users  
  
import "github.com/gogf/gf/v2/crypto/gmd5"  
  
...
func (u *Users) encryptPassword(password string) string {  
    return gmd5.MustEncryptString(password)  
}
gmd5组件帮助我们快速实现md5加密功能。编写注册逻辑代码,引入密码加密。
internal/logic/users/users_register.go
package users  
  
...
  
func (u *Users) Register(ctx context.Context, username, password, email string) error {  
    ...
  
    _, err := dao.Users.Ctx(ctx).Data(do.Users{  
       Username: username,  
       Password: u.encryptPassword(password),  
       Email:    email,  
    }).Insert()  
    if err != nil {  
       return err  
    }  
    return nil  
}  
...
删除原本的用户:
DELETE FROM users WHERE id = 1;
重新请求接口查看密码是否成功加密:
curl -X POST http://127.0.0.1:8000/v1/users/register -H "Content-Type: application/json" -d "{\"username\":\"oldme\", \"password\":\"123456\", \"email\":\"tyyn1022@gmail.com\"}"
结果:
| ID | Username | Password | Created_At | Updated_At | |
|---|---|---|---|---|---|
| 1 | oldme | e10adc3949ba59abbe56e057f20f883e | tyyn1022@gmail.com | 2024-11-08 10:36:48 | 2024-11-08 10:36:48 | 
Register 函数优化
自定义一个数据模型,用作Logic层的入参。
internal/logic/users/users_register.go
package users
...
type RegisterInput struct {  
    Username string  
    Password string  
    Email    string  
}
...
internal/logic/users/users_register.go
package users  
  
import (
    "star/internal/model"
    ...
)
func (u *Users) Register(ctx context.Context, in RegisterInput) error {  
    if err := u.checkUser(ctx, in.Username); err != nil {  
       return err  
    }  
  
    _, err := dao.Users.Ctx(ctx).Data(do.Users{  
       Username: in.Username,  
       Password: encryptPassword(in.Password),  
       Email:    in.Email,  
    }).Insert()  
    if err != nil {  
       return err  
    }  
    return nil  
}
...
更改Controller层,将RegisterInput传入。
internal/controller/users/users_v1_register.go
package users  
  
import (  
    "context"  
  
    "star/api/users/v1"
    "star/internal/logic/users"
)  
  
func (c *ControllerV1) Register(ctx context.Context, req *v1.RegisterReq) (res *v1.RegisterRes, err error) {  
    err = c.users.Register(ctx, users.RegisterInput{  
        Username: req.Username,  
        Password: req.Password,  
        Email:    req.Email,  
    })  
    return nil, err  
}