JWT实战

以Golang的Beego框架为例,添加JWT

1.JWT概述

1)用户登录:
输入用户名和密码。
服务器验证凭据。
若凭据有效,服务器会生成一个JWT,其中包含用户ID、用户名和其他相关信息。

2)JWT的结构:
JWT由三部分组成:头部、载荷和签名。
头部: 包含令牌的类型和所使用的签名算法。
载荷: 包含用户声明,如用户ID、用户名、令牌过期时间等。
签名: 用于验证令牌在传输过程中未被篡改。

3)JWT的使用:
一旦获得了JWT,它就会随每个请求发送到服务器。
服务器接收到JWT后,会验证其签名,确保其有效且未过期。
如果验证通过,服务器会处理请求并返回相应的响应。

4)JWT的优势:
无状态: 由于所有用户信息都包含在令牌中,服务器不需要存储会话信息。
可扩展: 令牌可以在多个服务之间共享,适用于微服务架构。
跨域支持: 令牌可以跨域传输,适用于单页应用(SPA)。

5)潜在问题:
安全问题: 如果JWT被截获,攻击者可以利用它进行未授权访问。
过期管理: 需要妥善处理令牌的过期和刷新机制,以防止未授权访问

2.Beego安装JWT

1
go get github.com/dgrijalva/jwt-go

1)封装 JWT,创建一个工具类,用于生成、验证 JWT 等操作

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
package utils

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

// 定义 JWT 参数
//Custom 表示这是用户自定义的内容,Claims 是 JWT 的标准术语
type CustomClaims struct {
jwt.StandardClaims
UserID string `json:"userId"`
UserName string `json:"userName"`
}

// 生成 Token
func GenerateToken(userId, userName string, secretKey string) (string, error) {
claims := CustomClaims{
UserID: userId,
UserName: userName,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
},
}
/*这里得到的token是一个中间变量,这个方法创建了一个新的 JWT 实例,指定了签名
算法(HMAC-SHA256)和负载(claims)。头部包含令牌的类型和签名算法,负载包含
用户信息或其他数据。此时,JWT 的头部和负载部分已经准备好,但签名部分尚未生成。
token是未签名的中间状态。就像是一封未封口的信,SignedString 是封口的过程,
只有封口后信才能安全地传递。没有签名的 JWT 是不完整的,不能用于身份验证或授权。*/
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
/*这里使用指定的*签名算法*和*密钥*对头部和负载进行签名,生成签名部分。将头部、负
载和签名部分组合成一个完整的 JWT 字符串,格式为 base64url(header) + "." +
base64url(payload) + "." + signature。base64url(payload) + "." + signature。
最终传递给客户端的 token 实际上是签名后的字符串(完整的JWT字符串),这个字符串包含了
头部、负载和签名部分,是完整的 JWT。*/
return token.SignedString([]byte(secretKey))
}

// 解析 Token
func ParseToken(tokenString string, secretKey string) (*CustomClaims, error) {
//&CustomClaims{}用于存储解析后的声明
//用于解析 JWT 字符串
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secretKey), nil
})
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, err
}

secretKey:。
是一个自定义的密钥,可以是任意字符串。这个密钥用于对 JWT 进行签名,以确保其完整性和安全性。签名就是通过密钥和 JWT 的头部与负载生成的,密钥是验证签名的关键。所以,your_secret_key 必须是服务器和客户端都知晓的密钥。虽然可以使用任意字符串,但为了安全,应该避免简单或可预测的密钥。

1)密钥的作用
签名:在生成 JWT 时,密钥用于对 JWT 的头部和负载进行签名,生成签名部分。
验证:在解析 JWT 时,密钥用于验证签名是否正确,确保 JWT 没有被篡改。
2) 密钥的安全性
保密性:密钥必须保密,不能泄露给未经授权的方。如果密钥泄露,攻击者可以伪造 JWT。
复杂性:密钥应足够复杂且随机,避免使用简单的字符串,如 “secret” 或 “password”。
3)密钥的存储
环境变量:推荐将密钥存储在环境变量中,避免直接写在代码中。推荐做法
配置文件:可以将密钥存储在配置文件中,但确保配置文件不在版本控制系统中。
通常,系统中的所有用户会共享同一个 secret_key,这个密钥用于对所有用户的 JWT 进行签名和验证。共享密钥时,需要注意密钥的安全性,避免泄露。可以通过定时任务或外部服务定期更新配置文件中的 JWT 密钥。更新后需重启应用或监听配置文件变化并热加载新密钥。

1
2
3
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secretKey), nil
})

jwt.ParseWithClaims()首先是对tokenString进行解析变成3部分:头部、负载和签名。内部函数是个匿名函数,这个函数是ParseWithClaims的第三参数要求的,职责是提供用于验证 JWT 签名的密钥,token *jwt.Token就是tokenString解析后的成果,这个匿名函数返回二进制秘钥,用于验证这个成果中包含的签名部分是否正确,确保 JWT 没有被篡改。

1
2
3
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}

这是断言token.Claims是否CustomClaims类型数据,即断言tokenString解析后的负载是否类型正确。ok是个bool值,token.Valid表示签名验证的结果,也是个bool值。
负载的类型正确,如果签名无效,则说明 JWT 可能在传输过程中被篡改。

2)在 Beego 控制器中,使用中间件的方式来验证用户的身份。

3).用户登录逻辑中,生成 Token 并返回给客户端。

在 Beego 的 router.go 文件中添加一个中间件来拦截所有请求,并验证 Token。

token传回客户端的方式:
1.URL 参数的方式,这种方式简单直观,但安全性较差,因为 token 可能会被记录在服务器日志或浏览器历史中。因此,这种方式虽然可行,但不推荐用于生产环境。
2.使用 Cookie,这比 URL 参数更安全,因为 Cookie 可以设置为 HttpOnly,防止 XSS 攻击。Cookie 还能自动附带在后续请求中,减少了手动管理 token 的复杂性。
在前端页面中,虽然无法通过 JavaScript 访问 HttpOnly Cookie,但浏览器会自动将其附带在后续请求中。
3.使用 HTTP 头部传递 token,这种方式更加安全,因为 token 不会暴露在 URL 或 Cookie 中。客户端需要在每次请求时手动设置 Authorization 头部。