JWT详解

Olivia的小跟班 Lv3

题记

本文主要是讲JWT的有关知识。

1.Session

Session的使用过程是怎么样的?

  • 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中;
  • 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID;
  • 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中;
  • 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。

2.Token

Token的使用过程是怎么样的?

  • 客户端使用用户名和密码请求登录。
  • 服务端收到请求,验证用户名和密码。
  • 验证成功后,服务端会签发一个token,再把这个token返回给客户端。
  • 客户端收到token后可以把它存储起来,比如放到cookie中。
  • 客户端每次向服务端请求资源时需要携带服务端签发的token,可以在cookie或者header中携带。
  • 服务端收到请求,然后去验证客户端请求里面带着的token,如果验证成功,就向客户端返回请求数据。

Token对比session的优点:

  • 无状态:Token是无状态的,而Session需要维护服务器端的状态信息。这使得使用Token更容易在分布式系统和负载均衡器中进行管理。
  • 扩展性:由于没有状态信息,可以轻松地将Token集成到API、移动应用程序等不同的系统中。
  • 安全性:Token可以提供更高的安全性,因为它可以避免CSRF攻击,并且可以通过签名和加密来保护数据的完整性和机密性。
  • 性能:由于Session需要在服务器端维护状态信息,因此可能会影响性能。而Token可以在客户端进行验证和授权,从而减少了对服务器的负载。

3.什么是JWT

终于我们的主角JWT,登场了。JWT全称:JSON Web Token,本质上就是一个字符串(它是Token的一种具体实现方式)。

JWT的认证流程:

  • 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探。
  • 后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串
  • 后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可。
  • 前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)
  • 后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等。
  • 验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果。

img

4.为什么要使用JWT

4.1传统Session的弊端

传统Session认证存在一些弊端,包括以下几个方面:

  1. 服务器端状态管理:使用Session需要在服务器端维护用户的状态信息,这可能会增加服务器的负担和复杂度。
  2. 分布式环境下的问题:由于Session依赖于服务器端状态,因此在分布式环境下可能需要使用一些特殊技术来解决会话跨节点的问题。
  3. 扩展性差:传统Session认证的扩展性较差。如果要将应用程序扩展到多个客户端或不同的系统中,必须维护每个客户端的状态信息。这使得Session认证在一些场景下难以扩展和管理。
  4. CSRF攻击:Session认证容易受到CSRF攻击,攻击者可以通过伪造请求来获取用户的会话信息。
  5. 跨域问题:由于Session是基于Cookie实现的,而Cookie在跨域请求时会受到浏览器的同源策略限制,可能会导致某些跨域场景下无法正常使用。
  6. 占用带宽:每次请求都要携带Session ID等相关信息,增加了请求头的大小,占用了带宽,并且可能会影响网络性能。

相比之下,JWT认证可以更好地满足现代Web应用程序的安全和可扩展性需求。它具有以下优点:

  1. 无状态:JWT是无状态的,服务器不需要在本地存储会话信息,因此可以更容易地实现分布式、跨域和负载均衡等场景下的身份验证。
  2. 可扩展性:由于JWT是基于标准化的JSON格式,因此可以轻松地将其集成到API、移动应用程序等不同的系统中。
  3. 安全性:JWT提供了签名和加密机制,以确保数据的完整性和机密性,避免了CSRF攻击和会话劫持等安全问题。
  4. 可定制性:JWT可以根据业务需求进行自定义,例如添加特定的声明、过期时间等元数据,使其更适合特定的应用场景。

注意这不是说JWT就没有缺点了,它也是有缺点的:

  1. 信息泄露:JWT中的信息是经过Base64编码的,如果密钥不安全或者遭到暴力破解,可能会导致信息泄露。
  2. 不可撤销性:由于JWT是无状态的,一旦JWT签发后,就无法撤销。如果需要撤销访问权限,则必须等待到JWT过期或将其加入黑名单中。
  3. 大小限制:由于JWT包含了所有相关信息,因此它的大小可能会比较大,这可能会导致网络传输和存储方面的性能损失。
  4. 安全更新问题:如果需要更新JWT中的某些信息,例如访问权限、过期时间等,需要重新签发一个新的JWT。但是,这可能会导致之前签发的JWT仍然有效,从而导致安全性问题。

