什么是 JSON Web Token?
JSON Web Token (JWT) 是一种开放标准(RFC 7519),用于在网络应用之间传递信息。它是一个紧凑且自包含的令牌,通常用于身份验证和授权。JWT 由三部分组成:Header(头部)、Payload(负载)和 Signature(签名),通过点号(.
)连接,编码为 Base64 格式,形成如下的结构:
Header.Payload.Signature
- Header:包含元数据,如签名算法(通常是 HMAC SHA256 或 RSA)。Base64 编码。
- Payload:包含声明(claims),如用户ID、角色、过期时间等。Base64 编码。
- Signature:用于验证令牌的完整性,确保未被篡改。使用密钥对 Header 和 Payload 签名
JWT 的优点包括无状态、跨域支持以及易于扩展,广泛用于 RESTful API 的身份验证。
JWT 的工作原理
- 生成:服务器验证用户身份后,生成 JWT(包含用户信息和签名)并发送给客户端。
- 传递:客户端将 JWT 存储(通常在 localStorage 或 cookie 中),并在后续请求中通过 Authorization 头(如 Bearer )发送。
- 验证:服务器接收 JWT,验证签名和声明(如是否过期),确认合法后处理请求。
JWT 的特点
- 无状态:服务器无需存储令牌,验证仅依赖密钥和令牌本身,适合分布式系统。
- 跨域支持:JWT 可在不同域名间传递,适合微服务架构。
- 紧凑:Base64 编码使其适合 HTTP 头传输。
- 安全性:通过签名防止篡改,但 Payload 未加密,需避免存储敏感数据。
为什么在 Go 中使用 JWT?
Go 语言以其简洁、高性能和强大的标准库而闻名,非常适合构建安全的后端服务。在 Go 中使用 JWT 可以轻松实现用户认证和授权,特别是在构建微服务或 REST API 时。
准备工作
在开始之前,确保你的环境中已安装 Go(建议版本 1.16 或更高)。我们将使用 github.com/golang-jwt/jwt
库来处理 JWT。
首先,安装该库:
go get github.com/golang-jwt/jwt/v5
创建 JWT
以下是一个简单的 Go 程序示例,展示如何生成一个 JWT 令牌。
package main
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
func main() {
// 定义密钥
secretKey := []byte("my-secret-key")
// 创建声明(Claims)
claims := jwt.MapClaims{
"user_id": 123,
"role": "admin",
"exp": time.Now().Add(time.Hour * 24).Unix(), // 令牌过期时间(24小时)
"iat": time.Now().Unix(), // 签发时间
}
// 创建令牌
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 签名令牌
tokenString, err := token.SignedString(secretKey)
if err != nil {
fmt.Println("生成令牌失败:", err)
return
}
fmt.Println("生成的 JWT:", tokenString)
}
代码解析
- 导入库:我们导入了
jwt/v5
包来处理 JWT 的创建和验证。 - 密钥:
secretKey
是用于签名和验证的密钥,必须保密且足够复杂。 - 声明:
jwt.MapClaims
是一个通用的声明类型,允许你添加自定义字段(如user_id
和role
)。exp
表示过期时间,iat
表示签发时间。 - 签名方法:我们使用
jwt.SigningMethodHS256
(HMAC-SHA256)作为签名算法。 - 生成令牌:
token.SignedString
使用密钥对令牌进行签名,生成最终的 JWT 字符串。
运行上述代码将输出一个类似以下的 JWT 字符串:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjkwNzQyMzQsImlhdCI6MTcyODk4NzgzNCwicm9sZSI6ImFkbWluIiwidXNlcl9pZCI6MTIzfQ.3yXb4K5v6Qz8m9t2W1xY7Z8a9b0c1d2e3f4g5h6i7j8
验证 JWT
生成 JWT 后,服务器需要验证客户端发送的令牌是否有效。以下是一个验证 JWT 的示例:
package main
import (
"fmt"
"github.com/golang-jwt/jwt/v5"
)
func main() {
// 假设这是客户端发送的 JWT
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjkwNzQyMzQsImlhdCI6MTcyODk4NzgzNCwicm9sZSI6ImFkbWluIiwidXNlcl9pZCI6MTIzfQ.3yXb4K5v6Qz8m9t2W1xY7Z8a9b0c1d2e3f4g5h6i7j8"
// 定义密钥
secretKey := []byte("my-secret-key")
// 解析令牌
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// 验证签名方法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("意外的签名方法: %v", token.Header["alg"])
}
return secretKey, nil
})
if err != nil {
fmt.Println("解析令牌失败:", err)
return
}
// 验证令牌有效性
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
fmt.Println("令牌有效,声明内容:", claims)
fmt.Println("用户ID:", claims["user_id"])
fmt.Println("角色:", claims["role"])
} else {
fmt.Println("令牌无效")
}
}
代码解析
- 解析令牌:
jwt.Parse
解析 JWT 字符串,并通过回调函数提供密钥。 - 验证签名方法:确保令牌使用的是预期算法(这里是 HS256)。
- 验证有效性:检查令牌是否有效(未过期、签名正确等),并提取声明内容。
如果令牌有效,程序将输出类似以下内容:
令牌有效,声明内容: map[exp:1.729074234e+09 iat:1.728987834e+09 role:admin user_id:123]
用户ID: 123
角色: admin
在 REST API 中使用 JWT
在实际应用中,JWT 通常用于保护 API 端点。以下是一个使用 Go 的 net/http
包和 gorilla/mux
路由器实现的简单 REST API 示例,包含 JWT 认证。
首先,安装 gorilla/mux
:
go get github.com/gorilla/mux
以下是完整的代码:
package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/mux"
)
var secretKey = []byte("my-secret-key")
// 用户登录请求结构
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// 生成 JWT 的辅助函数
func generateJWT(userID int, role string) (string, error) {
claims := jwt.MapClaims{
"user_id": userID,
"role": role,
"exp": time.Now().Add(time.Hour * 24).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secretKey)
}
// 登录处理器
func loginHandler(w http.ResponseWriter, r *http.Request) {
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的请求", http.StatusBadRequest)
return
}
// 简单模拟用户验证
if req.Username == "admin" && req.Password == "password" {
token, err := generateJWT(123, "admin")
if err != nil {
http.Error(w, "生成令牌失败", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(map[string]string{"token": token})
} else {
http.Error(w, "认证失败", http.StatusUnauthorized)
}
}
// JWT 认证中间件
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")
if tokenString == "" {
http.Error(w, "缺少 Authorization 头", http.StatusUnauthorized)
return
}
// 移除 "Bearer " 前缀
if len(tokenString) > 7 && tokenString[:7] == "Bearer " {
tokenString = tokenString[7:]
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("意外的签名方法: %v", token.Header["alg"])
}
return secretKey, nil
})
if err != nil || !token.Valid {
http.Error(w, "无效的令牌", http.StatusUnauthorized)
return
}
// 将声明存储到上下文(可选)
if claims, ok := token.Claims.(jwt.MapClaims); ok {
fmt.Println("用户ID:", claims["user_id"])
}
next.ServeHTTP(w, r)
})
}
// 受保护的端点
func protectedHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"message": "欢迎访问受保护的端点!"})
}
func main() {
r := mux.NewRouter()
// 公开的登录端点
r.HandleFunc("/login", loginHandler).Methods("POST")
// 受保护的端点
protected := r.PathPrefix("/protected").Subrouter()
protected.Use(authMiddleware)
protected.HandleFunc("", protectedHandler).Methods("GET")
fmt.Println("服务器运行在 :8080")
http.ListenAndServe(":8080", r)
}
代码解析
- 登录端点:
/login
接受用户名和密码,验证通过后生成 JWT 并返回。 - 认证中间件:
authMiddleware
检查请求头中的Authorization
字段,验证 JWT 的有效性。 - 受保护的端点:
/protected
只有在提供有效 JWT 的情况下才能访问。 - Bearer 令牌:遵循标准的 Bearer 令牌格式,客户端需在请求头中设置
Authorization: Bearer <token>
。
运行程序后,你可以通过以下方式测试:
登录:
curl -X POST http://localhost:8080/login -d '{"username":"admin","password":"password"}'
返回类似:
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
访问受保护端点:
curl -H "Authorization: Bearer <your-token>" http://localhost:8080/protected
返回:
{"message":"欢迎访问受保护的端点!"}
安全注意事项
- 密钥管理:永远不要将密钥硬编码在代码中。使用环境变量或密钥管理服务。
- HTTPS:在生产环境中使用 HTTPS 加密传输 JWT,防止中间人攻击。
- 过期时间:设置合理的过期时间(如 24 小时),并考虑使用刷新令牌。
- 验证声明:在验证 JWT 时,检查所有必要声明(如
exp
和iat
)。 - 避免敏感信息:不要在 Payload 中存储敏感数据(如密码),因为 Payload 只是 Base64 编码,未加密。
总结
通过 Go 语言和 github.com/golang-jwt/jwt
库,我们可以轻松实现 JWT 的生成和验证。结合 gorilla/mux
等工具,Go 非常适合构建安全的 REST API。JWT 提供了一种高效、无状态的认证机制,适合现代 Web 应用开发。