jwt
JWT
JWT - Json Web Token 官网已经讲的很清楚了其实
说白了就是使用某些方法得到一个token,然后通过这个token来验证用户的真假
这边主要介绍JWT来鉴权的知识,一般是作为中间件使用
结构
JSON Web 令牌由三个部分组成,由 . 分隔
- Header
- Payload
- Signature
通常表示为 xxxxxxxx.xxxxxxxx.xxxxxxxxx
三个字段都是JSON格式的,然后经过BASE64编码后获得下面这个
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
反正一坨你也看不懂,但是登录一个网址你就可以转换了 JSON Web Tokens - jwt.io
转换出来的格式就是刚才的三个格式
Header
Header 由 令牌类型 和 签名算法 组成
{
“alg”: “HS256”,
“typ”: “JWT”
}
Payload
由英文意思得,就是有效负载,在这个字段中会承载着最有用的信息
但是一般不放机密信息,并且这个字段的信息承载不能过多数据
里面大致有这些数据
- 预定义声明(已注册声明)
简称 全称 含义 iss Issuer 发行方 sub Subject 主体 aud Audience (接受)目标方 exp Expiration Time 过期时间 nbf Not Before 早于定义的时间的JWT不能处理 iat Issued At JWT发行的时间戳 jti JWT ID JWT的唯一标识 公共声明
服务器自己定义,比如说 后面我自己定义一个user啥的
私人声明
这是为在同意使用他们各方之间共享信息而创建的自定义声明
示例:
{
“sub”: “1234567890”,
“name”: “John Doe”,
“admin”: true
}
Signature
我们需要获得 header 和 payload
签名算法一般是 HMAC(HS256) 和 RSA(RS256)
首先,使用Base64 URL编码的头部和有效负载创建一个消息。消息的格式通常是 **
base64UrlEncode(header) + "." + base64UrlEncode(payload)
**。使用指定的算法(在JWT的头部中指定)对消息进行签名
将生成的签名附加到JWT的尾部,形成最终的JWT。
示例:
- 创建消息:**
base64UrlEncode(header) + "." + base64UrlEncode(payload)
**,假设编码后的头部为 **eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9
**,编码后的有效负载为 **eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogIkpvaG4gRG9lIiwgImFkbWluIjogdHJ1ZQ==
**,那么消息就是 **eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogIkpvaG4gRG9lIiwgImFkbWluIjogdHJ1ZQ==
**。 - 应用算法:使用HMAC SHA-256算法和密钥对消息进行签名,得到签名值。
- 添加签名:将签名值附加到JWT的尾部,形成最终的JWT。
反正就是 组合加签名
- 创建消息:**
工作原理
首先为什么要用JWT,因为Session不好用,为什么Session不好用
HTTP协议是无状态的,也就是说,如果我们已经认证了一个用户
那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证
Session
1、用户向服务器发送用户名和密码。
2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
3、服务器向用户返回一个 session_id,写入用户的 Cookie。
4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
这种模式的问题在于扩展性不好,如果是服务器集群,或者跨域的服务导向结构,需要session共享
如果实现session共享,可以使用数据库或者别的持久层,但是这样子依赖持久层,工程量大,一旦持久层崩溃,就没了。
JWT
服务器认证后,生成一个token,发回给用户,以后通信的时候用户只需要发送token即可
这相当于间接地将验证数据传递到了用户,服务器的压力会小很多
为了防止用户篡改数据,服务器在生成token的时候需要用到Signature
1,浏览器发起请求登陆,携带用户名和密码;
2,服务端验证身份,根据算法,将用户标识符打包生成 token,
3,服务器返回JWT信息给浏览器,JWT不包含敏感信息;
4,浏览器发起请求获取用户资料,把刚刚拿到的 token一起发送给服务器;
5,服务器发现数据中有 token,验明正身;
6,服务器返回该用户的用户资料;
区别
(1) session 存储在服务端占用服务器资源,而 JWT 存储在客户端
(2) session 存储在 Cookie 中,存在伪造跨站请求伪造攻击的风险
(3) session 只存在一台服务器上,那么下次请求就必须请求这台服务器,不利于分布式应用
(4) 存储在客户端的 JWT 比存储在服务端的 session 更具有扩展性
代码呈现
这个是Gin-Gorm下的实现gin-gorm功能的使用
其实一开始我只是想着写一个修改密码的案例,但是想着想着感觉不对啊,我们平时修改密码都是先登录,然后在登录状态下修改密码的,因此我就找到了JWT
下依赖
1
go get -u github.com/golang-jwt/jwt/v5
库更新了,网上很多代码都有小毛病,泪目。。
Router 路由
1
rootPath.PUT("/update", middleware.Auth(), handler.Update)
鉴权 要放在中间件里面
NewToken 生成令牌
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47package jwt
import (
"github.com/golang-jwt/jwt/v5"
"time"
)
var jwtKey = []byte("ItIsSecret") // 密钥是一个字节数组
// 把claims作为一个结构体
type Payload struct {
Authorized bool `json:"authorized"`
User string `json:"user"`
}
type MyCustomClaims struct {
Payload
jwt.RegisteredClaims
}
func NewToken(name string) (string, error) {
//设置一些预定义 Payload
claims := &MyCustomClaims{
Payload: Payload{
Authorized: true,
User: name,
},
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "Echin",
Subject: "Tom",
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
// jwt.NewNumericDate 可以创建一个符合JWT标准的时间格式
},
}
// 创建一个新的令牌
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Header,token是一个对象
//签名并获取完整的编码令牌作为字符串 Signature
tokenString, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return tokenString, nil
}- 这里基本上使用Claims来标识Payload了,赋值的时候把用户的name给user
- 按照刚才原理的介绍,生成一个token就直接使用jwt.NewWithClaims(算法,负载)
- 重要的是我这边负载使用的是结构体的形式(感谢NX模板),之前使用的是map的形式,但是发现他使得token后面传递值时候的类型断言有点问题,所以改成了结构体的形式。
- 密钥必须是一个 字节数组
- 时间传递的时候需要使用时间戳
- 还有一个很重要的,因为版本的原因很多网上预定义类型为jwt.StandardClaims,但是最新版本的类型是jwt.RegisterClaims
- 最后把返回一个tokenString,代码逻辑要注意: 生成token和验证token就是 *token与string之间的转化,要发送给客户端就需要使用string类型,但是到时候自己验证token的时候就需要string类型转化称为 *token 类型
ParseToken 验证token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35package jwt
import (
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
"strings"
)
// ParseToken 是 解析令牌
func ParseToken(bearerToken string) (*MyCustomClaims, error) {
// 解析方式需要添加 Bearer token模式
tokenParts := strings.Split(bearerToken, " ") //通过空格分隔出两个部分,并且存入数组之中
if len(tokenParts) != 2 || strings.ToLower(tokenParts[0]) != "bearer" { // strings.ToLower会把字符串变成小写的形式
return nil, errors.New("Invalid token format,you need add bearer")
}
tokenString := tokenParts[1]
// 解析后续token
claims := &MyCustomClaims{}
// 是*token和string之间的转换
// 这是一个回调函数具体结构就是 jwt.Parse(string,KeyFunc)
_, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
// 验证签名方法 HMAC-SHA56签名方法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("Unexpected Signing Method")
}
return jwtKey, nil
})
if err != nil {
fmt.Println(err)
return nil, err
}
return claims, nil
}- 由于 http头部往往是 http Bearer
因此在真实传递参数时需要先解析Bearer+空格这个字段,而后去解析token - 解析token就是使用了jwt.ParseWithClaims,因为传递的时claims的指针,claims直接获得值,后来就是使用claims.user中的值来鉴权的,因此返回claims就行,也不需要token变量
- 回调函数的结构还是有点蒙蔽的
- 由于 http头部往往是 http Bearer
Auth 中间件鉴权
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30package middleware
import (
"LearningGo/jwt"
"github.com/gin-gonic/gin"
"net/http"
)
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Failed to fetch token",
})
return
}
parseToken, err := jwt.ParseToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"msg": "Failed to Auth",
"error": err,
})
c.Abort()
return
}
c.Set("Payload", parseToken)
c.Next()
}
}- 获取http头部字段就直接使用c.GetHeader(“Authorization”),记得不要写成token,因为APIfox没有选项,并且在测试的时候记得在Header中测试,而不是Auth上面测试
- c.Next c.Abort c.Set 是中间件的知识,不明白可以去查资料
- 为什么要把jwt放到中间件,最重要的是有c.Set(“key”,value),作用是把键值对放入上下文(gin.Context)中,这样子后续的函数就可以通过c.Get()获得,这个后面再说
- 记住c.GetHeader(“Authorization”)返回的是一个字符串,要解析token就是把他变成一个*token的形式,用这个类型去传递值,毕竟一坨字符串根本看不懂
Login 登录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48package handler
import (
"LearningGo/db"
"LearningGo/jwt"
"LearningGo/model"
"github.com/gin-gonic/gin"
"net/http"
)
// 用户的登录
func Login(c *gin.Context) {
// 用PostForm 传输数据
name := c.PostForm("name")
password := c.PostForm("password")
var v2 model.User
// 判断用户是否存在
if tx := db.DB.Where(" name = ? ", name).First(&v2); tx.Error != nil {
c.JSON(http.StatusBadRequest, gin.H{
"msg": "用户不存在",
"err": tx.Error,
})
return
}
// 判断密码是否正确
if password != v2.Password {
c.JSON(http.StatusBadRequest, gin.H{
"err": "密码不正确,小笨蛋",
})
return
} else {
token, err := jwt.NewToken(name)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"msg": "生成token失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"msg": "登录成功,大聪明",
"user": v2,
"token": token,
})
}
}这个就是生成令牌的作用地方,当然也是我测试的时候知道token到底为多少的地方,不知道token我就没法验证token了,你说呢?
update 修改密码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30package handler
import (
"LearningGo/db"
"LearningGo/jwt"
"LearningGo/model"
"github.com/gin-gonic/gin"
"net/http"
)
// 修改密码
func Update(c *gin.Context) {
// 输入老的密码和新的密码
NewPassword := c.PostForm("NewPassword")
payload, exists := c.Get("Payload")
if !exists {
c.JSON(http.StatusBadRequest, gin.H{
"msg": "账号未登录",
})
return
}
load := payload.(*jwt.MyCustomClaims)
db.DB.Model(&model.User{}).Where("name = ? ", load.User).Update("password", NewPassword)
c.JSON(http.StatusOK, gin.H{
"msg": "修改密码成功",
"user": load.User,
"NewPassword": NewPassword,
})
}c.Get(“key”)是从上下文中获取数值,但是这边获得的数值类型是Any,因此需要给Payload进行类型断言(这个东西超级好用,但是别天天对着接口类型断言….),因为Payload本身形式就是jwt.MyCustomClaims的,本来结构体就是这么定义的,这样就可以顺其自然的获得token的user信息,再通过user信息进行数据库操作,这也没啥好说的反正。
总结
- JWT就是生成和验证令牌,放到中间件里面使用,完了。
- 项目驱动学习
- GPT+Github+BING
- 问鸟鸟和NX,虽然他们每次提出的点我都Get不到,会了以后就Get到了,特别是NX每次都言简意赅,听都听不懂,唉。。。