综上所述,虽然JWT具有许多优点,但在具体应用时要注意其潜在的安全风险和实施的复杂性。在使用JWT时,需要合理设置过期时间、密钥长度和加密算法等参数,以确保其安全可靠性。

5.JWT结构

JWT(JSON Web Token)的结构通常由三个部分组成,分别是Header、Payload和Signature。每个部分都使用Base64编码,并使用句点(.)将它们连接起来,形成一个完整的JWT字符串。

下面是一个示例JWT的完整结构:

1
2
3
4
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0Ijo
xNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

其中,Header部分为:

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

Payload部分为:

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}

Signature部分为:

1
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

需要注意的是,尽管JWT使用Base64进行编码,但它并不是加密算法,因此在使用时仍然需要保证密钥的安全性。

1.Header

JWT头是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

2.Payload

有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择

1
2
3
4
5
6
7
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

除以上默认字段外,我们还可以自定义私有字段,一般会把包含用户信息的数据放到payload中,如下例:

1
2
3
4
5
{
"sub": "1234567890",
"name": "Helen",
"admin": true
}

请注意,默认情况下JWT是未加密的,因为只是采用base64算法,拿到JWT字符串后可以转换回原本的JSON数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到JWT中,以防止信息泄露。JWT只是适合在网络中传输一些非敏感的信息。

3.Signature

签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据公式生成签名,在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用.分隔,就构成整个JWT对象。

注意JWT每部分的作用,在服务端接收到客户端发送过来的JWT token之后:

  • header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据
  • signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secretKey对header、payload进行加密,比对加密后的数据和客户端发送过来的是否一致。注意secretKey只能保存在服务端,而且对于不同的加密算法其含义有所不同,一般对于MD5类型的摘要加密算法,secretKey实际上代表的是盐值。

6.JWT种类

JWT(JSON Web Token)根据用途和属性的不同,可以分为以下几种:

  1. JWS:JSON Web Signature,用于验证消息的完整性和来源。JWS包含Header、Payload和Signature三个部分,其中Signature是通过指定算法生成的,用于验证消息是否被篡改。
  2. JWE:JSON Web Encryption,用于对消息进行加密。JWE包含Header、Payload和Encryption三个部分,其中Encryption使用指定算法对消息进行加密,以保证机密性。
  3. JWK:JSON Web Key,用于描述用于加密、解密或签名JWT的公钥和私钥信息。JWK通常包括key type、key id、public key等属性。
  4. JWA:JSON Web Algorithm,用于描述JWT所支持的加密、解密和签名算法,例如HMAC、RSA、AES等。

需要注意的是,这些JWT的种类并不是相互独立的,它们可以结合使用来实现更强大的功能,例如使用JWS和JWE来同时保证消息的完整性、机密性和来源可信度。

7.JWT在Go中的应用

Go中实现JWT需要使用第三方库:jwt-go (官方提供的JWT库,支持生成、解析和验证JWT),一般你需要go get第三库,然后编写相关代码。

1
2
3
4
5
6
7
8
9
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"`
}

这段代码定义了一个名为StandardClaims的结构体类型,它包含了一些常用的声明(claim),这些声明可以被添加到JWT的Payload部分,在身份验证、授权等方面起着重要的作用。具体来说,StandardClaims结构体包括以下字段:

  • Audience:表示接收JWT的一方,可以是单个值或多个值的集合。
  • ExpiresAt:表示JWT的过期时间,以Unix时间戳为单位。
  • Id:表示JWT的唯一标识符,用于避免重放攻击等安全问题。
  • IssuedAt:表示JWT的签发时间,以Unix时间戳为单位。
  • Issuer:表示JWT的签发者,通常是应用程序的名称或网站的域名。
  • NotBefore:表示JWT的生效时间,在此时间之前,JWT无法使用,以Unix时间戳为单位。
  • Subject:表示JWT所代表的主题,例如用户ID、用户名等。

