gin框架JWT验证实践(原理介绍,代码实践)

简介: gin框架JWT验证实践(原理介绍,代码实践)

前言

  作者大二,在网上看各种名词,什么cookie,session,JWT,看了几篇文章后,概念是理解了,但是真正想写一个小demo实践的时候,总是感觉无从下手,所以写本文的目的,意在帮助在与我同阶段的同学,从0到1的去梳理出流程,以提升自己的抽象和分析能力

前置要求:文demo使用了gin和gorm,如果不知道gin和gorm两个库,如果没有使用过的需要提前去了解一下基本用法即可

//TODO 挖坑 gin 和 gorm 相关内容 后续补充

GORM汇总

demo链接:gopherWxf-wake

JWT介绍

背景

在如今前后端分离开发的⼤环境中,我们需要解决⼀些登陆,后期身份认证以及鉴权相关的事情,通常的⽅案就是采⽤请求头携带token的⽅式进⾏实现。本篇⽂章主要分享下在Golang语⾔下使⽤jwt-go来实现后端的token认证逻辑。

 JSON Web Token(JWT) 是⼀个常⽤语HTTP的客户端和服务端间进⾏身份认证和鉴权的标准规范,使⽤JWT可以允许我们在⽤户和服务器之间传递安全可靠的信息。在开始学习JWT之前,我们可以先了解下早期的⼏种⽅案。

token

token

token的意思是“令牌”,是⽤户身份的验证⽅式,最简单的token组成: uid(⽤户唯⼀标识) + time(当前时间戳) + sign(签名,由token的前⼏位+哈希算法压缩成⼀定⻓度的⼗六进制字符串) ,同时还可以将不变的参数也放进token,这⾥我主要想讲的就是 Json Web Token ,也就是本文的主题:JWT

Json-Web-Token(JWT)介绍

  ⼀般⽽⾔,⽤户注册登陆后会⽣成⼀个jwt的token返回给浏览器,浏览器向服务端请求数据时携带token ,服务器端使⽤ header 中定义的⽅式进⾏解码,进⽽对token进⾏解析和验证。

JWT-Token组成部分:

  • header: ⽤来指定使⽤的算法(HMAC SHA256 RSA)和token类型(如JWT)
  • payload: 包含声明(要求),声明通常是⽤户信息或其他数据的声明,⽐如⽤户id,名称,邮箱等
  • signature: signature签名部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。

首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名

HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)

  签名是(signature) ⽤于验证消息在传递过程中有没有被更改 ,并且,对于使⽤私钥签名的token,它还可以验证JWT的发送⽅是否为它所称的发送⽅。

注意JWT每部分的作用,在服务端接收到客户端发送过来的JWT token之后:

  • header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据
  • signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secret对header、payload进行加密,比对加密后的数据和客户端发送过来的是否一致。注意secret只能保存在服务端。

  ⾃⼰计算出来的签名和接受到的签名不⼀样,那么就说明这个Token的内容被别⼈动过的,我们应该拒绝这个Token,返回⼀个HTTP 401 Unauthorized响应。

  注意:在JWT中,不应该在载荷⾥⾯加⼊任何敏感的数据,⽐如⽤户的密码。

JSON Web TokenS - jwt.io在jwt.io⽹站中,提供了⼀些JWT token的编码,验证以及⽣成jwt的⼯具。

可以看到header 和 payload 部分可以直接解码出来

此时签名为kV8BkKaSKlugSV6moWGJs87lS0hyNnqHYzroqKSGs-8

kV8BkKaSKlugSV6moWGJs87lS0hyNnqHYzroqKSGs-8=HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)

如果篡改了header或者payload的内容,那么计算hs256的值一定不一样

HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)

介绍完jwt的原理,下面我们就该动手实践了!

Demo代码分析

流程分析

