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
``` //驗證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") }) ```
- Jetpack Compose - Effect與協程 (十五)
- 谷歌的bug:當 CompileSdk 33 遇上Kotlin
- Jetpack Compose - DrawModifier (十三)
- Jetpack Compose - Transition動畫 (十)
- Jetpack Compose - 慣性衰減動畫AnimateDecay(八)
- Jetpack Compose - AnimationSpec (七)
- Jetpack Compose - Animatable與animateAsState (六)
- Jetpack Compose - 淺談 CompositionLocal (五)
- ASM 修改位元組碼 引發的R8 編譯報錯
- Go語言 基於gin框架從0開始構建一個bbs server(二)-使用者登入
- Go語言 基於gin定義一個簡單的web server 開發框架(三)
- Go語言 基於gin定義一個簡單的web server 開發框架(二)
- Go語言 viper與web服務的關機,重啟
- Go語言 zap日誌系統與gin繼承
- Go語言 go-redis與watch
- Go 語言 MySql 基本操作
- Go 語言 常用標準庫 介紹
- Go語言 併發-select 與 鎖
- Go語言 併發-Channel
- Go語言 錯誤處理以及資源管理