Go語言 基於gin框架從0開始構建一個bbs server(二)-使用者登入

語言: CN / TW / HK

完善登入流程

上一篇文章 我們已經完成了註冊的流程,現在只要 照著之前的方法 完善我們的登入機制 即可

定義登入的引數

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