Go语言JWT登录验证教程
应使用github.com/golang-jwt/jwt/v5替代旧版jwt-go,因其修复了alg=none的严重安全漏洞,移除了SigningMethodNone,并强制限定合法算法;需嵌入jwt.RegisteredClaims、用jwt.WithValidMethods校验、密钥从环境变量加载、区分access/refreshtoken时效与存储,中间件统一返回标准HTTP状态码。
应使用 github.com/golang-jwt/jwt/v5 替代旧版 jwt-go,因其修复了 alg=none 的严重安全漏洞,移除了 SigningMethodNone,并强制限定合法算法;需嵌入 jwt.RegisteredClaims、用 jwt.WithValidMethods 校验、密钥从环境变量加载、区分 access/refresh token 时效与存储,中间件统一返回标准 HTTP 状态码。

为什么用 github.com/golang-jwt/jwt/v5 而不是旧版 jwt-go
旧版 jwt-go(v3 及之前)在 2023 年被发现存在严重安全缺陷:当 alg 字段为 none 且签名为空时,部分配置下会跳过验签直接返回 nil 错误,导致伪造 token 可绕过认证。v5 版本彻底移除了 SigningMethodNone,并强制要求显式指定合法算法列表。
实操建议:
- 必须使用
github.com/golang-jwt/jwt/v5,别用github.com/dgrijalva/jwt-go或github.com/golang-jwt/jwt(无 /v5 后缀的默认是 v4,已归档) - 初始化
jwt.Parse时,keyFunc必须返回具体算法(如jwt.SigningMethodHS256)和密钥,不能返回nil或泛型interface{} - 验证失败时,检查错误是否为
jwt.ErrTokenExpired、jwt.ErrTokenUnverifiable等具体类型,而非仅判空
ParseWithClaims 中如何安全提取用户 ID 和权限字段
JWT payload 是可读的,敏感字段(如密码、手机号)绝不能塞进 token;只放最小必要标识,比如 user_id 和 role。自定义 claims 需嵌入 jwt.RegisteredClaims,否则无法触发标准校验(如过期、签发时间)。
示例结构定义:
type CustomClaims struct {
UserID uint `json:"user_id"`
Role string `json:"role"`
jwt.RegisteredClaims
}解析时必须传入该结构指针,并用 jwt.WithValidMethods 限定算法:
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, 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(os.Getenv("JWT_SECRET")), nil
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))常见错误:
- 把
CustomClaims定义成非指针传给ParseWithClaims→ 解析成功但字段为空 - 忘记嵌入
jwt.RegisteredClaims→ExpiresAt等字段不参与自动校验 - 密钥硬编码在代码里 → 应从环境变量或 secret manager 加载
登录接口生成 token 时怎么控制有效期和刷新逻辑
不要用固定 24 小时;生产环境应区分 access token(短时效,15–60 分钟)和 refresh token(长时效,7 天,且需存 DB 绑定设备/IP/指纹)。access token 过期后,前端用 refresh token 换新 access token,而不是重新登录。
生成 access token 示例:
claims := &CustomClaims{
UserID: user.ID,
Role: user.Role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Subject: strconv.FormatUint(uint64(user.ID), 10),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))关键点:
ExpiresAt必须用jwt.NewNumericDate包装time.Time,直接赋值 int64 会导致解析失败- 不要在 token 里存密码哈希、session ID 等可逆信息
- refresh token 必须服务端存储(Redis + 随机 UUID),验证时比对是否匹配且未被撤回
中间件里怎么统一拦截未授权请求并返回标准错误
Go HTTP 中间件应直接操作 http.ResponseWriter 并提前 return,避免后续 handler 执行。JWT 验证失败时,状态码必须是 401 Unauthorized(未登录)或 403 Forbidden(权限不足),不能混用 400 或 500。
典型中间件写法:
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "missing Authorization header", http.StatusUnauthorized)
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "invalid Authorization format", http.StatusUnauthorized)
return
}
tokenString := parts[1]
claims := &CustomClaims{}
_, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil {
switch {
case errors.Is(err, jwt.ErrTokenExpired):
http.Error(w, "token expired", http.StatusUnauthorized)
case errors.Is(err, jwt.ErrTokenInvalidClaim):
http.Error(w, "invalid claim", http.StatusUnauthorized)
default:
http.Error(w, "invalid token", http.StatusUnauthorized)
}
return
}
ctx := context.WithValue(r.Context(), "user_id", claims.UserID)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}容易忽略的坑:
- 没检查
authHeader是否为空就strings.Split→ panic - 验证通过后没把用户信息注入
context,下游 handler 只能重复解析 token - 错误响应没设
Content-Type: application/json,前端难解析
refresh token 的吊销和轮换逻辑比 access token 复杂得多,尤其是分布式部署时,务必用 Redis 的原子操作(SET key val EX 604800 NX)保证唯一性和时效性。别依赖内存 map 或本地文件。
Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。
极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。
















