当前注册流程还存在几个显著的问题需要优化:
- 接口参数没有校验,即使全部留空或随意填写也能入库成功。为此,用户名、密码、邮箱都应该必传,且需要有一定的安全验证。例如,密码应该在
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/register.go
package users
...
func Register(ctx context.Context, username, password, email string) error {
if err := 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 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/utility.go
package users
import "github.com/gogf/gf/v2/crypto/gmd5"
func encryptPassword(password string) string {
return gmd5.MustEncryptString(password)
}
gmd5
组件帮助我们快速实现md5
加密功能。编写注册逻辑代码,引入密码加密。
internal/logic/users/register.go
package users
...
func Register(ctx context.Context, username, password, email string) error {
...
_, err := dao.Users.Ctx(ctx).Data(do.Users{
Username: username,
Password: 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 函数优化
在model
层自定义一个数据模型,用作Logic
层的入参。
internal/model/users.go
package model
type UserInput struct {
Username string
Password string
Email string
}
internal/logic/users/register.go
package users
import (
"star/internal/model"
...
)
func Register(ctx context.Context, in *model.UserInput) error {
if err := 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
层,将UserInput
传入。
internal/controller/users/users_v1_register.go
package users
import (
"star/internal/model"
...
)
func (c *ControllerV1) Register(ctx context.Context, req *v1.RegisterReq) (res *v1.RegisterRes, err error) {
err = users.Register(ctx, &model.UserInput{
Username: req.Username,
Password: req.Password,
Email: req.Email,
})
return nil, err
}