Go语言 基于gin框架从0开始构建一个bbs server

完善登录流程

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

定义登录的参数

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

定义 登录的controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
javascript复制代码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

1
2
3
4
5
6
7
go复制代码func Login(login *models.ParamLogin) error {
user := models.User{
Username: login.UserName,
Password: login.Password,
}
return mysql.Login(&user)
}

最后 看下登录的dao层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
go复制代码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函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
go复制代码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层 把我们的 错误 定义成常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
go复制代码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 这个写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
go复制代码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的 登录认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
go复制代码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层:

1
2
3
4
5
6
7
8
9
10
go复制代码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返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
scss复制代码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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码//验证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

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%