10篇带你手摸手封装gin框架(8)-jwt验证与登录接口完善

2,516 阅读4分钟

前言

这是我参与更文挑战的第8天,大家好,我是作曲家种太阳
上一篇我们学完了redis接入和图形验证码接口的开发
这一篇我们主要是是讲中间件并 完善登录接口接口

1. 介绍

之前在 Zap日志管理章节 了解过了中间件,并编写了记录http异常的日志中间件
这里有几篇文章讲的非常好:
Go Web轻量级框架Gin学习系列:中间件使用详解
Gin框架系列03:换个姿势理解中间件
中间件图示: image.png 我对中间件的理解:
1.类似于koa的洋葱模型,Django的钩子函数,前端的生命周期
2.在请求接口前或者后做一些逻辑出来
中间件几个关键字:
1. c.Next() 进入下一个中间件
1. c.Abort() 中断中间件(return 不能中断中间件的调用)

2. JWT介绍

jwt官网: image.png 你可以吧后面的生成的token放到这里反向解析下~ 安装jwt库:

 go get github.com/dgrijalva/jwt-go

(1).JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:

  • Header
  • Payload
  • Signature (2)用Token的好处
    无状态和可扩展性:Tokens存储在客户端。完全无状态,可扩展。我们的负载均衡器可以将用户传递到任意服务器,因为在任何地方都没有状态或会话信息。
    安全性:每次请求的时候Token都会被发送。而且,由于没有Cookie被发送,还有助于防止CSRF攻击。即使在你的实现中将token存储到客户端的Cookie中,这个Cookie也只是一种存储机制,而非身份认证机制。没有基于会话的信息可以操作,因为我们没有会话!

与前端约定:

  1. 登录请求的时候,后端把token传递给前端,前端保存token
  2. 前端在http请求的header上加入key为x-token的请求头,值为token

2. JWT中间件开发

(1).添加token配置

2.1.1 在 settings-dev.yaml 中加入,生成key的地址

jwt:
  key: "EYsnfKMf5XWk87LASEs28Dj5ZqGkSerH"

2.1.2 在config/config.go加入配置的struct

image.png

(2).删除forms/user.go中的PasswordLoginForm.UserName里tag中mobile自定义校验

ps: 不再验证username必须为手机号

(3).编写JWT中间件和相关代码

3.2.1 在 middlewares/jwt中编写(复制的同时,请大致读一下注释和代码逻辑~)

package middlewares
import (
	"errors"
	"fmt"
	"github.com/dgrijalva/jwt-go"
	"github.com/fatih/color"
	"github.com/gin-gonic/gin"
	"go_gin/Response"
	"go_gin/global"
	"net/http"
	"time"
)

type CustomClaims struct {
	ID          uint   //
	NickName    string //
	AuthorityId uint   //
	jwt.StandardClaims
}

func JWTAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localSstorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录
		token := c.Request.Header.Get("x-token")
		color.Yellow(token)
		if token == "" {
			Response.Err(c, http.StatusUnauthorized, 401, "请登录", "")
			c.Abort()
			return
		}
		j := NewJWT()
		// parseToken 解析token包含的信息
		claims, err := j.ParseToken(token)
		if err != nil {
			if err == TokenExpired {
				if err == TokenExpired {
					Response.Err(c, http.StatusUnauthorized, 401, "授权已过期", "")
					c.Abort()
					return
				}
			}
			Response.Err(c, http.StatusUnauthorized, 401, "未登陆", "")
			c.Abort()
			return
		}
		fmt.Println(c)
		// gin的上下文记录claims和userId的值
		c.Set("claims", claims)
		c.Set("userId", claims.ID)
		c.Next()
	}
}

type JWT struct {
	SigningKey []byte
}

var (
	TokenExpired     = errors.New("Token is expired")
	TokenNotValidYet = errors.New("Token not active yet")
	TokenMalformed   = errors.New("That's not even a token")
	TokenInvalid     = errors.New("Couldn't handle this token:")
)

func NewJWT() *JWT {
	return &JWT{
		[]byte(global.Settings.JWTKey.SigningKey),
	}
}

// 创建一个token
func (j *JWT) CreateToken(claims CustomClaims) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(j.SigningKey)
}

