【Golang】Gin框架中如何使用JWT来实现登录认证
✨✨ 欢迎大家来到景天科技苑✨✨
🎈🎈 养成好习惯,先赞后看哦~🎈🎈
🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Python全栈,Golang开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。所属的专栏:Go语言开发零基础到高阶实战
景天的主页:景天科技苑
文章目录
- Gin框架JWT登录认证
- 1. token、cookie、session的区别
- 2. 什么是JWT
- 3. 什么时候用JWT
- 4. gin框架封装jwt
- 1. 生成token
- 2. 解析token
Gin框架JWT登录认证
背景: 在如今前后端分离开发的大环境中,我们需要解决一些登陆,后期身份认证以及鉴权相关的事情,通常的方案就是采用请求头携带token的方式进行实现。
在开始学习JWT之前,我们可以先了解下早期的几种方案。
1. token、cookie、session的区别
Cookie
Cookie总是保存在客户端中,按在客户端中的存储位置,可分为内存Cookie
和硬盘Cookie
。
内存Cookie由浏览器维护,保存在内存中,浏览器关闭后就消失了,其存在时间是短暂的。硬盘Cookie保存在硬盘里,有一个过期时间,除非用户手工清理或到了过期时间,硬盘Cookie不会被删除,其存在时间是长期的。所以,按存在时间,可分为非持久Cookie和持久Cookie
。
cookie 是一个非常具体的东西,指的就是浏览器里面能永久存储的一种数据,仅仅是浏览器实现的一种数据存储功能。
cookie由服务器生成,发送给浏览器
,浏览器把cookie以key-value形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该cookie发送给服务器。由于cookie是存在客户端上的,所以浏览器加入了一些限制确保cookie不会被恶意使用,同时不会占据太多磁盘空间,所以每个域的cookie数量是有限的。
Session
Session字面意思是会话,主要用来标识自己的身份。比如在无状态的api服务在多次请求数据库时,如何知道是同一个用户,这个就可以通过session的机制,服务器要知道当前发请求给自己的是谁
为了区分客户端请求,服务端会给具体的客户端生成身份标识session
,然后客户端每次向服务器发请求的时候,都带上这个“身份标识”,服务器就知道这个请求来自于谁了。
至于客户端如何保存该标识,可以有很多方式,对于浏览器而言,一般都是使用cookie
的方式
服务器使用session把用户信息临时保存了服务器上,用户离开网站就会销毁,这种凭证存储方式相对于cookie来说更加安全,但是session会有一个缺陷: 如果web服务器做了负载均衡,那么下一个操作请求到了另一台服务器的时候session会丢失。
因此,通常企业里会使用redis,memcached
缓存中间件来实现session的共享,此时web服务器就是一个完全无状态的存在,所有的用户凭证可以通过共享session的方式存取,当前session的过期和销毁机制需要用户做控制。
Token
token的意思是“令牌”,是用户身份的验证方式,最简单的token组成: uid(用户唯一标识)
+time(当前时间戳)
+sign(签名,由token的前几位+盐以哈希算法压缩成一定长度的十六进制字符串)
,同时还可以将不变的参数也放进token
今天我们主要想讲的就是Json Web Token
,也就是本篇的主题:JWT
2. 什么是JWT
JWT: JSON Web Token,是一种用于身份验证和授权的开放标准,JWT可以在网络应用间安全的传输。JWT由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature)
JWT具有可扩展性、简单、轻量级、跨语言等优点,是前后端分离框架中最常用的验证方式。JWT工作流程大致如下:
1.当用户成功登录后,服务器会生成一个JWT并返回给客户端
2.客户端将JWT储存在本地
3.之后每次向服务器请求时都会在请求头中携带JWT
4.服务器会验证JWT的合法性,并根据其中的信息判断用户的身份和权限,从而决定是否允许用户访问请求的资源
JWT Token组成部分
- header: 用来指定使用的算法alg(HMAC HS256 RS256)和token类型typ(如JWT)
- payload: 包含声明(要求),声明通常是用户信息或其他数据的声明,比如用户id,名称,邮箱等. 声明可分为三种: registered,public,private
- signature: 用来保证JWT的真实性,可以使用不同的算法
header
{
“alg”: “HS256”,
“typ”: “JWT”
}
对上面的json进行base64编码即可得到JWT的第一个部分
payload
载荷(Payload)用来表示需要传递的数据,例如用户ID、权限信息等,
包含声明(claims),即用户的相关信息。这些信息可以是公开的,也可以是私有的,但应避免放入敏感信息,因为该部分可以被解码查看。载荷中的声明可以验证,但不加密。
常用的字段如下:
Issuer:发行人,缩写iss
ExpiresAt:过期时间,exp
Subject:主题信息,sub
NotBefore:在此时间之前不可以用,nbf
IssuedAt:发布时间,iat
ID:JWT的ID,jti
- registered claims: 预定义的声明,通常会放置一些预定义字段,比如过期时间,主题等(iss:issuer,exp:expiration time,sub:subject,aud:audience)
- public claims: 可以设置公开定义的字段
- private claims: 用于统一使用他们的各方之间的共享信息
{
“sub”: “xxx-api”,
“name”: “bgbiao.top”,
“admin”: true
}
对payload部分的json进行base64编码后即可得到JWT的第二个部分
注意:
不要在header和payload中放置敏感信息,除非信息本身已经做过脱敏处理
signature
为了得到签名部分,必须有编码过的header和payload,以及一个秘钥,签名算法使用header中指定的那个,然后对其进行签名即可
Signature = HMAC SHA256(base64UrlEncode(header)+“.”+base64UrlEncode(payload),secret)
签名是用于验证消息在传递过程中有没有被更改
,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
JWT Token: base64(header).base64(payload).Signature
jwt官网:https://jwt.io
下图就是一个典型的jwt-token的组成部分。
3. 什么时候用JWT
- Authorization(授权): 典型场景,用户请求的token中包含了该令牌允许的路由,服务和资源。单点登录其实就是现在广泛使用JWT的一个特性
- Information Exchange(信息交换): 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式.因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
JWT的工作流程
基于Token的身份认证是无状态的,服务器或者session中不会存储任何用户信息.(很好的解决了共享session的问题)
- 用户携带用户名和密码请求获取token(接口数据中可使用appId,appKey等)
- 服务端校验用户凭证,并返回用户或客户端一个Token
- 客户端存储token,并在请求头中携带Token
- 服务端校验token并返回数据
- 随后客户端的每次请求都需要使用token
- token应该放在header中
所以,基本上整个过程分为两个阶段,第一个阶段,客户端向服务端获取token,第二阶段,客户端带着该token去请求相关的资源.
通常比较重要的是,服务端如何根据指定的规则进行token的生成。
在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token将会被返回。
此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。
一般而言,你保存令牌的时候不应该超过你所需要它的时间。
无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,通常放在Authorization header中,用Bearer schema: Authorization: Bearer <token>
服务器上的受保护的路由将会检查Authorization header中的JWT是否有效,如果有效,则用户可以访问受保护的资源。如果JWT包含足够多的必需的数据,那么就可以减少对某些操作的数据库查询的需要,尽管可能并不总是如此。
如果token是在授权头(Authorization header)中发送的,那么跨源资源共享(CORS)将不会成为问题,因为它不使用cookie.
- 客户端向授权接口请求授权
- 服务端授权后返回一个access token给客户端
- 客户端使用access token访问受保护的资源
4. gin框架封装jwt
我们在go官方提供的包里面搜jwt https://pkg.go.dev/
我们使用第一个最常用的
下载
go get -u github.com/golang-jwt/jwt/v5
jwt的功能很多,我们不用每个都搞清楚,目前只需要把examples里面的就可以了
我们先生成一个token,然后再去解析这个token
我们使用可以自定义参数的
1. 生成token
package jwtutilimport ("github.com/golang-jwt/jwt/v5""jingtian/myproject/config""time"
)// 这种不能用段变量方式创建
var mySigningKey = []byte(config.JwtSecretKey)// MyCustomClaims 1.自定义声明类型
type MyCustomClaims struct {Username string `json:"username"`jwt.RegisteredClaims
}// GenToken 2. 封装生成token的函数
// 根据官方定义,返回一个token和error
func GenToken(username string) (string, error) {// Create claims with multiple fields populatedclaims := MyCustomClaims{username, //根据用户名来动态生成jwt.RegisteredClaims{// A usual scenario is to set the expiration time relative to the current timeExpiresAt: jwt.NewNumericDate(time.Now().Add(config.TokenExpire)), //过期时间,是个可变参数IssuedAt: jwt.NewNumericDate(time.Now()),NotBefore: jwt.NewNumericDate(time.Now()),Issuer: "jingtian",Subject: "myjwt",},}//生成tokentoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)ss, err := token.SignedString(mySigningKey)return ss, err
}
然后,在登录的地方调用,先登录,用户名和密码是对的情况下。生成token
2. 解析token
看下官网用法
我们使用第一个Custom
我们在config.go里面封装成函数
// ParseToken 3.解析token
func ParseToken(tokenString string) (*MyCustomClaims, error) {token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {// byte里面改成我们设置的keyreturn []byte(config.JwtSecretKey), nil})if err != nil {//fmt.Println("解析token失败", err.Error())fields := map[string]interface{}{"错误原因": err.Error(),}logs.Error(fields, "解析token失败")return nil, err} else if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {//说明token合法//fmt.Println(claims.Username, claims.RegisteredClaims.Issuer)return claims, nil} else {logs.Error(nil, "token不合法")return nil, err}}
在main里面调用,测试
//验证token是否合法
claims, tokenerr := jwtutil.ParseToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imppbmd0aWFuIiwiaXNzIjoiamluZ3RpYW4iLCJzdWIiOiJteWp3dCIsImV4cCI6MTczMDM0Mjk3MiwibmJmIjoxNzMwMzQyODUyLCJpYXQiOjE3MzAzNDI4NTJ9.aqBEft2N1zkOISfQ-b1VvBDRnyhMiPZ17Ct-r0sNvgU")if tokenerr != nil {fmt.Println("token不合法: ", tokenerr)
} else {fmt.Println("token合法:", claims)
}
使用合法的token验证
我们设置的token过期时间是2分钟,过两分钟再验证
可以看到token不合法,已过期
登录登出代码
我们在router层写路由信息
package authimport ("github.com/gin-gonic/gin""jingtian/myproject/controllers/auth"
)// 实现登录接口
func login(authGroup *gin.RouterGroup) {//具体逻辑写到控制器controller里面authGroup.POST("/login", auth.Login)
}// 实现登出接口
func loginout(authGroup *gin.RouterGroup) {authGroup.GET("/loginout", auth.Loginout)
}// RegisterSubRouter 认证子路由
func RegisterSubRouter(g *gin.RouterGroup) {//配置登录功能路由策略authGroup := g.Group("/auth")login(authGroup)loginout(authGroup)}
在controllers.go里面写具体的登录登出逻辑
package authimport ("github.com/gin-gonic/gin""jingtian/myproject/utils/logs""net/http"
)// UserInfo 创建结构体,绑定用户信息
type UserInfo struct {Username string `json:"username"`Password string `json:"password"`
}// Login 登录逻辑
func Login(c *gin.Context) {//1.获取前端传来的用户信息var user UserInfo//绑定结构体 ShouldBing绑定,可以根据结构体中的标签来 确定请求的content-type类型if err := c.ShouldBind(&user); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}logs.Debug(map[string]interface{}{"用户名": user.Username,"密码": user.Password,}, "开始验证用户登录信息")}// Loginout 登出
func Loginout(c *gin.Context) {//如果我们将token存到了redis里面,需要做清除逻辑,保存到内存,只需要前端把存到本地的token删掉即可c.JSON(http.StatusOK, gin.H{"code": 200,"msg": "success",})logs.Debug(nil, "退出成功")
}
在routers.go里面调用
在main.go里面调用
运行,postman测试登录接口
拿到数据
测试登出接口
登录验证
package authimport ("github.com/gin-gonic/gin""jingtian/myproject/utils/jwtutil""jingtian/myproject/utils/logs""net/http"
)// UserInfo 创建结构体,绑定用户信息
type UserInfo struct {Username string `json:"username"`Password string `json:"password"`
}// Login 登录逻辑
func Login(c *gin.Context) {//1.获取前端传来的用户信息var user UserInfo//绑定结构体 ShouldBing绑定,可以根据结构体中的标签来 确定请求的content-type类型if err := c.ShouldBind(&user); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}logs.Debug(map[string]interface{}{"用户名": user.Username,"密码": user.Password,}, "开始验证用户登录信息")//登录成功之后开始验证,验证通过生成token//模拟从数据库中查询用户名和密码if user.Username == "jingtian" && user.Password == "123456" {logs.Info(nil, "用户名密码正确")//生成tokenss, err := jwtutil.GenToken(user.Username)if err != nil {logs.Error(map[string]interface{}{"用户名": user.Username,"错误信息": err.Error(),}, "用户名密码正确,生成token失败")c.JSON(http.StatusOK, gin.H{"error": err.Error(),"status": 400,})return}logs.Info(nil, "用户名密码正确,生成token成功")//将token返回给前端data := make(map[string]interface{})data["token"] = ssc.JSON(http.StatusOK, gin.H{"status": 200,"data": data,"msg": "登录成功",})return} else {c.JSON(http.StatusOK, gin.H{"status": 400,"msg": "用户名或密码不正确",})return}}// Loginout 登出
func Loginout(c *gin.Context) {//如果我们将token存到了redis里面,需要做清除逻辑,保存到内存,只需要前端把存到本地的token删掉即可c.JSON(http.StatusOK, gin.H{"code": 200,"msg": "success",})logs.Debug(nil, "退出成功")
}
用户名和密码都正确,返回token
当用户名或密码不正确,拿不到token
登录验证成功后,前端在访问其他接口的时候,都需要验证是否携带正确的token
此时,我们需要通过中间件来验证,除了登录和登出的接口,其他接口都需要验证
// Package middlewares 中间件层 配置中间件
package middlewaresimport ("fmt""github.com/gin-gonic/gin""jingtian/myproject/utils/jwtutil""jingtian/myproject/utils/logs""net/http"
)// CheckToken 校验jwt token
func CheckToken(c *gin.Context) {//验证token是否合法,除了login和loginout之外的请求,都要验证token是否合法//获取请求路径,c.FullPath()获取请求群路径 这个也可以c.Request.URL.Path//requestUrl := c.FullPath()requestUrl := c.Request.URL.Path//requestUrl := c.FullPath()logs.Debug(map[string]interface{}{"url": requestUrl,}, "获取的请求路径")//我们可以做下判断,当请求路径不是登录或者登出的路径时,就做token校验if requestUrl == "/api/auth/login" || requestUrl == "/api/auth/loginout" {c.Next()} else {//其他接口需要验证合法性//token一般会存放在请求头Header中的 Authorization字段中//先获取请求头中是否包含该字段//tokenString := c.Request.Header.Get("Authorization")tokenString := c.GetHeader("Authorization")if tokenString == "" {c.JSON(http.StatusOK, gin.H{"code": http.StatusUnauthorized,"msg": "请求没有携带token,请登录后在尝试",})c.Abort()} else {claims, tokenerr := jwtutil.ParseToken(tokenString)if tokenerr != nil {fmt.Println("token不合法: ", tokenerr)c.JSON(http.StatusOK, gin.H{"code": http.StatusUnauthorized,"msg": "token不合法",})c.Abort()} else {//验证通过的话,把claims放在Context里面c.Set("claims", claims)//其他的逻辑里面,如果需要获取claims值,可以使用c.Get("claims")c.Next()fmt.Println("token合法:", claims)}}}}
正常的登录登出,都不验证token
登录生成token
其他请求,不带Authorization 请求头的,一律拦截
带上Authorization请求头,但是token不合法的,也拦截
只有带上Authorization请求头,token也合法的请求,才能通过