Go语言 基于gin框架从0开始构建一个bbs server(二)-用户登录

完善登录流程

上一篇文章 我们已经完成了注册的流程,现在只要 照着之前的方法 完善我们的登录机制 即可

定义登录的参数

type ParamLogin struct {
   UserName string `json:"username" binding:"required"`
   Password string `json:"password" binding:"required"`
}
复制代码

定义 登录的controller

func LoginHandler(c *gin.Context) {
   p := new(models.ParamLogin)

   if err := c.ShouldBindJSON(p); err != nil {
      zap.L().Error("LoginHandler with invalid param", zap.Error(err))
      // 因为有的错误 比如json格式不对的错误 是不属于validator错误的 自然无法翻译,所以这里要做类型判断
      errs, ok := err.(validator.ValidationErrors)
      if !ok {
         c.JSON(http.StatusOK, gin.H{
            "msg": err.Error(),
         })
      } else {
         c.JSON(http.StatusOK, gin.H{
            "msg": removeTopStruct(errs.Translate(trans)),
         })
      }
      return
   }

   // 业务处理
   err := logic.Login(p)
   if err != nil {
      // 可以在日志中 看出 到底是哪些用户一直在尝试登录
      zap.L().Error("login failed", zap.String("username", p.UserName), zap.Error(err))
      c.JSON(http.StatusOK, gin.H{
         "msg": "用户名或密码不正确",
      })
      return
   }
   // 返回响应
   c.JSON(http.StatusOK, "login success")
}
复制代码

定义 登录的logic

func Login(login *models.ParamLogin) error {
   user := models.User{
      Username: login.UserName,
      Password: login.Password,
   }
   return mysql.Login(&user)
}
复制代码

最后 看下登录的dao层

func Login(user *models.User) error {
   oldPassword := user.Password
   sqlStr := `select user_id,username,password from user where username=?`
   err := db.Get(user, sqlStr, user.Username)
   if err == sql.ErrNoRows {
      return errors.New("该用户不存在")
   }
   if err != nil {
      return err
   }
   if encryptPassword(oldPassword) != user.Password {
      return errors.New("密码不正确")
   }
   return nil
}
复制代码

封装我们的响应方法

前面完成了登录和注册的方法以后 我们会发现 流程上 还有点冗余,响应方法有些重复 代码,这里 尝试优化一下

首先定义我们的 response code

package controllers

type ResCode int64

const (
   CodeSuccess ResCode = 1000 + iota
   CodeInvalidParam
   CodeUserExist
   CodeInvalidPassword
   CodeServerBusy
)

var codeMsgMap = map[ResCode]string{
   CodeSuccess:         "success",
   CodeInvalidParam:    "请求参数错误",
   CodeUserExist:       "用户已存在",
   CodeInvalidPassword: "用户名或密码不正确",
   CodeServerBusy:      "服务繁忙 请稍后再试",
}

func (c ResCode) Msg() string {
   msg, ok := codeMsgMap[c]
   if !ok {
      msg = codeMsgMap[CodeServerBusy]
   }
   return msg
}
复制代码

然后定义我们的response函数

package controllers

import (
   "net/http"

   "github.com/gin-gonic/gin"
)

type Response struct {
   Code ResCode     `json:"code"`
   Msg  interface{} `json:"msg"`
   Data interface{} `json:"data"`
}

func ResponseError(c *gin.Context, code ResCode) {
   c.JSON(http.StatusOK, &Response{
      Code: code,
      Msg:  code.Msg(),
      Data: nil,
   })
}

func ResponseErrorWithMsg(c *gin.Context, code ResCode, msg interface{}) {
   c.JSON(http.StatusOK, &Response{
      Code: code,
      Msg:  msg,
      Data: nil,
   })
}

func ResponseSuccess(c *gin.Context, data interface{}) {

   c.JSON(http.StatusOK, &Response{
      Code: CodeSuccess,
      Msg:  CodeSuccess.Msg(),
      Data: data,
   })
}
复制代码

顺便要去dao层 把我们的 错误 定义成常量

package mysql

