小工具      在线工具  汉语词典  dos游戏  css  js  c++  java

Go的Gin框架拦截器实现登录认证结合JWT实现会话记录

# Gin,golang,jwt,gin 额外说明

收录于:15天前

JWT

jwt的全称是Json web token,是一种身份验证和信息交换的工具。

授权:这是使用 JWT 最常见的场景。用户登录后,每个后续请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。

信息交换:JSON Web 令牌是在各方之间安全传输信息的好方法。

https://jwt.io/

JWT包含三个部分,标题,有效载荷,签名。因此jwt的格式为Header.Payload.Signature。如下是一个JWT生成的token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InhpYW94dSIsImV4cCI6MTY4MTUyNzcyNSwibmJmIjoxNjgxNTI3OTA1fQ.zOKqaUl2Z9BzuOIB9P0GmoHqAkHLp7O6yMy4lQ6FJ9U

标头通常由两部分组成:令牌类型 (JWT) 和使用的签名算法(例如 HMAC SHA256 或 RSA)。

{
     
"alg": "HS256",
"typ": "JWT"
}

alg 表示签名的算法,默认是 HMAC SHA256(写成 HS256)
typ 表示这个 token 的类型,类型为 “JWT”
这个JSON被Base64Url编码以形成JWT的第一部分即生成header

有效负载通常是用户信息和附加数据的声明

{
     
 "sub": "1234567890",
 "name": "xiaoxu",
 "admin": true
}

Base64Url 对有效负载进行编码以形成 JSON Web 令牌的第二部分。

签名部分主要对header和payload进行签名,防止数据被篡改。签名还需要一个密钥,该密钥仅存储在服务器上。签名算法为Header中指定的签名算法。

在这里插入图片描述

智威汤逊工作模式

在身份验证中,当用户使用其凭据成功登录时,将返回 JSON Web 令牌。每当用户想要访问受保护的路由或资源时,用户代理应该发送 JWT,通常在授权标头中使用承载模式。

Authorization: Bearer <token>

如果您通过 HTTP 标头发送 JWT 令牌,则应尽量防止它们变得太大。某些服务器不接受大于 8 KB 的头文件。

JWT认证的大致流程是:

  1. 登录认证返回token
  2. 每次访问维度时都携带令牌
  3. token过期或过期时重新登录认证

JWT在服务器端返回token的大致流程是:

  1. 实现Header的base64编码
  2. 实现 Payload 的 basse64 编码
  3. 使用签名和加密算法对Header和Payload进行加密,返回Signature,也就是token。

智威汤逊的优势

基于服务器的身份认证,基于session+cookie的会话保持技术,通过会话认证后,需要将用户的会话数据保存在内存中。随着认证用户数量的增加,内存开销也会增加;当多个CORS终端访问相同数据时,会出现禁止访问的情况;用户容易受到CSRF攻击;不利于集群部署,多集群会话内存共享实现复杂;不利于反向代理,因为代理服务器也需要会话共享。

基于Token的身份认证是无状态的。服务器直接从令牌中解析出有用的信息,不存储任何用户信息。 token文件较小,方便网络传输。

OAuth2是一个授权和认证框架,JWT是一个认证协议。

JWT规则生成Token

  1. 下载并引入jwt-go包并引入JWT
//下载依赖
go get -u github.com/dgrijalva/jwt-go

//引入依赖
import "github.com/dgrijalva/jwt-go"

  1. 构建Payload数据格式并配置有用信息
//Pyaload
type MyClaim struct {
    
	Username string `json:"username"`
	jwt.StandardClaims
}

jwt.StandardClaims是jwt包下的结构体,定义了Payload的一些标准预定义信息,用户自定义其余信息需要通过继承该类实现,即通过结构体嵌套实现。

type StandardClaims struct {
    
 Audience  string `json:"aud,omitempty"`(受众,即接受 JWT 的一方)
 ExpiresAt int64  `json:"exp,omitempty"`(所签发的JWT的过期时间)
 Id        string `json:"jti,omitempty"`(JWT的Id)
 IssuedAt  int64  `json:"iat,omitempty"`(签发时间)
 Issuer    string `json:"iss,omitempty"`(JWT的签发者)
 NotBefore int64  `json:"nbf,omitempty"`(JWT的生效时间)
 Subject   string `json:"sub,omitempty"`(主题)
}

在自定义负载仅仅添加了Username一项。

  1. 根据 JWT 的 Header、Payload 和加密算法生成 JSON Web Token
//定义签名
// secret签名
var mySignatureSecret []byte = []byte("!@#qwe")