// 解析 token
func (j *JWT) ParseToken(tokenString string) (*CustomClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) {
		return j.SigningKey, nil
	})
	if err != nil {
		if ve, ok := err.(*jwt.ValidationError); ok {
			if ve.Errors&jwt.ValidationErrorMalformed != 0 {
				return nil, TokenMalformed
			} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
				// Token is expired
				return nil, TokenExpired
			} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
				return nil, TokenNotValidYet
			} else {
				return nil, TokenInvalid
			}
		}
	}
	if token != nil {
		if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
			return claims, nil
		}
		return nil, TokenInvalid

	} else {
		return nil, TokenInvalid

	}

}

// 更新token
func (j *JWT) RefreshToken(tokenString string) (string, error) {
	jwt.TimeFunc = func() time.Time {
		return time.Unix(0, 0)
	}
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		return j.SigningKey, nil
	})
	if err != nil {
		return "", err
	}
	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		jwt.TimeFunc = time.Now
		claims.StandardClaims.ExpiresAt = time.Now().Add(1 * time.Hour).Unix()
		return j.CreateToken(*claims)
	}
	return "", TokenInvalid
}

3.2.2 . 在utils/createToken.go中添加CreateToken函数

package utils

import (
	"github.com/dgrijalva/jwt-go"
	"github.com/gin-gonic/gin"
	"go_gin/Response"
	"go_gin/middlewares"
	"time"
)

func CreateToken(c *gin.Context, Id int, NickName string, Role int) string {
	//生成token信息
	j := middlewares.NewJWT()
	claims := middlewares.CustomClaims{
		ID:          uint(Id),
		NickName:    NickName,
		AuthorityId: uint(Role),
		StandardClaims: jwt.StandardClaims{
			NotBefore: time.Now().Unix(),
			// TODO 设置token过期时间
			ExpiresAt: time.Now().Unix() + 60*60*24*30, //token -->30天过期
			Issuer:    "test",
		},
	}
	//生成token
	token, err := j.CreateToken(claims)
	if err != nil {
		Response.Success(c, 401, "token生成失败,重新再试", "test")
		return ""
	}
	return token
}

4.完善登录接口

(1) dao/userDao.go中添加UsernameFindUserInfo函数

var user models.User
// UsernameFindUserInfo 通过username找到用户信息
func FindUserInfo(username string, password string) (*models.User, bool) {
	// 查询用户
	rows := global.DB.Where(&models.User{NickName: username, Password: password}).Find(&user)
	fmt.Println(&user)
	if rows.RowsAffected < 1 {
		return &user, false
	}
	return &user, true
}

(2) 在controller/user.go中改写PasswordLogin逻辑

image.png ps: 通过username查询到用户是否存在,并且吧user的信息返回
注意几个点:
1.暂时屏蔽了图形验证码的校验
2.HandleUserModelToMap(controller/user.go)代码是

func HandleUserModelToMap(user *models.User) map[string]interface{} {
  birthday := ""
  if user.Birthday == nil {
  	birthday = ""
  } else {
  	birthday = user.Birthday.Format("2006-01-02")
  }
  userItemMap := map[string]interface{}{
  	"id":        user.ID,
  	"nick_name": user.NickName,
  	"head_url":  user.HeadUrl,
  	"birthday":  birthday,
  	"address":   user.Address,
  	"desc":      user.Desc,
  	"gender":    user.Gender,
  	"role":      user.Role,
  	"mobile":    user.Mobile,
  }
  return userItemMap
}

3.测试jwt中间件和登录接口测试

(1).登录接口测试

postman打开:http://127.0.0.1:8022/v1/user/list?page=2&pageSize=10 image.png ps:登录的时候正确的返回了token信息

(2). 测试jwt中间件验证

在router/user.go中添加jwt中间件验证: image.png ps: 中间件确实也是放在中间🤪🤪🤪
测试 userlist 接口结果:
postman打开:http://127.0.0.1:8022/v1/user/list?page=2&pageSize=10 image.png ps:可以自己测试下添加了token后成功的请求,这里不再赘述
到这一步,说明jwt的中间件就添加成功了~继续下面其他中间件的开发吧!

如果这系列的文章对你有有用,请点赞和留言吧~