流程分析:

  1. 什么时候创建token并返回给前端呢?用户登陆的时候,因为接下来的网页用户不需要再次登陆才能访问了
  2. 前端用户携带token访问后端的时候,后端需要干什么呢?后端需要校验token是否合法,如果合法则继续执行业务逻辑,如果不合法直接返回错误,中断本次业务逻辑

好,初步的流程是

  1. 登陆的时候后端创建token返回给前端
  2. 除了登陆和注册的接口,其他接口都携带该token访问后端

框架分析

我们首先需要对demo的项目注册有个大概的了解

demo演示

注册:

登陆:

访问测试接口:

更新token时间:

后端日志打印:

走入源码

入口main:

首先我们的程序需要连接数据库(因为用户注册和登陆需要数据库),定义4个接口

POST   /apis/v1/register         --> jwtDemo/controller.RegisterUser  //注册
POST   /apis/v1/login            --> jwtDemo/controller.Login         //登陆
下面两个接口都有一个token校验的中间件sv1.Use(middleware.JWTAuth)
GET    /apis/v1/auth/sayHello    --> jwtDemo/controller.SayHello      //测试
GET    /apis/v1/auth/refresh     --> jwtDemo/controller.Refresh       //更新token
func main() {
  //从配置文件中读取数据库的配置信息并连接数据库
  if dbErr := opdb.InitMySqlConn(); dbErr != nil {
    log.Panicln(dbErr)
  }
  defer opdb.DB.Close()
  //初始化表结构
  opdb.InitModel()
  router := gin.Default()
  v1 := router.Group("apis/v1")
  {   //注册
    v1.POST("/register", controller.RegisterUser)
    //登陆,返回token
    v1.POST("/login", controller.Login)
  }
  sv1 := v1.Group("/auth")
  //检验token
  sv1.Use(middleware.JWTAuth)
  {   //测试
    sv1.GET("/sayHello", controller.SayHello)
    //更新token
    sv1.GET("/refresh", controller.Refresh)
  }
  router.Run(":8080")
}
  • controller.RegisterUser:前端返回的json,包含用户名,密码,手机号,地址等然后后端存入数据库
  • controller.Login:通过前端返回的json,查询数据库中是否有该用户,有则创建一个json并返回
  • middleware.JWTAuth:校验改token是否合法,如果合法则继续执行,不合法直接结束流程
  • controller.SayHello:通过了JWTAuth的校验,改前端返回一个hello wxf
  • controller.Refresh:通过了JWTAuth的校验,给token续费,返回一个新的token

好,现在各个回调函数的流程都知道了,我们详细来看看创建token和校验token的代码

创建token:

  1. 创建一个包含secret的结构体
  2. 构造用户claims信息(负荷jwt中的第二部分)
  3. 通过secret密钥返回token
  //验证账号密码是否正确,即是否在数据库中存在,注册过
  pass, dbErr := opdb.LoginPass(loginReq)
  if !pass {
    c.JSON(http.StatusOK, gin.H{
      "status": -1,
      "msg":    "账号或密码错误" + dbErr.Error(),
      "data":   nil,
    })
    return
  }
  //创建一个token
  token := generateToken(c, loginReq)
//创建一个token
func generateToken(c *gin.Context, loginReq dfst.LoginReq) (token string) {
  // 构造SignKey: 签名和解签名需要使用一个值
  jwt := middleware.NewJWT()
  // 构造用户claims信息(负荷)
  claims := middleware.CustomClaims{
    Name: loginReq.Name,
    StandardClaims: jwt2.StandardClaims{
      NotBefore: time.Now().Unix() - 1000, // 签名生效时间
      ExpiresAt: time.Now().Unix() + 3600, // 签名过期时间
      Issuer:    "wxf.top",                // 签名颁发者
    },
  }
  // 根据claims生成token对象
  token, err := jwt.CreateToken(claims)
  if err != nil {
    c.JSON(http.StatusOK, gin.H{
      "status": -1,
      "msg":    err.Error(),
      "data":   nil,
    })
  }
  log.Println("create token", token)
  return
}
//创建token
func (j *JWT) CreateToken(claims CustomClaims) (string, error) {
  token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
  //获取完整的签名令牌
  return token.SignedString(j.SigningKey)
}