//实例化负载payload
c := MyClaim{
    
	Username: "xiaoxu",
	StandardClaims: jwt.StandardClaims{
    
		NotBefore: time.Now().Unix() + 60,  //JWT的生效时间
		ExpiresAt: time.Now().Unix() - 120, //签发JWT的过期时间
	},
}


//生成Header
//Header一般都是如下的json使用默认即可不用专门实例化,因此直接默认即可
/* { "alg": "HS256", "typ": "JWT" } */


//生成token

//返回未加密signature
sensitiveToken := jwt.NewWithClaims(jwt.SigningMethodHS256, c)

//利用secret签名对token加密
signature, err := sensitiveToken.SignedString(mySignatureKey)

通过上述步骤得到signature即最终的token。所有代码如下:

package main

import (
	"fmt"
	"github.com/dgrijalva/jwt-go"
	"time"
)

//Pyaload

type MyClaim struct {
    
	Username string `json:"username"`
	jwt.StandardClaims
}

// secret签名
var mySignatureKey []byte = []byte("!@#qwe")

func main() {
    
	/* engine := gin.Default() engine.GET("/", func(context *gin.Context) { context.String(200, "Hello World") }) engine.Run("127.0.0.1:80") */

	//实例化负载payload
	c := MyClaim{
    
		Username: "xiaoxu",
		StandardClaims: jwt.StandardClaims{
    
			NotBefore: time.Now().Unix() + 60,  //JWT的生效时间
			ExpiresAt: time.Now().Unix() - 120, //签发JWT的过期时间
		},
	}

	//Header就是默认也是不用专门实例化
	/* { "alg": "HS256", "typ": "JWT" } */

	//返回未加密signature
	sensitiveToken := jwt.NewWithClaims(jwt.SigningMethodHS256, c)

	//利用secret签名对token加密
	signature, err := sensitiveToken.SignedString(mySignatureKey)
	if err != nil {
    
		panic(err)
	}
	fmt.Println(signature)
}

生成代币

在这里插入图片描述

解密Token

//解密Token
token1, _ := jwt.ParseWithClaims(signature, &MyClaim{
    }, func(token *jwt.Token) (interface{
    }, error) {
    
	return mySignatureKey, nil
})

fmt.Println(token1)
fmt.Println(token1.Claims)
fmt.Println(token1.Claims.(*MyClaim).Username)

解密后得到jwt.Token对象,从该对象可以获取Header,Payload,Signature(claims)等信息。

在这里插入图片描述
jwt.ParseWithClaims()方法用于解密Token,第一个参数为生成的token,第二个参数为自定义Payload的结构体类型,第三个参数为一个方法返回签名。

解密后得到的jwt.Token对象一般包含以下几个变量。
在这里插入图片描述

成员变量 描述
索赔 jwt表头和负载加密生成的不完整Token,此时还未对签名加密,也就是回到之前的sensitiveToken := jwt.NewWithClaims(jwt.SigningMethodHS256, c)此步骤。一般通过断言转化为自定义Payload结构体
SignedString(密钥接口{}) 该方法是传入一个签名,并对签名进行加密,生成完整的token。
标头 返回页眉
方法 返回加密算法
有效的 验证token是否过期

另外需要注意的是,定义的负载必须是公共的即结构体名和成员名首字母都必须大写,不然无法将为加密的*TokenClaims类型断言成自定义的结构体类型。如下,如果将字段改为小写在同包下的测试包都无法生成和解析Token。

在这里插入图片描述

拦截器

Go的拦截器可以在方法执行之前和之后执行拦截器方法,并通过拦截器释放或拦截该方法。在Gin框架中,路由方法有多种,因此在执行路由后的逻辑时,可以传入任意数量的拦截器方法作为参数,实现对不逻辑逻辑的释放和控制。

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes

如上所示,gin.Default().GET()方法中,第一个参数为路由地址,其后为若干拦截器方法,对于拦截器方法type HandlerFunc func(*Context)其实就是实现了gin.context的方法的重命名函数。

因此任意以gin.context为参数的方法均为拦截器方法。

//定义权限认证中间件

func Certification() gin.HandlerFunc {
    
	return func(context *gin.Context) {
    
		context.Set("username", "xiaoxu")
		if context.PostForm("username") != "xiaoxu" {
    
			context.String(200, "用户名错误!")
			context.Abort()
		} else {
    
			context.Next()
		}
	}
}
//gin的路由调用拦截器

import "github.com/gin-gonic/gin"

