Skip to main content
Version: 2.8.x(Latest)

There are several significant issues that need to be optimized in the current registration process:

  • Interface parameters are not validated, meaning entries with empty or random values can be successfully stored. Therefore, username, password, and email should be required, with a certain level of security verification. For example, passwords should be between 6-12 characters, and emails should follow the xx@xx.xx format;
  • Prohibiting the registration of identical users;
  • Passwords should not be stored in plain text but should be encrypted before storage;
  • The logic/Register function has too many parameters, which is neither elegant nor maintainable. User information should be defined in a structure and used as a function parameter.

Parameter Validation


GoFrame has built-in powerful interface parameter validation features, which can be enabled by adding v to the g.Meta tag.

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 {
}

Multiple validation rules are separated by |, required indicates the field is mandatory, length indicates the length is between 3-12, and email indicates only valid email addresses are accepted. Available validation rules can be found in the Developer's Manual.

Testing with an empty username request:

$ 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 indicates that the username cannot be empty.

If you are not satisfied with the English prompt, you can use the i18n component provided by the framework to change it to a Chinese prompt.

Parameter Validation i18n

Download the file from Github and store it in the manifest/i18n directory, or simply copy it from below.

manifest/i18n/zh-CN/validation.toml

"gf.gvalid.rule.required"             = "{field} field cannot be empty"
"gf.gvalid.rule.required-if" = "{field} field cannot be empty"
"gf.gvalid.rule.required-unless" = "{field} field cannot be empty"
"gf.gvalid.rule.required-with" = "{field} field cannot be empty"
"gf.gvalid.rule.required-with-all" = "{field} field cannot be empty"
"gf.gvalid.rule.required-without" = "{field} field cannot be empty"
"gf.gvalid.rule.required-without-all" = "{field} field cannot be empty"
"gf.gvalid.rule.date" = "{field} field value `{value}` does not match date format Y-m-d, e.g.: 2001-02-03"
"gf.gvalid.rule.datetime" = "{field} field value `{value}` does not match datetime format Y-m-d H:i:s, e.g.: 2001-02-03 12:00:00"
"gf.gvalid.rule.date-format" = "{field} field value `{value}` does not match the specified format {format}"
"gf.gvalid.rule.email" = "{field} field value `{value}` is not a valid email address format"
"gf.gvalid.rule.phone" = "{field} field value `{value}` is not a valid phone number format"
"gf.gvalid.rule.phone-loose" = "{field} field value `{value}` is not a valid phone number format"
"gf.gvalid.rule.telephone" = "{field} field value `{value}` is not a valid telephone number format"
"gf.gvalid.rule.passport" = "{field} field value `{value}` is not a valid account format, must start with a letter and can only contain letters, numbers, and underscores, with a length of 6~18"
"gf.gvalid.rule.password" = "{field} field value `{value}` is not a valid password format, must be any visible character of length 6-18"
"gf.gvalid.rule.password2" = "{field} field value `{value}` is not a valid password format, must be any visible character of length 6-18, containing uppercase and lowercase letters and numbers"
"gf.gvalid.rule.password3" = "{field} field value `{value}` is not a valid password format, must be any visible character of length 6-18, containing uppercase and lowercase letters, numbers, and special characters"
"gf.gvalid.rule.postcode" = "{field} field value `{value}` is not a valid postal code"
"gf.gvalid.rule.resident-id" = "{field} field value `{value}` is not a valid resident ID number format"
"gf.gvalid.rule.bank-card" = "{field} field value `{value}` is not a valid bank card number format"
"gf.gvalid.rule.qq" = "{field} field value `{value}` is not a valid QQ number format"
"gf.gvalid.rule.ip" = "{field} field value `{value}` is not a valid IP address format"
"gf.gvalid.rule.ipv4" = "{field} field value `{value}` is not a valid IPv4 address format"
"gf.gvalid.rule.ipv6" = "{field} field value `{value}` is not a valid IPv6 address format"
"gf.gvalid.rule.mac" = "{field} field value `{value}` is not a valid MAC address format"
"gf.gvalid.rule.url" = "{field} field value `{value}` is not a valid URL format"
"gf.gvalid.rule.domain" = "{field} field value `{value}` is not a valid domain format"
"gf.gvalid.rule.length" = "{field} field value `{value}` length should be between {min} and {max} characters"
"gf.gvalid.rule.min-length" = "{field} field value `{value}` minimum length should be {min}"
"gf.gvalid.rule.max-length" = "{field} field value `{value}` maximum length should be {max}"
"gf.gvalid.rule.size" = "{field} field value `{value}` length must be {size}"
"gf.gvalid.rule.between" = "{field} field value `{value}` size should be between {min} and {max}"
"gf.gvalid.rule.min" = "{field} field value `{value}` minimum value should be {min}"
"gf.gvalid.rule.max" = "{field} field value `{value}` maximum value should be {max}"
"gf.gvalid.rule.json" = "{field} field value `{value}` should be in JSON format"
"gf.gvalid.rule.xml" = "{field} field value `{value}` should be in XML format"
"gf.gvalid.rule.array" = "{field} field value `{value}` should be an array"
"gf.gvalid.rule.integer" = "{field} field value `{value}` should be an integer"
"gf.gvalid.rule.float" = "{field} field value `{value}` should be a float"
"gf.gvalid.rule.boolean" = "{field} field value `{value}` should be a boolean"
"gf.gvalid.rule.same" = "{field} field value `{value}` must be the same as {field}"
"gf.gvalid.rule.different" = "{field} field value `{value}` cannot be the same as {field}"
"gf.gvalid.rule.in" = "{field} field value `{value}` should be within the range: {pattern}"
"gf.gvalid.rule.not-in" = "{field} field value `{value}` should not be within the range: {pattern}"
"gf.gvalid.rule.regex" = "{field} field value `{value}` does not match the rule: {pattern}"
"gf.gvalid.rule.__default__" = "{field} field value `{value}` is not valid"
"CustomMessage" = "Custom Error"
"project id must between {min}, {max}" = "Project ID must be greater than or equal to {min} and less than or equal to {max}"