校验token:

  1. 从header的头部的token字段获取token
  2. 解析token,并将PAYLOAD负载提取出来
  3. 将负载添加到context上下文中供调用链中的函数使用
func JWTAuth(c *gin.Context) {
  //从header的头部的token字段获取token
  token := c.Request.Header.Get("token")
  if token == "" {
    c.JSON(http.StatusOK, gin.H{
      "status": -1,
      "msg":    "请求未携带token,无权限访问",
      "data":   nil,
    })
    c.Abort()
    return
  }
  log.Println("recv tokens:", token)
  j := NewJWT()
  //解析token,并将PAYLOAD负载提取出来
  claims, err := j.ParserToken(token)
  if err != nil {
    // token过期
    if err == TokenExpired {
      c.JSON(http.StatusOK, gin.H{
        "status": -1,
        "msg":    "token授权已过期,请重新申请授权",
        "data":   nil,
      })
      //中断调用链
      c.Abort()
      return
    }
    // 其他错误
    c.JSON(http.StatusOK, gin.H{
      "status": -1,
      "msg":    err.Error(),
      "data":   nil,
    })
    c.Abort()
    return
  }
  //将负载添加到context上下文中供调用链中的函数使用
  c.Set("claims", claims)
}
//解析token,并将PAYLOAD负载提取出来
func (j *JWT) ParserToken(tokenString string) (*CustomClaims, error) {
  token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
    return j.SigningKey, nil
  })
  if err != nil {
    // jwt.ValidationError 是一个无效token的错误结构
    if ve, ok := err.(*jwt.ValidationError); ok {
      // ValidationErrorMalformed是一个uint常量,表示token不可用
      if ve.Errors&jwt.ValidationErrorMalformed != 0 {
        return nil, TokenMalformed
        // ValidationErrorExpired表示Token过期
      } else if ve.Errors&jwt.ValidationErrorExpired != 0 {
        return nil, TokenExpired
        // ValidationErrorNotValidYet表示无效token
      } 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
}


目录
相关文章
|
2月前
|
存储 JSON 算法
无懈可击的身份验证:深入了解JWT的工作原理
无懈可击的身份验证:深入了解JWT的工作原理
70 0
|
5月前
|
JSON 安全 Java
JWT的原理及实际使用
JWT的原理及实际使用
62 0
|
5月前
|
前端开发
什么是JWT?深入理解JWT从原理到应用(下)
什么是JWT?深入理解JWT从原理到应用(下)
31 0
|
6月前
|
JSON 算法 Java
Spring boot框架 JWT实现用户账户密码登录验证
Spring boot框架 JWT实现用户账户密码登录验证
|
6月前
|
存储 JSON 算法
JWT的原理及实际应用
JWT的原理及实际应用
78 1
|
6月前
|
存储 JSON 前端开发
了解什么是JWT的原理及实际应用
了解什么是JWT的原理及实际应用
1093 0
|
3月前
|
JSON 安全 网络安全
超详细的用户认证、权限、安全原理详解(认证、权限、JWT、RFC 7235、HTTPS、HSTS、PC端、服务端、移动端、第三方认证等等)
超详细的用户认证、权限、安全原理详解(认证、权限、JWT、RFC 7235、HTTPS、HSTS、PC端、服务端、移动端、第三方认证等等)
336 0
|
4月前
|
JSON 算法 测试技术
JWT库生成Token的使用与原理
JWT库生成Token的使用与原理
106 0
|
4月前
|
JSON 算法 数据库
JWT(JSON Web Token)的基本原理
JWT(JSON Web Token)的基本原理
|
5月前
|
JSON 算法 前端开发
什么是JWT?深入理解JWT从原理到应用(上)
什么是JWT?深入理解JWT从原理到应用(上)
191 0