zl程序教程

您现在的位置是:首页 >  其他

当前栏目

Beego学习——Jwt实现用户登录注册

注册学习 实现 用户 登录 JWT Beego
2023-06-13 09:18:38 时间

启动: bee run -gendoc=true -downdoc=true 文章内容中有的注释是个人的理解,可能不严谨 使用 mysql 的话记得导入包: _ "github.com/go-sql-driver/mysql"

1. models包

1.1 jwt.go

// JWT : header payload signature
// json web token: 标头 有效负载 签名
const (
	SecretKEY              string = "JWT-Secret-Key"
	DEFAULT_EXPIRE_SECONDS int    = 600 // 默认10分钟
	PasswordHashBytes             = 16
)

// MyCustomClaims
// This struct is the payload
// 此结构是有效负载
type MyCustomClaims struct {
	UserID int `json:"userID"`
	jwt.StandardClaims
}

// JwtPayload
// This struct is the parsing of token payload
// 此结构是对token有效负载的解析
type JwtPayload struct {
	Username  string `json:"username"`
	UserID    int    `json:"userID"`
	IssuedAt  int64  `json:"iat"` // 发布日期
	ExpiresAt int64  `json:"exp"` // 过期时间
}

// GenerateToken
// @Title GenerateToken
// @Description "生成token"
// @Param loginInfo 		*models.LoginRequest 	"登录请求"
// @Param userID 			int 					"用户ID"
// @Param expiredSeconds 	int 					"过期时间"
// @return    tokenString   string         	"编码后的token"
// @return    err   		error         	"错误信息"
func GenerateToken(loginInfo *LoginRequest, userID int, expiredSeconds int) (tokenString string, err error) {
	// 如果没设置过期时间,默认为 DEFAULT_EXPIRE_SECONDS 600s
	if expiredSeconds == 0 {
		expiredSeconds = DEFAULT_EXPIRE_SECONDS
	}


	// 创建声明
	mySigningKey := []byte(SecretKEY)
	// 过期时间 = 当前时间(/s)+ expiredSeconds(/s)
	expireAt := time.Now().Add(time.Second * time.Duration(expiredSeconds)).Unix()
	logs.Info("Token 将到期于:", time.Unix(expireAt, 0))

	user := *loginInfo
	claims := MyCustomClaims{
		userID,
		jwt.StandardClaims{
			Issuer:    user.Username,		// 发行者
			IssuedAt:  time.Now().Unix(),	// 发布时间
			ExpiresAt: expireAt,			// 过期时间
		},
	}


	// 利用上面创建的声明 生成token
	// NewWithClaims(签名算法 SigningMethod, 声明 Claims) *Token
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// 利用密钥对token签名
	tokenStr, err := token.SignedString(mySigningKey)
	if err != nil {
		return "",
		errors.New("错误: token生成失败!")
	}
	return tokenStr, nil
}

// ValidateToken
// @Title ValidateToken
// @Description "验证token"
// @Param tokenString 		string 	"编码后的token"
// @return   	*JwtPayload     "Jwt有效负载的解析"
// @return   	error         	"错误信息"
func ValidateToken(tokenString string) (*JwtPayload, error) {
	// 获取编码前的token信息
	token, err := jwt.ParseWithClaims(tokenString,
		&MyCustomClaims{},
		func(token *jwt.Token) (interface{}, error) {
			return []byte(SecretKEY), nil
		})
	// 获取payload-声明内容
	claims, ok := token.Claims.(*MyCustomClaims)
	if ok && token.Valid {
		logs.Info("%v %v",
			claims.UserID,
			claims.StandardClaims.ExpiresAt, // 过期时间
		)
		logs.Info("Token 将过期于:",
			time.Unix(claims.StandardClaims.ExpiresAt, 0),
		)
		return &JwtPayload{
			Username:  claims.StandardClaims.Issuer, 	// 用户名:发行者
			UserID:    claims.UserID,
			IssuedAt:  claims.StandardClaims.IssuedAt,
			ExpiresAt: claims.StandardClaims.ExpiresAt,
		}, nil
	} else {
		logs.Info(err.Error())
		return nil, errors.New("错误: token验证失败")
	}
}

// RefreshToken
// @Title RefreshToken
// @Description "更新token"
// @Param tokenString 		string 		"编码后的token"
// @return   newTokenString string    "编码后的新的token"
// @return   err   			error     "错误信息"
func RefreshToken(tokenString string) (newTokenString string, err error) {
	// 获取上一个token
	token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{},
		func(token *jwt.Token) (interface{}, error) {
			return []byte(SecretKEY), nil
		})
	// 获取上一个token 的 payload-声明
	claims, ok := token.Claims.(*MyCustomClaims)
	if !ok || !token.Valid {
		return "", err
	}

	// 创建新的声明
	mySigningKey := []byte(SecretKEY)
	expireAt := time.Now().Add(time.Second * time.Duration(DEFAULT_EXPIRE_SECONDS)).Unix() //new expired
	newClaims := MyCustomClaims{
		claims.UserID,
		jwt.StandardClaims{
			Issuer:    claims.StandardClaims.Issuer, //name of token issue
			IssuedAt:  time.Now().Unix(),            //time of token issue
			ExpiresAt: expireAt,
		},
	}

	// 利用新的声明,生成新的token
	newToken := jwt.NewWithClaims(jwt.SigningMethodHS256, newClaims)
	// 利用签名算法对新的token进行签名
	tokenStr, err := newToken.SignedString(mySigningKey)
	if err != nil {
	return "", errors.New("错误: 新的新json web token 生成失败!")
	}

	return tokenStr, nil
}

