Go开发Gin项目添加jwt功能实例详解
作者:慧慧来咯 发布时间:2024-04-26 17:31:00
啥是JWT
JWT全称JSON Web Token是一种跨域认证解决方案,属于一个开放的标准,它规定了一种Token实现方式,目前多用于前后端分离项目和OAuth2.0业务场景下。
为什么要用在你的Gin中使用JWT
传统的Cookie-Sesson模式占用服务器内存, 拓展性不好,遇到集群或者跨服务验证的场景的话, 要支持Sesson复制或者sesson持久化
JWT的基本原理
在服务器验证之后, 得到用户信息JSON
{
"user_id": "xxxxxx",
"role": "xxxxxx",
"refresh_token": "xxxxx"
}
JWT TOKEN怎么组成
JWT是一个很长的字符串
eyJhbGciOiJI123afasrwrqqewrcCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
它由三部分组成, 每部分用点(.)分隔, 三个部分依次如下
Header(头部)
Payload(负载)
Signature(签名)
Header
Header是一个经过BASE64URL算法加密过的JSON对象, 解密后如下
{
"alg": "HS256",
"typ": "JWT"
}
其中, alg属性表示签名所用的算法,默认是HS256;
typ则表示当前token的类型, 而JWT的类型则为jwt
Base64URL
与BASE64类似, 由于+、/、=这几个符号在URL中有特殊含义, 因此BASE64RUL算法, 把这几个符号进行了替换
+ -> -
= -> 被忽略
/ -> _
Payload
Payload部分也是JSON对象经过BASE64URL算法转成字符串的, Payload部分包含7个基本字段
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
也可以往里面塞入自定义的业务字段, 如下
user_id
role
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
解密过程
当系统接收到TOKEN时, 拿出Header和Payload的字符串用.拼接在一起之后, 用Header里面指定的哈希方法通过公式
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
进行加密得出密文
然后用刚刚得出的密文与TOKEN传过来的密文对比, 如果相等则表明密文没有更改.
一些特点(优点和缺点)
JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
JWT 不加密的情况下,不能将秘密数据写入 JWT。
JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
GIN整合JWT
go get -u github.com/dgrijalva/jwt-go
go get github.com/gin-gonic/gin
编写jwtutil
var Secret = []byte("whatasecret")
// jwt过期时间, 按照实际环境设置
const expiration = 2 * time.Minute
type Claims struct {
// 自定义字段, 可以存在用户名, 用户ID, 用户角色等等
Username string
// jwt.StandardClaims包含了官方定义的字段
jwt.StandardClaims
}
func GenToken(username string) (string, error) {
// 创建声明
a := &Claims{
Username: username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(expiration).Unix(), // 过期时间
IssuedAt: time.Now().Unix(), // 签发时间
Issuer: "gin-jwt-demo", // 签发者
Id: "", // 按需求选这个, 有些实现中, 会控制这个ID是不是在黑/白名单来判断是否还有效
NotBefore: 0, // 生效起始时间
Subject: "", // 主题
},
}
// 用指定的哈希方法创建签名对象
tt := jwt.NewWithClaims(jwt.SigningMethodHS256, a)
// 用上面的声明和签名对象签名字符串token
// 1. 先对Header和PayLoad进行Base64URL转换
// 2. Header和PayLoadBase64URL转换后的字符串用.拼接在一起
// 3. 用secret对拼接在一起之后的字符串进行HASH加密
// 4. 连在一起返回
return tt.SignedString(Secret)
}
func ParseToken(tokenStr string) (*Claims, error) {
// 第三个参数: 提供一个回调函数用于提供要选择的秘钥, 回调函数里面的token参数,是已经解析但未验证的,可以根据token里面的值做一些逻辑, 如`kid`的判断
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return Secret, nil
})
if err != nil {
return nil, err
}
// 校验token
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
Secret是秘钥, 用于加密签名
expiration是TOKEN过期时间
Claims是签名声明对象, 包含自定义的字段和JWT规定的字段
type Claims struct {
// 自定义字段, 可以存在用户名, 用户ID, 用户角色等等
Username string
// jwt.StandardClaims包含了官方定义的字段
jwt.StandardClaims
}
type StandardClaims struct {
Audience string `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
Id string `json:"jti,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
}
GenToken方法
GenToken方法为某个username生成一个token, 每次生成都不一样
jwt.NewWithClaims(jwt.SigningMethodHS256, a)声明了一个签名对象, 并且指定了HS256的哈希算法
token.SignedString(Secret)表明用刚刚的声明对象和SECRET利用指定的哈希算法去加密,包含下面流程
先对Header和PayLoad进行Base64URL转换
Header和PayLoadBase64URL转换后的字符串用.拼接在一起
用secret对拼接在一起之后的字符串进行HASH加密
BASE64URL(Header).BASE64URL(Payload).signature连在一起的字符串返回
func GenToken(username string) (string, error) {
// 创建声明
a := &Claims{
Username: username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(expiration).Unix(), // 过期时间
IssuedAt: time.Now().Unix(), // 签发时间
Issuer: "gin-jwt-demo", // 签发者
Id: "", // 按需求选这个, 有些实现中, 会控制这个ID是不是在黑/白名单来判断是否还有效
NotBefore: 0, // 生效起始时间
Subject: "", // 主题
},
}
// 用指定的哈希方法创建签名对象
tt := jwt.NewWithClaims(jwt.SigningMethodHS256, a)
// 用上面的声明和签名对象签名字符串token
// 1. 先对Header和PayLoad进行Base64URL转换
// 2. Header和PayLoadBase64URL转换后的字符串用.拼接在一起
// 3. 用secret对拼接在一起之后的字符串进行HASH加密
// 4. 连在一起返回
return tt.SignedString(Secret)
}
ParseToken方法
ParseToken方法解析一个Token, 并验证Token是否生效
jwt.ParseWithClaims方法, 用于解析Token, 其第三个参数:
提供一个回调函数用于提供要选择的秘钥, 回调函数里面的token参数,是已经解析但未验证的,可以根据token里面的值做一些逻辑, 如判断kid来选用不同的secret
KID(可选): 代表秘钥序号。开发人员可以用它标识认证token的某一秘钥
func ParseToken(tokenStr string) (*Claims, error) {
// 第三个参数: 提供一个回调函数用于提供要选择的秘钥, 回调函数里面的token参数,是已经解析但未验证的,可以根据token里面的值做一些逻辑, 如`kid`的判断
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return Secret, nil
})
if err != nil {
return nil, err
}
// 校验token
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
编写中间件
从Header中取出Authorization并拿去解析jwt.ParseToken,
验证token是否被串改, 是否过期
从token取出有效信息并设置到上下文
func JWTAuthMiddleware() func(ctx *gin.Context) {
return func(ctx *gin.Context) {
// 根据实际情况取TOKEN, 这里从request header取
tokenStr := ctx.Request.Header.Get("Authorization")
if tokenStr == "" {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"code": code.ERR_AUTH_NULL,
"msg": code.GetMsg(code.ERR_AUTH_NULL),
})
return
}
claims, err := jwt.ParseToken(tokenStr)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"code": code.ERR_AUTH_INVALID,
"msg": code.GetMsg(code.ERR_AUTH_INVALID),
})
return
} else if time.Now().Unix() > claims.ExpiresAt {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"code": code.ERR_AUTH_EXPIRED,
"msg": code.GetMsg(code.ERR_AUTH_EXPIRED),
})
return
}
// 此处已经通过了, 可以把Claims中的有效信息拿出来放入上下文使用
ctx.Set("username", claims.Username)
ctx.Next()
}
}
使用中间件
/login不用中间件
中间件指定在authorizedrouter, 因此authorized下的所有路由都会使用此中间件
func main() {
r := gin.Default()
r.POST("/login", router.Login)
authorized := r.Group("/auth")
authorized.Use(jwt.JWTAuthMiddleware())
{
authorized.GET("/getUserInfo", router.GetUserInfo)
}
r.Run(":8082")
}
测试
login请求获取token
POST http://localhost:8082/login
把token放入getUserInfo请求
GET http://localhost:8082/auth/getUserInfo
其他
完整的JWT登录还应该包括
使TOKEN失效(过期或者黑名单等功能)
refresh token
…
通过上述流程你就能快速的在你的Go Gin项目中应用JWT能力
来源:https://juejin.cn/post/7120796419836870669


