在使用 JSON Web Token(JWT) 进行身份验证的过程中,如何处理令牌过期是一个关键问题。由于 JWT 的无状态特性,一旦令牌过期,用户必须重新登录获取新的令牌,这无疑会影响用户体验。为了解决这一问题,通常会引入 刷新令牌(Refresh Token) 机制,允许用户在不重新登录的情况下获取新的访问令牌。
本文将详细介绍 如何处理 JWT 令牌过期,以及 刷新令牌的实现逻辑,帮助你更好地理解这一过程,并在实际项目中灵活运用。
一、JWT 令牌过期处理
1. 设置合理的过期时间
JWT 中有一个重要的声明——exp(Expiration Time),用于指定令牌的过期时间戳。通过合理设置这个值,可以在一定程度上平衡安全性和用户体验。
- 短期令牌:如 15 分钟或 30 分钟的有效期,适用于对安全性要求较高的场景,可以减少令牌被窃取后利用的风险。
- 长期令牌:对于一些低敏感度的应用,可以适当延长有效期,但不应过长,以免增加安全风险。
{
"exp": 1691049422 // Unix 时间戳,表示 2023-08-03 14:37:02 UTC
}
2. 检测令牌过期
当客户端发送带有已过期 JWT 的请求时,服务器可以通过以下方式检测:
- 解析失败:尝试解析 JWT 时,如果
exp声明的时间早于当前时间,则抛出异常。 - 返回特定错误码:常见的 HTTP 状态码是
401 Unauthorized,并附带提示信息,告知客户端令牌已过期。
func validateToken(tokenString string) (*jwt.Token, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(secretKey), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
if exp, ok := claims["exp"].(float64); ok && time.Now().Unix() > int64(exp) {
return nil, errors.New("token expired")
}
return token, nil
}
return nil, errors.New("invalid token")
}
3. 处理过期令牌
一旦检测到令牌过期,服务器应返回相应的错误响应,提示客户端需要重新获取新的令牌。此时,客户端有两种选择:
- 重新登录:最简单的方式是让用户重新输入用户名和密码进行登录。
- 使用刷新令牌:如果启用了刷新令牌机制,客户端可以使用刷新令牌来获取新的访问令牌,而无需重新登录。
二、刷新令牌的实现逻辑
1. 什么是刷新令牌?
刷新令牌是一种特殊的令牌,专门用于从授权服务器获取新的访问令牌。它的主要特点包括:
- 较长的有效期:相比访问令牌,刷新令牌的有效期更长,通常为数天甚至数周。
- 高安全性:刷新令牌应存储在安全的地方(如 HttpOnly Cookie 或本地存储),并且仅在 HTTPS 协议下传输。
- 可撤销性:为了提高安全性,刷新令牌应支持撤销功能,以便在发现异常情况时立即失效。
2. 刷新令牌的工作流程
步骤 1:初次登录
用户首次登录成功后,服务器生成两个令牌:
- 访问令牌(Access Token):短期有效的 JWT,用于访问受保护的资源。
- 刷新令牌(Refresh Token):长期有效的令牌,用于在访问令牌过期后获取新的访问令牌。
func generateTokens(userID string) (string, string, error) {
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": userID,
"exp": time.Now().Add(time.Minute * 30).Unix(),
})
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": userID,
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(), // 刷新令牌有效期为一周
})
accessTokenString, err := accessToken.SignedString([]byte(secretKey))
if err != nil {
return "", "", err
}
refreshTokenString, err := refreshToken.SignedString([]byte(refreshSecretKey))
if err != nil {
return "", "", err
}
return accessTokenString, refreshTokenString, nil
}
步骤 2:访问受保护资源
客户端在每次请求受保护资源时,都会在 HTTP 请求头中携带访问令牌。
Authorization: Bearer <access_token>
步骤 3:令牌过期
当访问令牌过期时,服务器返回 401 Unauthorized 错误,并提示客户端使用刷新令牌获取新的访问令牌。
步骤 4:使用刷新令牌获取新令牌
客户端收到 401 错误后,使用刷新令牌向 /refresh-token 端点发起请求。
POST /refresh-token HTTP/1.1
Content-Type: application/json
{
"refresh_token": "<refresh_token>"
}
服务器接收到请求后,首先验证刷新令牌的有效性:
- 检查签名:确保刷新令牌未被篡改。
- 验证过期时间:确认刷新令牌是否仍在有效期内。
- 检查黑名单:如果刷新令牌已被撤销,则拒绝请求。
如果验证通过,服务器生成新的访问令牌和刷新令牌,并将其返回给客户端。
func refreshAccessToken(refreshTokenString string) (string, string, error) {
token, err := jwt.Parse(refreshTokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(refreshSecretKey), nil
})
if err != nil || !token.Valid {
return "", "", errors.New("invalid refresh token")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return "", "", errors.New("cannot convert claims to map")
}
newAccessToken, newRefreshToken, err := generateTokens(claims["sub"].(string))
if err != nil {
return "", "", err
}
return newAccessToken, newRefreshToken, nil
}
步骤 5:更新客户端存储
客户端收到新的访问令牌和刷新令牌后,更新本地存储中的令牌信息,继续使用新的访问令牌进行后续请求。
三、刷新令牌的安全考虑
尽管刷新令牌提供了便利,但也带来了额外的安全挑战。以下是几个关键的安全措施:
1. HTTPS 必须启用
所有涉及令牌传输的操作都应在 HTTPS 下进行,以防止中间人攻击窃取令牌。
2. 刷新令牌应加密存储
刷新令牌应存储在安全的位置,如 HttpOnly Cookie 或加密后的本地存储中,避免泄露给恶意脚本。
3. 定期轮换密钥
无论是访问令牌还是刷新令牌,都应定期更换签名密钥,增强安全性。
4. 刷新令牌黑名单
当用户登出或刷新令牌被盗用时,应立即将其加入黑名单,禁止进一步使用。
5. 限制刷新令牌的使用次数
可以通过记录刷新令牌的使用次数或最近一次使用时间,限制其频繁使用,降低被盗用的风险。
四、总结
通过引入 刷新令牌机制,我们可以有效地解决 JWT 令牌过期 导致的用户体验问题,同时保持系统的安全性。在实际应用中,合理设置访问令牌和刷新令牌的有效期,结合多种安全措施,能够显著提升系统的健壮性和可靠性。
希望这篇文章能帮助你深入理解 JWT 令牌过期处理与刷新令牌机制,并在实际项目中灵活运用这些技术。
2万+

被折叠的 条评论
为什么被折叠?