import (
   "crypto/md5"
   "database/sql"
   "encoding/hex"
   "errors"
   "go_web_app/models"

   "go.uber.org/zap"
)

const serect = "wuyue.com"

// 定义 error的常量方便判断
var (
   UserAleadyExists = errors.New("用户已存在")
   WrongPassword    = errors.New("密码不正确")
   UserNoExists     = errors.New("用户不存在")
)

// dao层 其实就是将数据库操作 封装为函数 等待logic层 去调用她

func InsertUser(user *models.User) error {
   // 密码要加密保存
   user.Password = encryptPassword(user.Password)
   sqlstr := `insert into user(user_id,username,password) values(?,?,?)`
   _, err := db.Exec(sqlstr, user.UserId, user.Username, user.Password)
   if err != nil {
      zap.L().Error("InsertUser dn error", zap.Error(err))
      return err
   }
   return nil
}

//
func Login(user *models.User) error {
   oldPassword := user.Password
   sqlStr := `select user_id,username,password from user where username=?`
   err := db.Get(user, sqlStr, user.Username)
   if err == sql.ErrNoRows {
      return UserNoExists
   }
   if err != nil {
      return err
   }
   if encryptPassword(oldPassword) != user.Password {
      return WrongPassword
   }
   return nil
}

// CheckUserExist 检查数据库是否有该用户名
func CheckUserExist(username string) error {
   sqlstr := `select count(user_id) from user where username = ?`
   var count int
   err := db.Get(&count, sqlstr, username)
   if err != nil {
      zap.L().Error("CheckUserExist dn error", zap.Error(err))
      return err
   }
   if count > 0 {
      return UserAleadyExists
   }
   return nil
}

// 加密密码
func encryptPassword(password string) string {
   h := md5.New()
   h.Write([]byte(serect))
   return hex.EncodeToString(h.Sum([]byte(password)))
}
复制代码

最后 看下controller层如何处理

这里主要是关注一下 errors.Is 这个写法

package controllers

import (
   "errors"
   "go_web_app/dao/mysql"
   "go_web_app/logic"
   "go_web_app/models"

   "github.com/go-playground/validator/v10"

   "go.uber.org/zap"

   "github.com/gin-gonic/gin"
)

func LoginHandler(c *gin.Context) {
   p := new(models.ParamLogin)

   if err := c.ShouldBindJSON(p); err != nil {
      zap.L().Error("LoginHandler with invalid param", zap.Error(err))
      // 因为有的错误 比如json格式不对的错误 是不属于validator错误的 自然无法翻译,所以这里要做类型判断
      errs, ok := err.(validator.ValidationErrors)
      if !ok {
         ResponseError(c, CodeInvalidParam)
      } else {
         ResponseErrorWithMsg(c, CodeInvalidParam, removeTopStruct(errs.Translate(trans)))
      }
      return
   }

   // 业务处理
   err := logic.Login(p)
   if err != nil {
      // 可以在日志中 看出 到底是哪些用户不存在
      zap.L().Error("login failed", zap.String("username", p.UserName), zap.Error(err))
      if errors.Is(err, mysql.WrongPassword) {
         ResponseError(c, CodeInvalidPassword)
      } else {
         ResponseError(c, CodeServerBusy)
      }
      return
   }
   ResponseSuccess(c, "login success")
}

func RegisterHandler(c *gin.Context) {
   // 获取参数和参数校验
   p := new(models.ParamRegister)
   // 这里只能校验下 是否是标准的json格式 之类的 比较简单
   if err := c.ShouldBindJSON(p); err != nil {
      zap.L().Error("RegisterHandler with invalid param", zap.Error(err))
      // 因为有的错误 比如json格式不对的错误 是不属于validator错误的 自然无法翻译,所以这里要做类型判断
      errs, ok := err.(validator.ValidationErrors)
      if !ok {
         ResponseError(c, CodeInvalidParam)
      } else {
         ResponseErrorWithMsg(c, CodeInvalidParam, removeTopStruct(errs.Translate(trans)))
      }
      return
   }
   // 业务处理
   err := logic.Register(p)
   if err != nil {
      zap.L().Error("register failed", zap.String("username", p.UserName), zap.Error(err))
      if errors.Is(err, mysql.UserAleadyExists) {
         ResponseError(c, CodeUserExist)
      } else {
         ResponseError(c, CodeInvalidParam)
      }
      return
   }
   // 返回响应
   ResponseSuccess(c, "register success")
}
复制代码