// GenerateSalt
// @Title GenerateSalt
// @Description "生成用户的加密的钥匙|generate salt"
// @return   salt 		string    "生成用户的加密的钥匙"
// @return   err   		error     "错误信息"
func GenerateSalt() (salt string, err error) {
	buf := make([]byte, PasswordHashBytes)
	if _, err := io.ReadFull(rand.Reader, buf); err != nil {
		return "", errors.New("error: failed to generate user's salt")
	}

	return fmt.Sprintf("%x", buf), nil
}

// GeneratePassHash
// @Title GenerateSalt
// @Description "对密码加密|generate password hash"
// @Param password 		string 		"用户登录密码"
// @Param salt 			string 		"用户的加密的钥匙"
// @return    hash   string         "加密后的密码"
// @return    err    error         	"错误信息"
func GeneratePassHash(password string, salt string) (hash string, err error) {
	h, err := scrypt.Key([]byte(password), []byte(salt), 16384, 8, 1, PasswordHashBytes)
	if err != nil {
		return "", errors.New("error: failed to generate password hash")
	}

	return fmt.Sprintf("%x", h), nil
}

1.2 user.go

// TabUser 定义用户格式
type TabUser struct {
	Id            int    `json:"id" orm:"column(id);auto"`
	UserName      string `json:"username" orm:"column(username);size(128)"`
	Password      string `json:"password" orm:"column(password);size(128)"`
	Salt          string `json:"salt" orm:"column(salt);size(128)"`
}

// LoginRequest 定义登录请求格式
type LoginRequest struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

// LoginResponse 定义登录响应
type LoginResponse struct {
	Username    string             `json:"username"`
	UserID      int                `json:"userID"`
	Token       string             `json:"token"`
}

//CreateRequest 定义创建用户请求格式
type CreateRequest struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

//CreateResponse 定义创建用户响应
type CreateResponse struct {
	UserID   int    `json:"userID"`
	Username string `json:"username"`
}

// DoLogin
// @Title DoLogin
// @Description "用户登录"
// @Param lr 		*LoginRequest 	"登录请求"
// @return    *LoginResponse       	"登录响应"
// @return    int        			"状态码"
// @return    error         		"错误信息"
func DoLogin(lr *LoginRequest) (*LoginResponse, int, error) {
	// 获取用户名和密码
	username := lr.Username
	password := lr.Password

	// 验证用户名和密码是否为空
	if len(username) == 0 || len(password) == 0 {
		return nil,
			http.StatusBadRequest,
			errors.New("error: 用户名或密码为空")
	}

	// 连接数据库
	o := orm.NewOrm()

	// 检查用户名是否存在
	user := &TabUser{UserName: username}
	err := o.Read(user, "username")
	if err != nil {
		return nil,
			http.StatusBadRequest,	// 400
			errors.New("error: 用户名不存在")
	}

	// 生成hash加密后的密码
	hash, err := GeneratePassHash(password, user.Salt)
	if err != nil {
		return nil,
			http.StatusBadRequest, // 400
			err
	}
	// 比较用户输入的密码+用户的加密钥匙生成的hash密码 与 数据库中存的hash密码
	if hash != user.Password {
		return nil,
			http.StatusBadRequest,
			errors.New("错误: 密码错误!")
	}

	// 生成token
	tokenString, err := GenerateToken(lr, user.Id, 0)
	if err != nil {
		return nil,
			http.StatusBadRequest,
			err
	}

	// 生成的token 返回给前端
	return &LoginResponse{
		Username:    user.UserName,
		UserID:      user.Id,
		Token:       tokenString,
	}, http.StatusOK, nil
}

