完善登录流程
上一篇文章 我们已经完成了注册的流程,现在只要 照着之前的方法 完善我们的登录机制 即可
定义登录的参数
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")
})
复制代码