第四节制作中间件

上节课给大家讲了一个配置,也讲了一个简单的跨域中间件,那今天讲一下做一个jwt的token鉴权中间件。

什么是JWT

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

基于token的鉴权机制

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。 流程上是这样的:

  • 用户使用用户名密码来请求服务器
  • 服务器进行验证用户的信息
  • 服务器通过验证发送给用户一个token
  • 客户端存储token,并在每次请求时附送上这个token值
  • 服务端验证token值,并返回数据

这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *

JWT的构成

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

使用JWT

首先我们要使用JWT那么就需要将对应的包拉下来 执行github.com/dgrijalva/jwt-go

建立一个结构体

// 载荷,可以加一些自己需要的信息
type CustomClaims struct {
	Id        int64  `json:"user_id"`
	Username  string `json:"username"`
	Avatar    string `json:"avatar"`
	LoginTime int64  `json:"login_time"`
	Status    int64  `json:"status"`
	Email     string `json:"email"`
	Desc      string `json:"desc"`
	jwt.StandardClaims
}

这里面可以放一些我们后续可能需要的一些参数,比如说用户ID,用户名,等一些来作为用户唯一标示的参数, 还需要一个生成Token的方法

// 生成令牌
func GenerateToken(c *gin.Context, claims *jwt.CustomClaims) (token string, err error) {
	j := jwt.JWT{
		[]byte("now_im"),
	}
	token, err = j.CreateToken(*claims)
	return
}

有这个方法后就可以将参数传入后 我们生成一个token

claims := jwt.CustomClaims{
		User.ID,
		User.Username,
		User.Avatar,
		User.LoginTime,
		User.Status,
		User.Email,
		User.Desc,
		jwtgo.StandardClaims{
			NotBefore: time.Now().Unix(),           // 签名生效时间
			ExpiresAt: time.Now().Unix() + 86400*7, // 过期时间 一小时
			Issuer:    "now_im",                    // 签名的发行者
		},
	}
	token, _ := tools.GenerateToken(c, &claims)

这里是生成token可以看到上面我们还用到了CreateToken方法,在这个里面呢还需要创建一系列的token操作的方法,

// @File  : jwt.go
// @Author: JunLong.Liao&此处不应有BUG!
// @Date  : 2021/4/21
// @slogan: 又是不想写代码的一天,神兽保佑,代码无BUG!
//         ┏┓      ┏┓
//        ┏┛┻━━━━━━┛┻┓
//        ┃     ღ    ┃
//        ┃  ┳┛   ┗┳ ┃
//        ┃     ┻    ┃
//        ┗━┓      ┏━┛
//          ┃      ┗━━━┓
//          ┃ 神兽咆哮!  ┣┓
//          ┃         ┏┛
//          ┗┓┓┏━━━┳┓┏┛
//           ┃┫┫   ┃┫┫
//           ┗┻┛   ┗┻┛

package jwt

import (
	"errors"
	"log"
	"net/http"
	"time"

	"github.com/dgrijalva/jwt-go"
	"github.com/gin-gonic/gin"
)

// JWTAuth 中间件,检查token
func JWTAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		token := c.Request.Header.Get("token")
		if token == "" {
			c.JSON(http.StatusOK, gin.H{
				"status": 1000,
				"msg":    "请求未携带token,无权限访问",
			})
			c.Abort()
			return
		}

		log.Print("get token: ", token)

		j := NewJWT()
		// parseToken 解析token包含的信息
		claims, err := j.ParseToken(token)
		if err != nil {
			if err == TokenExpired {
				c.JSON(http.StatusUnauthorized, gin.H{
					"status": 1001,
					"msg":    "授权已过期",
				})
				c.Abort()
				return
			}
			c.JSON(http.StatusOK, gin.H{
				"status": 1002,
				"msg":    err.Error(),
			})
			c.Abort()
			return
		}
		// 继续交由下一个路由处理,并将解析出的信息传递下去
		c.Set("claims", claims)
	}
}

// JWT 签名结构
type JWT struct {
	SigningKey []byte
}

// 一些常量
var (
	TokenExpired     error  = errors.New("Token is expired")
	TokenNotValidYet error  = errors.New("Token not active yet")
	TokenMalformed   error  = errors.New("That's not even a token")
	TokenInvalid     error  = errors.New("Couldn't handle this token:")
	SignKey          string = "hignton"
)

// 载荷,可以加一些自己需要的信息
type CustomClaims struct {
	Id        int64  `json:"user_id"`
	Username  string `json:"username"`
	Avatar    string `json:"avatar"`
	LoginTime int64  `json:"login_time"`
	Status    int64  `json:"status"`
	Email     string `json:"email"`
	Desc      string `json:"desc"`
	jwt.StandardClaims
}

// 新建一个jwt实例
func NewJWT() *JWT {
	return &JWT{
		[]byte(GetSignKey()),
	}
}

// 获取signKey
func GetSignKey() string {
	return SignKey
}

// 这是SignKey
func SetSignKey(key string) string {
	SignKey = key
	return SignKey
}

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

// 解析Tokne
func (j *JWT) ParseToken(tokenString string) (*CustomClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, 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 claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		return claims, nil
	}
	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
}

这个文件中包含了token生成CreateToken,更新RefreshToken,解析ParseToken,以及token的一个中间件JWTAuth 这里主要讲一下JWTAuth这个中间件。首先的话用的是gin框架 那么我们需要返回一个HandlerFun

这里首先我们要将token获取到,这里token是传入在header中的 那么我们就需要通过 c.Request.Header.Get("token")来获取到token,拿到了之后判断是否为空,然后调用ParseToken来解析token,拿到其中的参数,用户ID,过期时间等,这里判断成功后 直接return,这样一个中间件就可以了

接下来在我们的路由处加入这个token即完成了一个中间件

api.Use(jwt.JWTAuth())
{
    // socket
    socketGroup.GET("/connect", websocket.WebsocketManager.WsClient)
}

就是这样简单。