最后看下我们的效果:

image.png

image.png

实现JWT的认证方式

关于JWT 可以自行查找相关概念,这里不重复叙述 仅实现一个JWT的 登录认证

package jwt

import (
   "errors"
   "time"

   "github.com/golang-jwt/jwt"
)

// MyClaims 注意这里不要 存储 密码之类的敏感信息哟
type MyClaims struct {
   UserId   int64  `json:"userId"`
   UserName string `json:"userName"`
   jwt.StandardClaims
}

const TokenExpireDuration = time.Hour * 2

var mySerect = []byte("wuyue is good man")

// GenToken 生成token
func GenToken(username string, userid int64) (string, error) {
   c := MyClaims{
      UserId:   userid,
      UserName: username,
      StandardClaims: jwt.StandardClaims{
         ExpiresAt: time.Now().Add(TokenExpireDuration).UnixNano(), //过期时间
         Issuer:    "bbs-project",                                  //签发人
      },
   }
   // 加密这个token
   token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
   // 用签名来 签名这个token
   return token.SignedString(mySerect)
}

// ParseToken 解析token
func ParseToken(tokenString string) (*MyClaims, error) {

   var mc = new(MyClaims)
   token, err := jwt.ParseWithClaims(tokenString, mc, func(token *jwt.Token) (interface{}, error) {
      return mySerect, nil
   })
   if err != nil {
      return nil, err
   }
   // 校验token
   if token.Valid {
      return mc, nil
   }

   return nil, errors.New("invalid token")

}
复制代码

剩下的就是 在登录成功的时候 返回这个token 给客户端即可

找到我们的logic层:

func Login(login *models.ParamLogin) (string, error) {
   user := models.User{
      Username: login.UserName,
      Password: login.Password,
   }
   if err := mysql.Login(&user); err != nil {
      return "", err
   }
   return jwt.GenToken(user.Username, user.UserId)
}
复制代码

在controller层 将我们的token返回:

func LoginHandler(c *gin.Context) {
   p := new(models.ParamLogin)

   if err := c.ShouldBindJSON(p); err != nil {
      zap.L().Error("LoginHandler with invalid param", zap.Error(err))
      // 因为有的错误 比如json格式不对的错误 是不属于validator错误的 自然无法翻译,所以这里要做类型判断
      errs, ok := err.(validator.ValidationErrors)
      if !ok {
         ResponseError(c, CodeInvalidParam)
      } else {
         ResponseErrorWithMsg(c, CodeInvalidParam, removeTopStruct(errs.Translate(trans)))
      }
      return
   }

   // 业务处理
   token, err := logic.Login(p)
   if err != nil {
      // 可以在日志中 看出 到底是哪些用户不存在
      zap.L().Error("login failed", zap.String("username", p.UserName), zap.Error(err))
      if errors.Is(err, mysql.WrongPassword) {
         ResponseError(c, CodeInvalidPassword)
      } else {
         ResponseError(c, CodeServerBusy)
      }
      return
   }
   ResponseSuccess(c, token)
}
复制代码

最后看下效果:

image.png

验证token

//验证jwt机制
r.GET("/ping", func(context *gin.Context) {
   // 这里post man 模拟的 将token auth-token
   token := context.Request.Header.Get("auth-token")
   if token == "" {
      controllers.ResponseError(context, controllers.CodeTokenIsEmpty)
      return
   }
   parseToken, err := jwt.ParseToken(token)
   if err != nil {
      controllers.ResponseError(context, controllers.CodeTokenInvalid)
      return
   }

   zap.L().Debug("token parese", zap.String("username", parseToken.UserName))
   controllers.ResponseSuccess(context, "pong")
})
复制代码

image.png

image.png

猜你喜欢

转载自juejin.im/post/7035242959813476383