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 thexx@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:
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 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
}