Modify the main function to enable i18n:

main.go

package main  

···

func main() {
var err error

// Globally set i18n
g.I18n().SetLanguage("zh-CN")

// Check if the database can be connected
err = connDb()
if err != nil {
panic(err)
}

cmd.Main.Run(gctx.GetInitCtx())
}

···

Make another request:

$ 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 field cannot be empty",
"data":null
}

You can see that the message has been changed to a Chinese prompt.

Prevent Duplicate Usernames


Usernames are an important basis for login. If there are two users with the same name in the system, it will cause major logical confusion. Therefore, we need to check if the user exists before data is stored. If it exists, return an error message indicating that the user already exists.

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("User already exists")
}
return nil
}

Test the request result:

$ 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":"User already exists",
"data":null
}

Code detection alone is not sufficient for security. We can add a unique index in the data table to enforce user uniqueness.

ALTER TABLE users ADD UNIQUE (username);

Password Encryption


Saving passwords in plain text is very insecure. A common practice is to perform a hash calculation before saving to the database, such as md5 or SHA-1.

Add a new function encryptPassword to implement password encryption functionality.

internal/logic/users/utility.go

package users  

import "github.com/gogf/gf/v2/crypto/gmd5"

func encryptPassword(password string) string {
return gmd5.MustEncryptString(password)
}

The gmd5 component helps us quickly implement md5 encryption functionality. Write registration logic code and introduce password encryption.

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 the original user:

DELETE FROM users WHERE id = 1;

Request the interface again to see if the password is successfully encrypted:

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\"}"

Result:

IDUsernamePasswordEmailCreated_AtUpdated_At
1oldmee10adc3949ba59abbe56e057f20f883etyyn1022@gmail.com2024-11-08 10:36:482024-11-08 10:36:48

Register Function Optimization


Customize a data model in the model layer for use as input parameters in the Logic layer.

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
}

...

Change the Controller layer to pass in 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
}