这些声明都是可选的,开发人员可以根据需要自由选择使用哪些声明,并将它们填充到StandardClaims结构体中,然后再将该结构体传递给JWT库生成JWT。

jwt.go

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
var mySignKey = []byte("自己填")  //secretKey

type Claims struct {
UserId int64
jwt.StandardClaims
} //Payload


// GenToken 获取 token
/*
这段代码使用了自定义的Claims结构体来生成JWT。其中,GenToken函数接收一个User类型的参数,并根据该用户信息生成JWT。具体来说,这段代码执行以下步骤:
定义一个过期时间expirationTime,这里设置为7天后。
创建一个Claims结构体对象claims,该结构体包括了UserId和StandardClaims两个字段,前者表示用户ID,后者表示标准声明(包括过期时间、签发时间、签发者等)。
使用jwt.NewWithClaims函数创建一个新的JWT对象token,并指定签名算法(这里使用HS256)以及要添加的声明结构体claims。
调用token.SignedString(mySignKey)函数对JWT进行签名,并将签名后的字符串返回。
需要注意的是,该代码中使用了一个自定义的密钥mySignKey来签名JWT,这个密钥应该保密地存储在服务器端,并且不应该泄露给任何人。同时,由于本代码没有对签名密钥进行加密或哈希处理,因此需要谨慎保管,以避免被恶意攻击者获取。
*/
func GenToken(user repository.User) (string, error) {
expirationTime := time.Now().Add(7 * 24 * time.Hour)
claims := &Claims{
UserId: user.ID,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
IssuedAt: time.Now().Unix(),
Issuer: "douyin_pro_bjxf",
Subject: "user token",
},
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenstring, err := token.SignedString(mySignKey)
if err != nil {
return "", err
}
return tokenstring, nil
}



// ParseToken 解析 token
/*
这段代码用于解析JWT并验证其合法性,具体来说它执行以下操作:
调用jwt.ParseWithClaims函数解析传入的tokenString,并使用mySignKey作为签名密钥进行验证。如果验证通过,则返回一个Token对象。
如果Token对象不为空,则说明该JWT已经成功解析和验证,此时再将其中的声明结构体claims转换为我们定义的自定义Claims类型(当然前提是Token中的声明确实是我们定义的Claims类型),并判断是否有效(这里使用了Valid字段)。
如果有效则返回解析后的Claims对象和true,否则返回nil和false。
需要注意的是,由于JWT可以被篡改或伪造,因此在解析和验证JWT时应该非常谨慎。在实际使用中,可以根据业务需求对JWT的各个部分(Header、Payload、Signature)进行详细的检查,以确保JWT的真实性和完整性。
*/
func ParseToken(tokenString string) (*Claims, bool) {
token, _ := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return mySignKey, nil
})
if token != nil {
if key, ok := token.Claims.(*Claims); ok {
if token.Valid {
return key, true
} else {
return key, false
}
}
}
return nil, false
}

JWTMiddleWare

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
// JWTMiddleWare 鉴权中间件,鉴权并设置user_id
func JWTMiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
tokenStr := c.Query("token")
if tokenStr == "" {
tokenStr = c.PostForm("token")
}
//用户不存在
if tokenStr == "" {
c.JSON(http.StatusOK, gin.H{"status_code": 2, "status_msg": "用户不存在"})
c.Abort() //阻止执行
return
}
//验证token
tokenStruck, ok := utils.ParseToken(tokenStr)
if !ok {
c.JSON(http.StatusOK, gin.H{
"status_code": 5,
"status_msg": "token不正确",
})
c.Abort() //阻止执行
return
}
//token超时
if time.Now().Unix() > tokenStruck.ExpiresAt {
c.JSON(http.StatusOK, gin.H{
"status_code": 5,
"status_msg": "token过期",
})
c.Abort() //阻止执行
return
}
c.Set("user_id", tokenStruck.UserId)
c.Next()
}
}
  • 标题: JWT详解
  • 作者: Olivia的小跟班
  • 创建于 : 2023-03-29 20:22:29
  • 更新于 : 2023-05-27 03:47:42
  • 链接: https://www.youandgentleness.cn/2023/03/29/JWT详解/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论