当前位置: 首页 > news >正文

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 utilsimport ("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

// 解析tokenfunc (j *JWT) ParseToken(tokenString string) error {// 解析tokentoken, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {return j.JWTKey, nil})// 校验tokenif 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//检验HeadertokenHeader := c.Request.Header.Get("Authorization")if tokenHeader == "" {code = errmsg.ERROR_TOKEN_NOT_EXISTc.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_WRONGc.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()}//解析tokenj := 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 middlewareimport ("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)
}// 解析tokenfunc (j *JWT) ParseToken(tokenString string) error {// 解析tokentoken, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {return j.JWTKey, nil})// 校验tokenif 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//检验HeadertokenHeader := c.Request.Header.Get("Authorization")if tokenHeader == "" {code = errmsg.ERROR_TOKEN_NOT_EXISTc.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_WRONGc.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()}//解析tokenj := 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)

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 项目:基于httplib/消息队列负载均衡式在线OJ
  • 【计算机网络】个人学习笔记——第四章 网络层:CIDR编址IP地址子网掩码ICMP路由聚合与子网划分ARP协议
  • 工业无线通信解决方案,企业在进行智能化升级改造
  • vue3模板语法总结
  • ios 获取图片的一部分区域
  • 01 Linux网络设置
  • 调整SinoDB数据库日志模式
  • 十种排序方法
  • 架构设计-用户信息及用户相关的密码信息设计
  • 目前常用的后端技术
  • 课时152:项目发布_手工发布_方案解读
  • 每天五分钟深度学习pytorch:pytorch中的广播机制是什么?
  • 将字符串转换为Python数据类型
  • 动态规划:买卖股票的最佳时机含手续费
  • Oracle EBS AP发票创建会计科目错误:子分类帐日记帐分录未按输入币种进行平衡
  • [ JavaScript ] 数据结构与算法 —— 链表
  • 【个人向】《HTTP图解》阅后小结
  • 【跃迁之路】【463天】刻意练习系列222(2018.05.14)
  • CAP理论的例子讲解
  • HashMap ConcurrentHashMap
  • js正则,这点儿就够用了
  • js中forEach回调同异步问题
  • SAP云平台里Global Account和Sub Account的关系
  • SpringCloud集成分布式事务LCN (一)
  • storm drpc实例
  • Tornado学习笔记(1)
  • 发布国内首个无服务器容器服务,运维效率从未如此高效
  • 函数式编程与面向对象编程[4]:Scala的类型关联Type Alias
  • 回顾2016
  • 面试遇到的一些题
  • 排序算法之--选择排序
  • 微信支付JSAPI,实测!终极方案
  • 为物联网而生:高性能时间序列数据库HiTSDB商业化首发!
  • 智能合约开发环境搭建及Hello World合约
  • - 转 Ext2.0 form使用实例
  • 深度学习之轻量级神经网络在TWS蓝牙音频处理器上的部署
  • 翻译 | The Principles of OOD 面向对象设计原则
  • #define,static,const,三种常量的区别
  • (10)工业界推荐系统-小红书推荐场景及内部实践【排序模型的特征】
  • (2)STM32单片机上位机
  • (arch)linux 转换文件编码格式
  • (C#)if (this == null)?你在逗我,this 怎么可能为 null!用 IL 编译和反编译看穿一切
  • (Charles)如何抓取手机http的报文
  • (env: Windows,mp,1.06.2308310; lib: 3.2.4) uniapp微信小程序
  • (vue)el-checkbox 实现展示区分 label 和 value(展示值与选中获取值需不同)
  • (定时器/计数器)中断系统(详解与使用)
  • (二)换源+apt-get基础配置+搜狗拼音
  • (三)uboot源码分析
  • (十五)devops持续集成开发——jenkins流水线构建策略配置及触发器的使用
  • (原创) cocos2dx使用Curl连接网络(客户端)
  • (转)Linq学习笔记
  • (转)Oracle存储过程编写经验和优化措施
  • (转)Unity3DUnity3D在android下调试
  • (转)可以带来幸福的一本书
  • (转)全文检索技术学习(三)——Lucene支持中文分词