func main() {
    

	engine := gin.Default()
	engine.GET("/", Certification(), func(context *gin.Context) {
    
		context.String(200, "Hello World")
	})

	engine.GET("/index", Certification(), func(context *gin.Context) {
    
		context.String(200, "welcome index !")
	})

	engine.GET("/test", Certification(), func(context *gin.Context) {
    
		context.String(200, "welcome test")
	})
	engine.Run("127.0.0.1:80")
}

当请求地址携带正确的用户名和密码时,会出现“用户名错误”字样!

在这里插入图片描述

当携带正确的用户名时,任意路径都可以访问

在这里插入图片描述

注意表单类型为form-datatext类型,而x-www-form-urlencoded时key-value类型。

上面的案例实现了拦截器对资源的拦截,然后实现数据查询模拟用户登录,使用SQL数据库,ORM框架是gorm。

//下载mysql驱动
go get -u gorm.io/driver/mysql

//gorm框架
go get -u gorm.io/gorm

//引入依赖
import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)
//构建如下的数据库表

mysql> select * from user;
+----+---------+----------+----------+
| id | user    | password | role     |
+----+---------+----------+----------+
|  1 | xiaoxu  | 123      | admin    |
|  2 | zhansan | 123      | personal |
|  3 | lisi    | 123      | role     |
+----+---------+----------+----------+
3 rows in set (0.01 sec)

//结构体映射数据库表
//定义数据表映射结构体

type User struct {
    
	Id       int    `json:"id"`
	User     string `json:"user"`
	Password string `json:"password"`
	Role     string `json:"role"`
}
//数据库驱动程序返回数据库操作对象

//数据库驱动
func ConnMysql() *gorm.DB {
    
	datasource := "root:root@tcp(127.0.0.1:3306)/user?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(datasource), &gorm.Config{
    })
	if err != nil {
    
		fmt.Println("error connect mysql", err)
	}
	return db
}
//主函数映入数据库对象(略)
import "xxx/../db"


//重构拦截器,添加查询数据库操作
func Certification() gin.HandlerFunc {
    
	return func(context *gin.Context) {
    
		//fmt.Println(context.PostForm("username"))
		var user db.User
		db.ConnMysql().Where("user = ?", context.PostForm("username")).Find(&user)
		//fmt.Println("user----------", user)

		//判断用户是否存在
		if user.User == "" {
    
			context.String(200, "用户名不存在!")
			context.Abort()
			//判断用户密码
		} else if user.Password != context.PostForm("password") {
    
			context.String(200, "用户名或密码错误!")
			context.Abort()
		} else {
    
			//fmt.Println("password-----------", user.Password)
			context.Next()
		}

	}
}

效果如下

请添加图片描述

Gin结合JWT实现认证

上一节已经实现了登录认证,遇到了新的问题。如果多个用户在同一时间间隔内登录系统,例如用户A登录自己的CSDN账号,发布了自己的文章。两个用户 B 和 C 在该时间间隔内登录。发表的文章如何识别为用户A?

这就需要用到会话技术。在之前的系统中,采用了Session技术和服务器端会话技术来在服务器端记录用户信息。每个用户都有自己独立的身份。

在session中存储用户信息,比如用户A的用户名,那么当用户A发布文章时,用户A的用户名和文章一起存储在数据库中,这样就可以区分该文章是属于用户A的但随着用户数量的增加,一段时间内访问用户的突然增加会增加服务的负载。

因此,JWT这种用于用户会话记录的新技术应运而生。在第一节中,JWT完美取代了SEESION,成为主流的身份验证和会话记录技术。

拦截器章节实现了用户登录,本章节将介绍如何利用JWT实现会话记录。

//下载引入go-jwt依赖

//下载依赖
go get -u github.com/dgrijalva/jwt-go

//引入依赖
import "github.com/dgrijalva/jwt-go"

项目结构如下

在这里插入图片描述

//JWTConfig包的源码

import (
	"github.com/dgrijalva/jwt-go"
	"time"
)

// MyPayload 定义负载继承jwt的标准负载
type MyPayload struct {
    
	Username string
	jwt.StandardClaims
}

// 定义secret签名
var signatureKey []byte = []byte("!@#qwe")

// MakeUserToken 生成加密token
func MakeUserToken(user string) string {
    
	//传入用户信息生成负载实例
	payload := MyPayload{
    
		Username: user,
		StandardClaims: jwt.StandardClaims{
    
			NotBefore: time.Now().Unix() + 10,
			ExpiresAt: time.Now().Unix() - 10,
		},
	}

	//生成加密Signature
	token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, payload).SignedString(signatureKey)
	if err != nil {
    
		panic(err)
	}
	return token
}