// DoCreateUser
// @Title DoCreateUser
// @Description "创建用户"
// @Param  cr 	*CreateRequest 		"用户创建请求"
// @return    *CreateResponse       "用户创建响应"
// @return    int        			"状态码"
// @return    error         		"错误信息"
func DoCreateUser(cr *CreateRequest) (*CreateResponse, int, error) {
	// 连接数据库
	o := orm.NewOrm()

	// 检查用户名是否存在
	userNameCheck := TabUser{UserName: cr.Username}
	err := o.Read(&userNameCheck, "username")
	if err == nil {
		return nil, http.StatusBadRequest, errors.New("username has already existed")
	}

	// 生成 用户的加密的钥匙
	saltKey, err := GenerateSalt()
	if err != nil {
		logs.Info(err.Error())
		return nil, http.StatusBadRequest, err
	}

	// 生成hash加密的密码
	hash, err := GeneratePassHash(cr.Password, saltKey)
	if err != nil {
		logs.Info(err.Error())
		return nil, http.StatusBadRequest, err
	}

	// 创建用户
	user := TabUser{}
	user.UserName = cr.Username
	user.Password = hash
	user.Salt = saltKey

	_, err = o.Insert(&user)
	if err != nil {
		logs.Info(err.Error())
		return nil, http.StatusBadRequest, err
	}

	return &CreateResponse{
		UserID:   user.Id,
		Username: user.UserName,
	}, http.StatusOK, nil
}

// 映射mysql数据
func init() {
	orm.RegisterModel(new(TabUser))
}

2. controller包

2.1 user.go

// UserController 处理与用户相关的请求
// user API
type UserController struct {
	beego.Controller
}

// 解析请求,并将请求体存储到v中
// unmarshalPayload
// @Param	v	interface{}	true	"接收解析后的请求体的变量"
func (c *UserController) unmarshalPayload(v interface{}) error {
	// json 解析
	// Unmarshal(data []byte, v interface{})
	// 将json字符串解码到相应的数据结构
	err := json.Unmarshal(c.Ctx.Input.RequestBody, &v)
	if err != nil {
		logs.Error("RequestBody 解析失败!")
	}
	if err != nil {
		logs.Error("unmarshal payload of %s error: %s", c.Ctx.Request.URL.Path, err)
	}
	return nil
}

// respond
// @Title respond
// @Description 返回给前端的信息
// @Param	code		 	int				true		"状态码"
// @Param	message		 	string			true		"返回信息"
// @Param	data		 	...interface{}	true		"数据"
func (c *UserController) respond(code int, message string, data ...interface{}) {
	c.Ctx.Output.SetStatus(code)
	var d interface{}
	if len(data) > 0 {
		d = data[0]
	}
	c.Data["json"] = struct {
		Code    int         `json:"code"`
		Message string      `json:"message"`
		Data    interface{} `json:"data,omitempty"`
	}{
		Code:    code,
		Message: message,
		Data:    d,
	}
	c.ServeJSON()
}

// Login
// @Title Login
// @Description 处理登录请求
func (c *UserController) Login() {
	lr := new(models.LoginRequest)

	if err := c.unmarshalPayload(lr); err != nil {
		c.respond(http.StatusBadRequest, err.Error())
		return
	}

	lrs, statusCode, err := models.DoLogin(lr)
	if err != nil {
		c.respond(statusCode, err.Error())
		return
	}
	// 将token设置到Header
	c.Ctx.Output.Header("Authorization", lrs.Token)

	c.respond(http.StatusOK, "", lrs)
}

// CreateUser
// @Title CreateUser
// @Description 新增用户
// @Success 200
// @router /register [post]
func (c *UserController) CreateUser() {
	cu := new(models.CreateRequest)
	// 获取request body
	if err := c.unmarshalPayload(cu); err != nil {
		c.respond(http.StatusBadRequest, err.Error())
	}
	createUser, statusCode, err := models.DoCreateUser(cu)
	if err != nil {
		c.respond(statusCode, err.Error())
		return
	}
	c.respond(http.StatusOK, "", createUser)
}

3.routers包

3.1 router.go

func init() {
	ns := beego.NewNamespace("/api",
		beego.NSNamespace("/user",
			beego.NSRouter("/login", &controllers.UserController{}, "post:Login"),
			beego.NSRouter("/register", &controllers.UserController{}, "post:CreateUser"),
		),
	)
	beego.AddNamespace(ns)
}

4. conf包

appname = 项目名称
httpport = 8080
runmode = dev
autorender = false
copyrequestbody = true
EnableDocs = true
sqlconn =

[mysql]
dbHost = "localhost"
dbPort = "3306"
dbUser = "用户名"
dbName = "数据库名称"
dbPassword = "数据库密码"

5. main.go

func main() {
	if beego.BConfig.RunMode == "dev" {
		beego.BConfig.WebConfig.DirectoryIndex = true
		beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
	}

	beego.Run()
}

func init() {
	// mysql
	dbHost := beego.AppConfig.String("dbHost")
	dbPort := beego.AppConfig.String("dbPort")
	dbUser := beego.AppConfig.String("dbUser")
	dbPassword := beego.AppConfig.String("dbPassword")
	dbName :=beego.AppConfig.String("dbName")
	dsn := dbUser + ":" + dbPassword +"@tcp("+ dbHost +":"+ dbPort +")/"+ dbName +"?charset=utf8"
	// 设置数据库基本信息,相当于连接数据库
	_ = orm.RegisterDataBase("default","mysql",dsn,30)
	// 生成表
	_ = orm.RunSyncdb("default", false, true)
}