go语言后端开发学习(一)——JWT的介绍以及基于JWT实现登录验证

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: go语言后端开发学习(一)——JWT的介绍以及基于JWT实现登录验证

什么是JWT

JWT,全名为JSON Web Token,是当下主流的一种服务端通信认证方式,具有轻量,无状态的特点,它实现了让我们在用户与服务器之间传递安全可靠的Json文本信息,它的使用过程主要是这样的:

当用户注册的时候,服务端会接受到来自用户输入的账号与密码,然后服务端会向客户端发送JWT,而当客户端有了JWT这个令牌后,当下一次客户端服务端请求数据时,我们只要利用这个令牌,就可以轻松访问服务端的数据了,因此这种信息传输方式也有着开销少传输安全的特点。

JWT的下载

终端输入以下命令即可:

go get -u github.com/golang-jwt/jwt/v4

JWT的构成

简介

RFC标准中,JWT由以下三个部分组成:

  • Header: 头部
  • Payload: 载荷
  • Signature: 签名

我们会将这里的每一个部分用一个点.来分隔,最后组成一个字符串,格式如下:

header.payload.signature

而这就是一个JWT令牌的标准结构,接下来网格大家来逐个讲解每个结构的作用。

头部

Header中主要是声明一些基本信息,通常是由两部分来组成:

  • 令牌的类型
  • 签名所使用的加密算法
    比如下面的这个示例:
{
  "alg":"HS526",
  "typ":"JWT"
}

上面这个Json格式的数据意思大致为:令牌的类型为JWT,签名所使用的加密算法为HS526,最后再将JSON对象通过Base64Url编码为字符串,该字符串就是JWT的头部

载荷

JWT的第二部分是载荷部分,主要就是声明部分claims,声明部分通常是一个实体的数据,比如一个用户。而关于声明的类型主要有以下几种:

  • reigsteredReigetered claims代表着一些预定义的声明 ,例如:iss(issuer 签发者),exp(expiration time 过期时间),aud(audience 受众)
  • public:Puiblic claims这部分可以让使用JWT的人随意定义,但是最好避免与其他声明部分冲突
  • private claims:这部分的声明同样也是自定义的,通常用于在服务双方共享一些信息。

示例:

{
  "sub": "1234567890",
    "name": "John Doe",
    "admin": true
}

该JSON对象将通过Base64Url编码为字符串,该字符串就是JWT的第二部分。

注意:虽然载荷部分也受到保护,也有防篡改,但是这一部分是公共可读的,所以不要把敏感信息存放在JWT内。

签名

在获得了编码的头部和编码的载荷部分后,就可以通过头部所指明的签名算法根据前两个部分的内容再加上密钥进行加密签名,所以一旦JWT的内容有任何变化,解密时得到的签名都会不一样,同时如果是使用私钥,也可以对JWT的签发者进行验证。

JWT的工作原理

在身份验证中,用户使用凭据成功登录是,将会返回一个JSON WEB令牌,由于令牌是凭证,所以要求我们要常常小心地防止出现安全问题,所以令牌的保存时间不应超过其所需的时间,同时无论何时用户想要访问受保护的路由与资源,在发送请求时都必须携带上token,服务端在收到JWT后会对其进行有效性认证,例如内容有篡改,token已过期等等,如果验证通过就可以顺利的访问资源。

注意:虽然JWT允许我们携带一些基本的信息,但是建议不要带有过大信息量的数据。

JWT的使用案例

这里我主要会以一个通过JWT来实现的登录验证中间件来讲解一下我们如何在项目中使用JWT:

设置JWTKey

首先我们在config文件中设置JWTKey:

然后我们基于这个配置文件读取来读取配置:

package utils
import (
  "fmt"
  "github.com/sirupsen/logrus"
  "gopkg.in/ini.v1"
)
type Config struct {
  Server   *server   `ini:"server"`
  Database *database `ini:"database"`
}
type server struct {
  AppMode  string `ini:"AppMode"`
  HttpPort string `ini:"HttpPort"`
  JWTKey   string `ini:"JWTKey"`
}
type database struct {
  Db         string `ini:"Db"`
  DbName     string `ini:"DbName"`
  DbUser     string `ini:"DbUser"`
  DbPassWord string `ini:"DbPassWord"`
  DbHost     string `ini:"DbHost"`
  DbPort     string `ini:"DbPort"`
}
var ServerSetting = &server{
  AppMode:  "debug",
  HttpPort: ":3000",
  JWTKey:   "FengXu123",
}
var DatabaseSetting = &database{
  Db:         "mysql",
  DbName:     "goblog",
  DbUser:     "root",
  DbPassWord: "ba161754",
  DbHost:     "localhost",
  DbPort:     "3306",
}
// Config_Message
var Config_Message = &Config{
  Server:   ServerSetting,
  Database: DatabaseSetting,
}
func init() {
  filename := "config/config.ini"
  cfg, err := ini.Load(filename)
  if err != nil {
    logrus.Errorf("配置文件加载失败: %v", err)
  }
  err = cfg.MapTo(Config_Message)
  if err != nil {
    logrus.Errorf("配置文件映射失败: %v", err)
  }
  fmt.Println(Config_Message.Server.JWTKey)
  logrus.Infof("配置文件加载成功")
}

具体go-ini第三方包的使用可以参考博主以前的文章:

go语言并发实战——日志收集系统(五) 基于go-ini包读取日志收集服务的配置文件

运行程序后,控制台显示:

说明我们的配置信息就成功被加载出来了。

定义相关结构体与信息

// JWT结构体
type JWT struct {
  JWTKey []byte // JWT密钥
}
func NewJWT() *JWT { //新建JWT结构体
  return &JWT{
    JWTKey: []byte(utils.Config_Message.Server.JWTKey),
  }
}
// 自定义声明
type MyClaims struct {
  Username string `json:"username"` //这里的与gorm中声明的保持一致
  jwt.RegisteredClaims
}
// 定义相关错误信息
var (
  TokenExpired     error = errors.New("token已过期,请重新登录")
  TokenNotValidYet error = errors.New("token无效,请重新登录")
  TokenMalformed   error = errors.New("token不正确,请重新登录")
  TokenInvalid     error = errors.New("这不是一个token,请重新登录")
)

生成token

func(j *JWT)CreateToken(claims MyClaims) (string, error) {
  token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
  return token.SignedString(j.JWTKey)
}

部分函数记录

  1. NewWithClaims:
func NewWithClaims(method SigningMethod, claims Claims) *Token
  • 作用:创建一个新的token
  • 相关参数:
  • SigningMethod:所采用的加密方法
  • claims:我们所定义的声明
  1. SignedString
  • 作用:SignedString创建并返回一个完整的、已签名的JWT

解析token

// 解析token
func (j *JWT) ParseToken(tokenString string) error {
  // 解析token
  token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
    return j.JWTKey, nil
  })
  // 校验token
  if token.Valid {
    return nil
  } else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
    return TokenExpired
  } else if errors.Is(err, jwt.ErrSignatureInvalid) {
    return TokenInvalid
  } else if errors.Is(err, jwt.ErrTokenMalformed) {
    return TokenMalformed
  } else {
    return TokenNotValidYet
  }
}

实现token中间件

//jwt中间件
func JwtToken() gin.HandlerFunc {
  return func(c *gin.Context) {
    var code int
    //检验Header
    tokenHeader := c.Request.Header.Get("Authorization")
    if tokenHeader == "" {
      code = errmsg.ERROR_TOKEN_NOT_EXIST
      c.JSON(200, gin.H{
        "status":  code,
        "message": errmsg.GetErrMsg(code),
      })
      c.Abort() //拦截中间件
    }
    checkToken := strings.Split(tokenHeader, " ")
    if len(checkToken) == 0 {
      code = errmsg.ERROR_TOKEN_TYPE_WRONG
      c.JSON(200, gin.H{
        "status":  code,
        "message": errmsg.GetErrMsg(code),
      })
      c.Abort()
    }
    if len(checkToken) != 2 || checkToken[0] != "Bearer" {
      c.JSON(200, gin.H{
        "status":  code,
        "message": errmsg.GetErrMsg(code),
      })
      c.Abort()
    }
    //解析token
    j := NewJWT()
    err := j.ParseToken(checkToken[1])
    if err != nil {
      if errors.Is(err, TokenExpired) {
        c.JSON(200, gin.H{
          "status":  errmsg.ERROR_TOKEN_RUNTIME,
          "message": errmsg.GetErrMsg(errmsg.ERROR_TOKEN_RUNTIME),
        })
        c.Abort()
      } else {
        c.JSON(200, gin.H{
          "status":  errmsg.ERROR,
          "message": err.Error(),
        })
        c.Abort()
      }
    }
    c.Next()
  }
}