// 解密token
func ParserUserToken(token string) string {
    
	//解密后jwt.Token对象,从该对象可以获取Header,Payload,Signature(claims)等信息
	unsafeToken, _ := jwt.ParseWithClaims(token, &MyPayload{
    }, func(token *jwt.Token) (interface{
    }, error) {
    
		return signatureKey, nil
	})
	user := unsafeToken.Claims.(*MyPayload).Username
	return user
}

引入之后应该可以使用JWTConfig包下的生成token和解析token的方法。
在这里插入图片描述

由于会话记录也是全局的,因此在全局拦截器中引入jwt,嵌入jwt认证,实现登录后的会话记录。

实现代码如下:

  1. 用于生成令牌和解析令牌的jwt代码
package jwtconfig

import (
	"github.com/dgrijalva/jwt-go"
	"time"
)

// MyPayload 定义负载继承jwt的标准负载
type MyPayload struct {
    
	Username string
	jwt.StandardClaims
}

// 定义secret签名
var signatureKey []byte = []byte("!@#qwe")

// MakeUserToken 生成加密token
func MakeUserToken(user string) string {
    
	//传入用户信息生成负载实例
	payload := MyPayload{
    
		Username: user,
		StandardClaims: jwt.StandardClaims{
    
			NotBefore: time.Now().Unix() + 10,
			ExpiresAt: time.Now().Unix() - 10,
		},
	}

	//生成加密Signature
	token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, payload).SignedString(signatureKey)
	if err != nil {
    
		panic(err)
	}
	return token
}

// 解密token
func ParserUserToken(token string) (*MyPayload, error) {
    
	//解密后jwt.Token对象,从该对象可以获取Header,Payload,Signature(claims)等信息
	unsafeToken, err1 := jwt.ParseWithClaims(token, &MyPayload{
    }, func(token *jwt.Token) (interface{
    }, error) {
    
		return signatureKey, nil
	})

	//将负载转化为结构体
	claims, ok := unsafeToken.Claims.(*MyPayload)

	if ok && unsafeToken.Valid {
    
		return claims, nil
	} else {
    
		return claims, err1
	}

	/* //验证token是否有效 if unsafeToken.Valid { //错误判断并返回错误信息 if err1 != nil { return "未携带有效token", unsafeToken.Claims } else if ve, ok := err1.(*jwt.ValidationError); ok { if ve.Errors&jwt.ValidationErrorMalformed != 0 { return "无效token", nil } else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 { return "token已过期", nil } else { return "", nil } } } */

}
  1. 路由jwt验证码
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"go-jwt/db"
	"go-jwt/jwtconfig"
)

func main() {
    

	engine := gin.Default()
	engine.POST("/login", Certification(), func(context *gin.Context) {
    
		user := context.PostForm("user")
		fmt.Println(jwtconfig.MakeUserToken(user))
		context.String(200, "welcome register")
	})
	engine.GET("/", JWTHandler(), func(context *gin.Context) {
    
		context.String(200, "Hello World")
	})

	engine.GET("/test1", JWTHandler(), func(context *gin.Context) {
    
		context.String(200, "welcome index !")
	})

	engine.GET("/test2", JWTHandler(), func(context *gin.Context) {
    
		context.String(200, "welcome test")
	})
	engine.Run("127.0.0.1:80")
}

//定义权限认证中间件

func Certification() gin.HandlerFunc {
    
	return func(context *gin.Context) {
    
		//fmt.Println(context.PostForm("username"))
		var user db.User
		db.ConnMysql().Where("user = ?", context.PostForm("username")).Find(&user)
		//fmt.Println("user----------", user)
		//判断用户是否存在
		if user.User == "" {
    
			context.String(500, "用户名不存在!")
			context.Abort()
			//判断用户密码
		} else if user.Password != context.PostForm("password") {
    
			context.String(500, "用户名或密码错误!")
			context.Abort()
		} else {
    
			//fmt.Println("password-----------", user.Password)
			context.Next()
		}

	}
}

//jwt拦截器

func JWTHandler() gin.HandlerFunc {
    
	return func(context *gin.Context) {
    

		//引入jwt实现登录后的会话记录,登录会话发生登录完成之后
		//header获取token
		token := context.Request.Header.Get("token")
		if token == "" {
    
			context.String(302, "请求未携带token无法访问!")
			context.Abort()
		}
		//解析token
		claims, err := jwtconfig.ParserUserToken(token)
		if claims == nil || err != nil {
    
			context.String(401, "未携带有效token或已过期")
			context.Abort()
		} else {
    
			//context.Set("user", claims.Username)
			context.Next()

		}
	}
}

