好的!我将以更细致的步骤和详细解释,手把手带你掌握这个库的用法。以下是完全从零开始的完整指南:
1. JWT 基础概念
什么是JWT?
- 全称:JSON Web Token
- 用途:安全地在两个系统之间传输信息(常用于身份验证)
- 结构:
Header.Payload.Signature
(用点分隔的三部分)
示例Token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImpvaG5fZG9lIiwiaXNzIjoibXktYXBwIiwiZXhwIjoxNjkzMjYwMDAwfQ.6Q9k_2XnRU0JzrqC7Z4Xqo4T7d8tVv6l1y3w0qWY3M4
- Header:描述算法和类型(如HS256)
- Payload:携带的数据(如用户ID、过期时间)
- Signature:前两部分的签名,防止篡改
2. 安装库
在终端执行:
go get github.com/golang-jwt/jwt/v5
3. 创建第一个JWT
步骤1:导入包
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
步骤2:定义Claims结构
// 自定义Claims结构
type MyClaims struct {
UserID int `json:"user_id"` // 自定义字段
jwt.RegisteredClaims // 内嵌标准字段(如过期时间、签发者等)
}
RegisteredClaims
包含标准字段:ExpiresAt
:过期时间Issuer
:签发者Subject
:主题- 等等
步骤3:生成Token
func generateToken() string {
// 1. 准备密钥(重要!实际使用要保密)
secretKey := []byte("your-256-bit-secret")
// 2. 创建Claims(数据载体)
claims := MyClaims{
UserID: 123, // 自定义数据
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)), // 1小时后过期
Issuer: "my-server", // 签发者标识
},
}
// 3. 创建Token对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 4. 生成签名字符串
signedToken, err := token.SignedString(secretKey)
if err != nil {
panic(err) // 实际应返回错误
}
return signedToken
}
4. 解析并验证JWT
步骤1:解析函数
func parseToken(tokenString string) (*MyClaims, error) {
// 1. 定义用于接收数据的Claims对象
claims := &MyClaims{}
// 2. 解析Token
parsedToken, err := jwt.ParseWithClaims(
tokenString,
claims,
func(t *jwt.Token) (interface{}, error) {
// 验证签名算法是否正确
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte("your-256-bit-secret"), nil
},
jwt.WithLeeway(5*time.Second), // 允许5秒时间误差
)
// 3. 处理错误
if err != nil {
return nil, err
}
// 4. 验证Claims是否有效
if !parsedToken.Valid {
return nil, fmt.Errorf("invalid token")
}
return claims, nil
}
步骤2:使用解析函数
func main() {
// 生成Token
token := generateToken()
fmt.Println("Generated Token:", token)
// 解析Token
claims, err := parseToken(token)
if err != nil {
fmt.Println("解析失败:", err)
return
}
fmt.Printf("用户ID: %d, 签发者: %s\n", claims.UserID, claims.Issuer)
}
5. 关键点详解
签名方法选择
- HS256(常用):对称加密,服务端用同一个密钥签名和验证
- RS256:非对称加密,用私钥签名,公钥验证(更安全)
- 其他支持的方法:ES256, EdDSA等
Claims验证细节
自动验证的标准字段:
ExpiresAt
:检查是否过期NotBefore
:是否已生效Issuer
:签发者是否匹配(需配合jwt.WithIssuer()
选项)
- 自定义验证:通过
jwt.WithValidator()
添加
错误处理示例
claims, err := parseToken(invalidToken)
if err != nil {
switch {
case errors.Is(err, jwt.ErrTokenMalformed):
fmt.Println("根本不是有效的JWT格式")
case errors.Is(err, jwt.ErrTokenExpired):
fmt.Println("Token已过期")
case errors.Is(err, jwt.ErrTokenSignatureInvalid):
fmt.Println("签名不匹配,可能被篡改")
default:
fmt.Println("其他错误:", err)
}
return
}
6. 完整可运行示例
package main
import (
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
type MyClaims struct {
UserID int `json:"user_id"`
jwt.RegisteredClaims
}
func main() {
// 生成Token
secret := []byte("your-secret-key-here")
tokenString, err := createToken(secret)
if err != nil {
panic(err)
}
fmt.Println("生成Token:", tokenString)
// 解析验证
claims, err := parseAndValidate(tokenString, secret)
if err != nil {
fmt.Println("验证失败:", err)
return
}
fmt.Printf("验证成功!用户ID: %d, 签发者: %s\n", claims.UserID, claims.Issuer)
}
func createToken(secret []byte) (string, error) {
claims := MyClaims{
UserID: 1001,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
Issuer: "my-auth-server",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secret)
}
func parseAndValidate(tokenString string, secret []byte) (*MyClaims, error) {
claims := &MyClaims{}
token, err := jwt.ParseWithClaims(
tokenString,
claims,
func(t *jwt.Token) (interface{}, error) {
if t.Method.Alg() != jwt.SigningMethodHS256.Name {
return nil, errors.New("invalid signing algorithm")
}
return secret, nil
},
jwt.WithIssuer("my-auth-server"),
)
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
7. 常见问题解决
Q1:如何添加自定义字段?
在自定义Claims结构体中添加字段即可:
type MyClaims struct {
Username string `json:"username"`
Role string `json:"role"`
jwt.RegisteredClaims
}
Q2:如何处理过期时间?
- 生成时设置
ExpiresAt
- 解析时自动验证过期,如果过期会返回
jwt.ErrTokenExpired
Q3:密钥泄露怎么办?
- 立即更换新密钥
- 使所有基于旧密钥的Token失效
- 推荐使用环境变量存储密钥,不要硬编码在代码中
8. 安全最佳实践
- 永远不要在客户端存储敏感数据(如密码)
- 使用足够强度的密钥(HS256至少32字节随机字符串)
- 设置合理的过期时间(如15分钟-1小时)
- 启用HTTPS防止Token被窃听
- 考虑使用刷新令牌(Refresh Token)机制
通过这个详细指南,你应该能够:
- 理解JWT的基本工作原理
- 使用
golang-jwt/jwt/v5
生成安全的Token - 正确解析和验证收到的Token
- 处理各种常见错误情况
实际开发时,建议将密钥管理、错误处理等封装成独立的安全模块。