猜你喜欢
- 本文实例讲述了vue组件定义,全局、局部组件,配合模板及动态组件功能。分享给大家供大家参考,具体如下:一、定义一个组件定义一个组件:1. 全
- 是在客户端确认还是在服务器端确认? <SCRIPT LANGUAGE="VBSc
- 今天用vue来实现一个分页组件,总体来说,vue实现比较简单,样式部分模仿了elementUI。所有代码的源码可以再github上下载的到:
- 以下公共方法支持列表,元组,字典,字符串。内置函数Python 包含了以下内置函数:函数描述备注len(item)计算容器中元素个数del(
- subplot函数介绍matplotlib下, 一个 Figure 对象可以包含多个子图(Axes), 可以使用 subplot() 快速绘
- 本文实例讲述了Python使用百度翻译开发平台实现英文翻译为中文功能。分享给大家供大家参考,具体如下:#coding=utf8import
- 目录一、为什么使用 .gitignore ?二、使用规则2.1 注释2.2 忽略文件(1)忽略文件和目录(2)仅忽略文件2.3 忽略目录2.
- 先来思考一个问题,B站一个视频的弹幕最多会有多少?比较多的会有2000条吧,这么多数据,B站肯定是不会直接把弹幕和这个视频绑在一起的。也就是
- 1 存储过程1.1 什么是存储过程存储过程是一组为了完成某项特定功能的sql语句集,其实质上就是一段存储在数据库中的代码,他可以由声明式的s
- 一、Matplotlib简介与安装Matplotlib也就是Matrix Plot Library,顾名思义,是Python的绘图库。它可与
- 提示框提示框的基本使用方式为:<span data-toggle="tooltip" data-original-
- Python作为一种脚本语言,其要求强制缩进,使其易读、美观,它的数据类型可以实现自动转换,而不需要像C、Java那样给变量定义数据类型,使
- 前言一、查看安装的库1.命令查询pip list2.从安装路径site-packages查看二、导出库安装文件1.导出在我们要导出的库文件夹
- 过去有很多网页设计师喜欢将他们的网页效果图用table布局实现成网页,但是这样做会遇到一个比较麻烦的问题就是,后期调试和维护会相当的困难。现
- 本文实例为大家分享了抖音代码舞python制作代码,供大家参考,具体内容如下一、效果图二、转换代码from img import Image
- 本文实例讲述了js实现简单的可切换选项卡效果的方法。分享给大家供大家参考。具体如下:如图,最简单的纯粹的选项卡第一步,当然是先写html代码
- 因此,常常出现这样的错误select * from [IND] where INDID>10unionselect * from [I
- 一. 删除完全重复的记录完全重复的数据,通常是由于没有设置主键/唯一键约束导致的。测试数据:if OBJECT_ID('duplic
- 对于从事数据领域的小伙伴来说,当需要阐述自己观点、展示项目成果时,我们需要在最短时间内让别人知道你的想法。我相信单调乏味的语言很难让别人快速
- 什么是事务?事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。数据库事务通常包含了一个序列的对数据库的读/写操作