完整代码

package middleware
import (
  "errors"
  "gin_vue_blog/utils"
  "gin_vue_blog/utils/errmsg"
  "github.com/gin-gonic/gin"
  "github.com/golang-jwt/jwt/v4"
  "strings"
)
// JWT结构体
type JWT struct {
  JWTKey []byte // JWT密钥
}
func NewJWT() *JWT { //新建JWT结构体
  return &JWT{
    JWTKey: []byte(utils.Config_Message.Server.JWTKey),
  }
}
// 自定义声明
type MyClaims struct {
  Username string `json:"username"` //这里的与gorm中声明的保持一致
  jwt.RegisteredClaims
}
// 定义相关错误信息
var (
  TokenExpired     error = errors.New("token已过期,请重新登录")
  TokenNotValidYet error = errors.New("token无效,请重新登录")
  TokenMalformed   error = errors.New("token不正确,请重新登录")
  TokenInvalid     error = errors.New("这不是一个token,请重新登录")
)
// 生成token
func (j *JWT) CreateToken(claims MyClaims) (string, error) {
  token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
  return token.SignedString(j.JWTKey)
}
// 解析token
func (j *JWT) ParseToken(tokenString string) error {
  // 解析token
  token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
    return j.JWTKey, nil
  })
  // 校验token
  if token.Valid {
    return nil
  } else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
    return TokenExpired
  } else if errors.Is(err, jwt.ErrSignatureInvalid) {
    return TokenInvalid
  } else if errors.Is(err, jwt.ErrTokenMalformed) {
    return TokenMalformed
  } else {
    return TokenNotValidYet
  }
}
//jwt中间件
func JwtToken() gin.HandlerFunc {
  return func(c *gin.Context) {
    var code int
    //检验Header
    tokenHeader := c.Request.Header.Get("Authorization")
    if tokenHeader == "" {
      code = errmsg.ERROR_TOKEN_NOT_EXIST
      c.JSON(200, gin.H{
        "status":  code,
        "message": errmsg.GetErrMsg(code),
      })
      c.Abort() //拦截中间件
    }
    checkToken := strings.Split(tokenHeader, " ")
    if len(checkToken) == 0 {
      code = errmsg.ERROR_TOKEN_TYPE_WRONG
      c.JSON(200, gin.H{
        "status":  code,
        "message": errmsg.GetErrMsg(code),
      })
      c.Abort()
    }
    if len(checkToken) != 2 || checkToken[0] != "Bearer" {
      c.JSON(200, gin.H{
        "status":  code,
        "message": errmsg.GetErrMsg(code),
      })
      c.Abort()
    }
    //解析token
    j := NewJWT()
    err := j.ParseToken(checkToken[1])
    if err != nil {
      if errors.Is(err, TokenExpired) {
        c.JSON(200, gin.H{
          "status":  errmsg.ERROR_TOKEN_RUNTIME,
          "message": errmsg.GetErrMsg(errmsg.ERROR_TOKEN_RUNTIME),
        })
        c.Abort()
      } else {
        c.JSON(200, gin.H{
          "status":  errmsg.ERROR,
          "message": err.Error(),
        })
        c.Abort()
      }
    }
    c.Next()
  }
}

备注:拦截中间件与响应中间件的具体细节可以参考博主之前的文章:

Gin框架学习笔记(五) ——文件上传与路由中间件

拓展:签名算法的选择(仅供参考)

可用的签名算法有好几种,在使用之前应该先了解下它们之间的区别以便更好的去选择签名算法,它们之间最大的不同就是对称加密和非对称加密。

最简单的对称加密算法HSA,让任何[]byte都可以用作有效的密钥,所以计算速度稍微快一点。在生产者和消费者双方都是可以被信任的时候,对称加密算法的效率是最高的。不过由于签名和验证都使用相同的密钥,因此无法轻松的分发用于验证的密钥,毕竟签名的密钥也是同一个,签名泄露了则JWT的安全性就毫无意义。

非对称加密签名方法,例如RSA,使用不同的密钥来进行签名和验证token,这使得生成带有私钥的令牌成为可能,同时也允许任何使用公钥验证的人正常访问。

