JWT
JSON Web Token (JWT) 是一种开放标准 (RFC 7519),提供一种简洁且自包含的方式,以JSON形式在通信双方间传递信息。这些信息可通过数字签名进行验证,确保其可信度。JWT 可以使用密钥(HMAC)或 RSA 或 ECDSA 的公钥/私钥对进行签名。虽然JWT可以加密以提供私密消息,但我们一般指的是已签名的 tokens。
JWT作用
- 身份验证和授权:JWT常被用来在身份提供者和服务提供者之间传递被认证的用户身份信息,以便于从资源服务器获取资源。这种方式特别适用于分布式站点的登录场景357。此外,JWT还可以包含用户标识、用户角色和权限等信息,进一步实现细粒度的授权控制。
- 信息交换:由于JWT的载荷部分可以自定义,它可以存储任何JSON格式的数据,这使得JWT成为一种便捷的方式来进行信息交换。例如,可以使用JWT来存储用户喜好、配置信息等自定义功能所需的信息。
- 单点登录(SSO):JWT因其小巧和自包含的特性,特别适合用于实现单点登录(SSO)。在单点登录场景中,用户只需进行一次登录操作,便可以在多个服务之间自由切换,而无需重复登录,极大地提高了用户体验。
- API密钥管理:JWT也广泛应用于API密钥管理领域。通过使用JWT作为API请求的一部分,可以有效地管理和控制对API的访问权限。
- 替代传统的Session和Cookie机制:在某些情况下,JWT可以用来替代传统的Session和Cookie机制。与Session和Cookie相比,JWT具有更小的服务器开销和更好的扩展性,因为它直接将用户数据下发给客户端,每次请求时附带发送给服务器。
- 跨域请求(Cross-Origin Requests):JWT可用于跨域请求的身份验证。由于JWT是自包含的,它不依赖于任何服务器端的会话存储,因此非常适合用于跨域API调用。
认证流程
- 前端通过Web将用户名和密码发送至后端,通常采用HttpPost请求。
- 后端在核对用户名和密码后,会将用户ID及其他信息作为JWT payload,并生成一个token返回给前端。
- 前端保存这个token,可以选择将其保存在localStorage或sessionStorage中,退出登录时删除token即可。
- 在后续的每次请求中,前端需要在header中放入token。
- 后端会检查token的有效性,包括签名是否正确,token是否过期等。
- 当后端验证token通过后,就会进行业务处理,并返回相应结果。
JWT的组成:
一个JWT实际上由三个部分组成,它们之间用点(.
)分隔:
- 头部(Header) :
- 通常包含两个部分:token的类型(typ),这里固定为JWT;以及所使用的签名算法(alg),例如HS256表示使用HMAC和SHA256算法进行签名。
- 例如:
{"alg": "HS256", "typ": "JWT"}
- 载荷(Payload) :
- 包含了一系列可以被添加到JWT中的声明(claims)。这些声明可以分为三类:
- 公共声明(Public Claims): 任何JWT都可以包含的声明,如
exp
(过期时间)、iat
(发行时间)和sub
(主题)。 - 注册声明(Registered Claims): 一组预定义的声明,具有特定的语义意义,如
iss
(发行者)、aud
(接收方)等。 - 私有声明(Private Claims): 由应用程序定义的声明,可以在标准声明之外添加额外的信息。
- 公共声明(Public Claims): 任何JWT都可以包含的声明,如
- 例如:
{"sub": "1234567890", "name": "John Doe", "iat": 1516239022}
- 包含了一系列可以被添加到JWT中的声明(claims)。这些声明可以分为三类:
- 签名(Signature) :
- 为了确保JWT的安全性,使用头部中指定的算法和服务器的密钥对头部和载荷进行签名。接收方可以使用相同的密钥和算法来验证签名的有效性。
- 签名的生成通常使用Base64 URL编码的头部和载荷,然后与密钥一起通过签名算法进行加密。
将这三部分编码(Base64 URL编码)并用点连接起来,就形成了一个完整的JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cU5O62AvvewJHsfqh1EJpTbuE7sBqVb64hjSDgZSTrZDRbky3ofGkeXBXBQgTJgzTyTTh0b2tlbg
在这个例子中,JWT由三部分组成,每部分都用.
分隔。第一部分是头部,指定了签名算法;第二部分是载荷,包含了用户信息和过期时间等声明;第三部分是签名,用于验证JWT的真实性。
代码实例
1.安装JWT库
安装golang-jwt/jwt库
go
go get -u [github.com/golang-jwt/jwt](http://github.com/golang-jwt/jwt)
2.创建JWT密钥
go
import (
"github.com/golang-jwt/jwt"
)
// 生成JWT密钥
key := jwt.New(jwt.SigningMethodHS256)
3.创建JWT令牌
创建JWT令牌时,你需要定义载荷(Payload)中的声明(Claims),并使用密钥对令牌进行签名
go
import (
"github.com/golang-jwt/jwt"
"time"
)
//创建JWT令牌
func createToken(claims map[string]interface{})(string,error){
//创建JWT令牌
token:=jwt.NewWithClaims(key,jwy.MapClaims(claims))
//设置过期时间
token.Claims.(jwt.MapClaims)["exp"]=time.Now().Add(5*time.Minute).Unix()
//签发令牌
tokenString, err := token.SignedString()
if err != nil {
return "", err
}
return tokenString, nil
}
4.验证JWT令牌
验证JWT令牌时,你需要使用相同的密钥和签名方法来验证签名的有效性,并检查令牌是否过期:
go
// parseToken 从给定的token字符串中解析出一个有效的jwt.Token实例。
// 函数接受一个tokenString参数,表示待解析的JWT字符串,
// 并返回一个指向jwt.Token的指针以及一个error。
// 如果解析成功,返回的jwt.Token将包含解码后的JWT声明信息,
// 否则返回一个非nil error说明解析失败的原因。
func parseToken(tokenString string)(*jwt.Token,error){
// 使用jwt.ParseWithClaims方法解析tokenString,
// 将声明部分解析为jwt.MapClaims类型(一个可索引的声明映射)。
// 提供一个自定义的解析密钥验证函数作为第三个参数。
tokne,err:=jwt.ParseWithClaims(tokenString,&jwt.MapClaims{},
func(tokekn *jwt.Token)(interface{},error){
return key,nil
})
if err!=nil{
return nil,error
}
// 如果token解析成功且有效(即其声明经过验证且未过期等),
// 则尝试将其声明部分转换为jwt.MapClaims类型,并检查转换是否成功。
// 若成功且token仍处于有效状态(token.Valid == true),则返回解析得到的token及其原始声明。
if claims, ok := token.Claims.(*jwt.MapClaims); ok && token.Valid {
return token, nil
}
// 如果上述条件均不满足(即解析或验证过程中出现错误,
// 或token已过期、被篡改等导致无效),则返回一个表示无效JWT的预定义错误。
return nil, jwt.ErrInvalid
}
此函数的主要功能是接收一个JWT字符串,通过调用jwt.ParseWithClaims
方法对其进行解析。在解析过程中,它会使用传入的密钥(key
)验证JWT的签名,确保其完整性和有效性。如果解析和验证成功,函数返回解析后的jwt.Token
实例以及nil
错误。否则,返回nil
的jwt.Token
指针和相应的错误信息。
5.实现登录和注册逻辑
在后端服务中,实现登录和注册逻辑,并在登录成功后返回JWT令牌:
go
func login(w http.ResponseWriter, r *http.Request) {
// 假设你已经验证了用户的凭据
// 创建JWT令牌
claims := jwt.MapClaims{
"userID": "user-id",
"exp": time.Now().Add(5 * time.Minute).Unix(),
}
tokenString, err := createToken(claims)
if err != nil {
// 处理错误
}
// 返回JWT令牌
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"token": tokenString})
}
6. 保护路由
创建一个中间件来保护你的路由,确保只有携带有效JWT令牌的请求才能访问:
go
// requireAuth 是一个HTTP中间件函数,用于对请求进行JWT身份验证。
// 它接收一个http.HandlerFunc类型的next参数,代表待执行的下一个HTTP处理器。
// 函数返回一个新的http.HandlerFunc,该处理器在实际执行next之前,
// 会对传入的请求进行JWT令牌验证。若验证失败,则返回HTTP 401 Unauthorized响应。
func requireAuth(next http.HandlerFunc) http.HandlerFunc {
// 返回一个匿名http.HandlerFunc,作为实际执行的身份验证中间件。
return func(w http.ResponseWriter, r *http.Request) {
// 从请求头的"Authorization"字段中提取JWT令牌字符串。
tokenString := r.Header.Get("Authorization")
// 如果请求头中没有提供"Authorization"字段或其值为空,
// 则向客户端发送HTTP 401 Unauthorized响应,并附带错误消息。
if tokenString == "" {
http.Error(w, "Authorization header is required", http.StatusUnauthorized)
return
}
// 使用parseToken函数解析并验证JWT令牌字符串。
// 如果解析或验证过程中发生错误,或者令牌本身无效,
// 则向客户端发送HTTP 401 Unauthorized响应,并附带错误消息。
token, err := parseToken(tokenString)
if err != nil || !token.Valid {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// 如果JWT令牌验证成功,继续执行传入的next处理器(即后续的HTTP处理逻辑)。
next(w, r)
}
}
此代码实现了HTTP中间件requireAuth
,其作用是在接收到HTTP请求时,检查请求头中的Authorization
字段是否存在并含有有效的JWT令牌。如果请求缺少Authorization
字段或令牌无效,中间件会立即向客户端返回HTTP 401 Unauthorized状态码及相应的错误消息。反之,若令牌验证通过,则允许请求继续传递到下一个HTTP处理器(next
参数指定的函数)进行后续处理。这样的设计使得您可以轻松地将requireAuth
中间件应用于特定路由或全局,以确保只有持有有效JWT令牌的客户端才能访问受保护的资源。
然后,使用这个中间件来保护你的路由:
go
http.HandleFunc("/protected", requireAuth(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to the protected route!")
}))
实例
在Go语言中实现JWT鉴权的完整应用实例。
go
package main
import (
"fmt"
"net/http"
"time"
jwtgo "github.com/golang-jwt/jwt"
)
// jwtKey 是用于签署 JWT 令牌的秘密密钥,必须保密。
var jwtKey = []byte("my_secret_key")
// User 定义了用户的结构体,包含用户的ID和姓名。
// 这些字段将被编码到 JWT 令牌中。
type User struct {
ID int `json:"id"` // 用户的唯一标识符
Name string `json:"name"` // 用户的姓名
}
// createToken 根据提供的 User 对象创建一个新的 JWT 令牌。
// 它设置了一个 "userID" 声明和一个 5 分钟后过期的时间。
func createToken(user *User) (string, error) {
// 使用 HS256 签名方法创建一个新的 JWT 实例。
token := jwtgo.New(jwtgo.SigningMethodHS256)
// 将 token 的 Claims 设置为 MapClaims 类型,以便我们可以添加自定义声明。
claims := token.Claims.(jwtgo.MapClaims)
// 添加 "userID" 声明到 JWT 载荷中。
claims["userID"] = user.ID
// 设置 "exp" 声明为当前时间加上 5 分钟的 Unix 时间戳。
claims["exp"] = time.Now().Add(5 * time.Minute).Unix()
// 使用 jwtKey 签署令牌,并将其转换为字符串。
tokenString, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return tokenString, nil
}
// parseToken 接受一个 JWT 令牌字符串,并使用 jwtKey 验证其签名。
// 如果令牌有效,它将返回解析后的 jwtgo.Token 对象。
func parseToken(tokenString string) (*jwtgo.Token, error) {
// 使用 jwtgo.ParseWithClaims 解析 JWT 令牌,并使用 MapClaims 类型作为期望的声明类型。
token, err := jwtgo.ParseWithClaims(tokenString, &jwtgo.MapClaims{}, func(token *jwtgo.Token) (interface{}, error) {
// 提供 jwtKey 作为密钥来验证令牌的签名。
return jwtKey, nil
})
if err != nil {
return nil, err
}
// 确保解析后的声明是 MapClaims 类型。
if _, ok := token.Claims.(*jwtgo.MapClaims); !ok {
return nil, fmt.Errorf("invalid claims")
}
// 检查令牌是否有效(例如,是否已过期)。
if !token.Valid {
return nil, fmt.Errorf("token is not valid")
}
return token, nil
}
// register 是一个 HTTP 处理函数,用于处理用户注册请求。
// 它创建一个新的 User 对象,并为其生成一个 JWT 令牌。
func register(w http.ResponseWriter, r *http.Request) {
// 示例中省略了实际的用户注册逻辑(如保存用户信息到数据库)。
// 直接创建一个示例用户和令牌。
user := &User{ID: 1, Name: "John Doe"}
token, err := createToken(user)
if err != nil {
http.Error(w, "Error creating token", http.StatusInternalServerError)
return
}
// 设置响应头为 application/json 并将令牌编码为 JSON 返回。
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"token": token})
}
// login 是一个 HTTP 处理函数,用于处理用户登录请求。
// 它验证用户凭据(示例中省略),并返回一个 JWT 令牌。
func login(w http.ResponseWriter, r *http.Request) {
// 示例中省略了实际的用户登录逻辑(如验证用户凭据)。
// 直接创建一个示例用户和令牌。
user := &User{ID: 1, Name: "John Doe"}
token, err := createToken(user)
if err != nil {
http.Error(w, "Error creating token", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"token": token})
}
// protected 是一个 HTTP 处理函数,用于处理需要身份验证的请求。
// 它从 HTTP 头中提取 JWT 令牌,并验证其有效性。
func protected(w http.ResponseWriter, r *http.Request) {
// 从 "Authorization" HTTP 头中提取 JWT 令牌。
tokenString := r.Header.Get("Authorization")
if tokenString == "" {
http.Error(w, "Unauthorized access", http.StatusUnauthorized)
return
}
// 解析并验证 JWT 令牌。
token, err := parseToken(tokenString)
if err != nil {
http.Error(w, "Unauthorized access", http.StatusUnauthorized)
return
}
// 如果令牌有效,返回成功响应。
w.WriteHeader(http.StatusOK)
w.Write([]byte("Welcome to the protected route!"))
}
func main() {
// 设置 HTTP 路由处理函数。
http.HandleFunc("/register", register)
http.HandleFunc("/login", login)
http.HandleFunc("/protected", protected)
// 启动 HTTP 服务并监听 8080 端口。
http.ListenAndServe(":8080")
}
这个程序实现了一个简单的 JWT 鉴权系统。它提供了用户注册和登录功能,这两个功能都会生成一个 JWT 令牌。此外,它还提供了一个受保护的路由,该路由只允许携带有效 JWT 令牌的请求访问。