[每周一更]-(第147期):使用 Go 语言实现 JSON Web Token (JWT)

什么是 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 的工作原理

  1. 生成:服务器验证用户身份后,生成 JWT(包含用户信息和签名)并发送给客户端。
  2. 传递:客户端将 JWT 存储(通常在 localStorage 或 cookie 中),并在后续请求中通过 Authorization 头(如 Bearer )发送。
  3. 验证:服务器接收 JWT,验证签名和声明(如是否过期),确认合法后处理请求。

JWT 的特点

  • 无状态:服务器无需存储令牌,验证仅依赖密钥和令牌本身,适合分布式系统。
  • 跨域支持:JWT 可在不同域名间传递,适合微服务架构。
  • 紧凑:Base64 编码使其适合 HTTP 头传输。
  • 安全性:通过签名防止篡改,但 Payload 未加密,需避免存储敏感数据。

为什么在 Go 中使用 JWT?

Go 语言以其简洁、高性能和强大的标准库而闻名,非常适合构建安全的后端服务。在 Go 中使用 JWT 可以轻松实现用户认证和授权,特别是在构建微服务或 REST API 时。

准备工作

在开始之前,确保你的环境中已安装 Go(建议版本 1.16 或更高)。我们将使用 github.com/golang-jwt/jwt 库来处理 JWT。

首先,安装该库:

bash 复制代码
go get github.com/golang-jwt/jwt/v5

创建 JWT

以下是一个简单的 Go 程序示例,展示如何生成一个 JWT 令牌。

go 复制代码
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)
}

代码解析

  1. 导入库 :我们导入了 jwt/v5 包来处理 JWT 的创建和验证。
  2. 密钥secretKey 是用于签名和验证的密钥,必须保密且足够复杂。
  3. 声明jwt.MapClaims 是一个通用的声明类型,允许你添加自定义字段(如 user_idrole)。exp 表示过期时间,iat 表示签发时间。
  4. 签名方法 :我们使用 jwt.SigningMethodHS256(HMAC-SHA256)作为签名算法。
  5. 生成令牌token.SignedString 使用密钥对令牌进行签名,生成最终的 JWT 字符串。

运行上述代码将输出一个类似以下的 JWT 字符串:

复制代码
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjkwNzQyMzQsImlhdCI6MTcyODk4NzgzNCwicm9sZSI6ImFkbWluIiwidXNlcl9pZCI6MTIzfQ.3yXb4K5v6Qz8m9t2W1xY7Z8a9b0c1d2e3f4g5h6i7j8

验证 JWT

生成 JWT 后,服务器需要验证客户端发送的令牌是否有效。以下是一个验证 JWT 的示例:

go 复制代码
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("令牌无效")
	}
}

代码解析

  1. 解析令牌jwt.Parse 解析 JWT 字符串,并通过回调函数提供密钥。
  2. 验证签名方法:确保令牌使用的是预期算法(这里是 HS256)。
  3. 验证有效性:检查令牌是否有效(未过期、签名正确等),并提取声明内容。

如果令牌有效,程序将输出类似以下内容:

复制代码
令牌有效,声明内容: 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

bash 复制代码
go get github.com/gorilla/mux

以下是完整的代码:

go 复制代码
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)
}

代码解析

  1. 登录端点/login 接受用户名和密码,验证通过后生成 JWT 并返回。
  2. 认证中间件authMiddleware 检查请求头中的 Authorization 字段,验证 JWT 的有效性。
  3. 受保护的端点/protected 只有在提供有效 JWT 的情况下才能访问。
  4. Bearer 令牌 :遵循标准的 Bearer 令牌格式,客户端需在请求头中设置 Authorization: Bearer <token>

运行程序后,你可以通过以下方式测试:

  1. 登录

    bash 复制代码
    curl -X POST http://localhost:8080/login -d '{"username":"admin","password":"password"}'

    返回类似:

    json 复制代码
    {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
  2. 访问受保护端点

    bash 复制代码
    curl -H "Authorization: Bearer <your-token>" http://localhost:8080/protected

    返回:

    json 复制代码
    {"message":"欢迎访问受保护的端点!"}

安全注意事项

  1. 密钥管理:永远不要将密钥硬编码在代码中。使用环境变量或密钥管理服务。
  2. HTTPS:在生产环境中使用 HTTPS 加密传输 JWT,防止中间人攻击。
  3. 过期时间:设置合理的过期时间(如 24 小时),并考虑使用刷新令牌。
  4. 验证声明 :在验证 JWT 时,检查所有必要声明(如 expiat)。
  5. 避免敏感信息:不要在 Payload 中存储敏感数据(如密码),因为 Payload 只是 Base64 编码,未加密。

总结

通过 Go 语言和 github.com/golang-jwt/jwt 库,我们可以轻松实现 JWT 的生成和验证。结合 gorilla/mux 等工具,Go 非常适合构建安全的 REST API。JWT 提供了一种高效、无状态的认证机制,适合现代 Web 应用开发。

相关推荐
OpenTiny社区6 分钟前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
编程猪猪侠35 分钟前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞39 分钟前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路1 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到111 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构
风清云淡_A1 小时前
【REACT18.x】CRA+TS+ANTD5.X封装自定义的hooks复用业务功能
前端·react.js