不同的签名算法所需要的密钥的类型也不同,下面给出一些常见签名算法的类型:

  • HMAC:对称加密,需要类型[]byte的值用于签名和验证。 (HS256,HS384,HS512)
  • RSA:非对称加密,需要rsa.PrivateKey类型的值用于签名,和rsa.PublicKey类型的值用于验证。(RS256,RS384,RS512)
  • ECDSA:非对称加密,需要ecdsa.PrivateKey类型的值用于签名,和ecdsa.PublicKey类型的值用于验证。(ES256,ES384,ES512)
  • EdDSA:非对称加密,需要ed25519.PrivateKey类型的值用于签名和ed25519.PublicKey 类型的值用于验证。(Ed25519)
相关文章
|
5天前
|
存储 Go API
一个go语言编码的例子
【7月更文挑战第2天】本文介绍Go语言使用Unicode字符集和UTF-8编码。Go中,`unicode/utf8`包处理编码转换,如`EncodeRune`和`DecodeRune`。`golang.org/x/text`库支持更多编码转换,如GBK到UTF-8。编码规则覆盖7位至21位的不同长度码点。
70 1
一个go语言编码的例子
|
8天前
|
JSON 算法 测试技术
在go语言中调试程序
【6月更文挑战第29天】Go语言内置`testing`包支持单元测试、基准测试和模糊测试。`go test`命令可执行测试,如`-run`选择特定测试,`-bench`运行基准测试,`-fuzz`进行模糊测试。
17 2
在go语言中调试程序
|
6天前
|
安全 Go
Go语言的iota关键字有什么用途?
**Go语言中的`iota`是常量生成器,用于在`const`声明中创建递增的常量。`iota`在每个新的`const`块重置为0,然后逐行递增,简化了枚举类型或常量序列的定义。例如,定义星期枚举:** ```markdown ```go type Weekday int const ( Sunday Weekday = iota // 0 Monday // 1 Tuesday // 2 ... ) ``` 同样,`iota`可用于定义不同组的常量,如状态码和标志位,保持各自组内的递增,提高代码可读性。
|
2天前
|
监控 搜索推荐 Go
万字详解!在 Go 语言中操作 ElasticSearch
本文档通过示例代码详细介绍了如何在Go应用中使用`olivere/elastic`库,涵盖了从连接到Elasticsearch、管理索引到执行复杂查询的整个流程。
8 0
|
6天前
|
IDE Linux Go
记录一个go语言与IDE之间的问题
【7月更文挑战第1天】本文介绍在IDE中调试Go应用可能遇到的问题。当问题与IDE的自动完成有关,可以试着使用其他编辑器如Linux的vim是否无此问题。这可以验证表明IDE可能不完全兼容最新语言版本,建议使用无自动检测工具临时解决。
22 0
|
10天前
|
编译器 Go C++
必知的技术知识:go语言快速入门教程
必知的技术知识:go语言快速入门教程
|
11天前
|
编译器 Go 开发者
|
3天前
|
数据管理 物联网 开发者
现代化后端开发中的微服务架构设计与实现
在当今快速发展的软件开发领域,微服务架构已成为构建高效、可扩展和灵活的后端系统的重要方式。本文将探讨微服务架构的设计原则、实现方法以及应用场景,帮助开发者理解如何在项目中成功应用微服务。【7月更文挑战第4天】
16 2
|
10天前
|
IDE Java 开发工具
Spring Boot:加速Java后端开发的现代化利器
在当今快速迭代的软件开发环境中,Spring Boot 已成为Java后端开发领域的首选框架。作为Spring家族的一员,它以“约定优于配置”的设计理念,极大地简化了传统Spring应用的配置和部署过程,让开发者能够更加专注于业务逻辑的实现。本文将探讨Spring Boot的核心优势,并通过一个简单的示例展示如何快速启动一个基于Spring Boot的Java Web应用。
27 1
|
5天前
|
弹性计算 运维 Kubernetes
探索后端开发的未来:微服务架构与容器化技术
在数字化时代的浪潮中,后端开发正经历着前所未有的变革。微服务架构的兴起和容器化技术的普及,不仅重新定义了软件的开发、部署和管理方式,还为后端开发带来了新的挑战和机遇。本文将深入探讨微服务架构和容器化技术如何影响后端开发的未来,通过数据支撑和逻辑推理,揭示这些技术趋势背后的科学原理和实际应用价值。

热门文章

最新文章