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") }复制代码
最后看下我们的效果:
实现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) }复制代码
最后看下效果:
验证token
伪原创工具 SEO网站优化 https://www.237it.com/
//验证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") })复制代码
作者:vivo祁同伟
链接:https://juejin.cn/post/7035242959813476383