依据JWT规则加密解密Token不在赘述,这里主要阐明JWT会话逻辑,首先定义了JWTHandler()jwt拦截器,主要实现步骤是,从请求头获取Token(当然也可以放在http协议的其他位置),然后token判空,解密Token判断token是否过期是否有效,有效就放行。(这里只实现了简单的验证,官网又详细的验证)

需要注意的是,登录页面验证用户名身份,而其他路由则实现jwt会话验证。

案例演示如下:

请添加图片描述

如下是控制太的返回
在这里插入图片描述

在验证token之前,会返回302错误状态码。 http header携带token后,返回200状态码。

. . .

相关推荐

额外说明

MYSQL性能调优08_事务及其ACID属性、脏读、不可重复读、幻读、隔离级别、行锁、表锁、读锁、写锁、间隙锁、临时键锁

文章目录 ①. 事务及其ACID属性 ②. 脏读、不可重复读、幻读 ③. 隔离级别 ④. 锁分类 表索、行锁、读锁、写锁 ⑤. 表锁 ⑥. 行锁 ⑦. 间隙锁(Gap Lock) ⑧. 临键锁(Next-key Locks) ①. 事务及其ACID属性

额外说明

Android Studio 常见问题 与 操作指南

安装 自动选择好了 jre 每次安装下载 一 、常见问题 1. Error while waiting for device: The emulator process for AVD Pixel_4_XL_API_30 was killed. Andr

额外说明

【100个 Unity实用技能】 | Unity中Text文本框 和 InputField文本输入框 内容换行问题

- 博客主页:https://xiaoy.blog.csdn.net - 本文由 呆呆敲代码的小Y 原创,首发于 CSDN- - 学习专栏推荐:Unity系统学习专栏 - 游戏制作专栏推荐:游戏制作 -Unity实战100例专栏推荐:Unity 实战10

额外说明

Python-Matplotlib可视化(1)——一文详解常见统计图的绘制

Python-Matplotlib可视化(1)——一文详解常见统计图的绘制 matplotlib库 曲线图 曲线图的绘制 结合Numpy库,绘制曲线图 绘制多曲线图 读取数据文件绘制曲线图 散点图 条形图 单组条形图 垂直条形图 水平条形图 多组条形图

额外说明

docker部署grafana loki日志系统

Loki日志系统简介 Loki是 Grafana Labs 团队发布的开源项目,是一个水平可扩展,高可用性,多租户的日志聚合系统。 项目受 Prometheus 启发,官方的介绍就是:Like Prometheus, but for logs.,类似于

额外说明

【HTML&&CSS】页面版块布局(亲身实践,帮你避坑)

-音乐分享(点击链接可以听哦) 带我去很远地方 - 黄霄雲 ⭐⭐⭐----️‍--️‍--️‍- 目录 ⭐先说大招 案例1  ​编辑 原因: 案例2 法一: 法二: ⭐先说大招     案例1  平时学的时候没有在意这一点,感觉很简单,但是真正实践的时候

额外说明

Spring Boot项目之伪Masked Field弱点解决

伪Masked Field 指的是什么? Masked Field 的翻译是“掩码字段”, 是FindBugs 定义的一种代码弱点。 这里的“伪Masked Field” 指的不是真正的“掩码字段”漏洞,但是却被代码扫描工具扫描出来具有该类型的弱点。 S

额外说明

JavaScript 数组方法深入解析:some() 方法的用法与示例

Array.prototype.some() 是 JavaScript 中用于数组的方法之一,它用于检查数组中是否至少有一个元素满足指定的条件。这个方法会遍历数组的每个元素,直到找到一个满足条件的元素,然后立即返回 true。如果没有找到满足条件的元素,

额外说明

一文读懂“什么是Web 1.0,Web 2.0,Web 3.0?”

文章目录 1. Web3.0 简述与理解 2. Web 1.0 3. Web 2.0 4. Web 3.0 5. 结语 最近的投融资中Web 3.0的趋势直线上升,那么受到众多资本喜爱的Web 3.0到底是什么呢? Web 3.0的应用范围很广,大家所在

额外说明

计算机英语课程资源

计算机英语课程资源 第一部分:讲课笔记 一、2017年讲课笔记 1、2017年计算机专业英语讲课笔记(1) 2、2017年计算机专业英语讲课笔记(2) 3、2017年计算机专业英语讲课笔记(3) 4、2017年计算机专业英语讲课笔记(4) 5、2017年

ads via 小工具