在 Web 應用開發中,用戶登錄狀態的管理至關重要。為了避免用戶頻繁遇到登錄過期的問題,我們可以通過實現 JWT(JSON Web Token)刷新機制來提升用戶體驗
推薦: 使用 Refresh Token(雙 Token 機制)
1. 生成和使用雙 Token
通常會生成兩種 Token:訪問 Token (Access Token)?和?刷新 Token (Refresh Token)。
-
??訪問 Token:用于客戶端與服務器之間的身份驗證,有效期較短(例如 30 分鐘),以提高安全性。
-
??刷新 Token:用于獲取新的訪問 Token,有效期較長(例如 7 天),存儲在客戶端 。
Tokenservice.cs:
?
// 生成 JWT Access Token 和 Refresh Token
public?(string?AccessToken,?string?RefreshToken) GenerateTokens(string?userId,?string?userName)
{var?tokenHandler =?new?JwtSecurityTokenHandler();var?key = Encoding.ASCII.GetBytes(_configuration["JwtSettings:Secret"]);// 生成 Access Tokenvar?accessTokenDescriptor =?new?SecurityTokenDescriptor{Subject =?new?ClaimsIdentity(new?Claim[]{new?Claim(ClaimTypes.NameIdentifier, userId),new?Claim(ClaimTypes.Name, userName)}),Issuer = _configuration["JwtSettings:Issuer"],Audience = _configuration["JwtSettings:Audience"],Expires = DateTime.UtcNow.AddMinutes(double.Parse(_configuration["JwtSettings:ExpireMinutes"])),SigningCredentials =?new?SigningCredentials(new?SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)};var?accessToken = tokenHandler.CreateToken(accessTokenDescriptor);// 生成 Refresh Tokenvar?refreshTokenDescriptor =?new?SecurityTokenDescriptor{Subject =?new?ClaimsIdentity(new?Claim[]{new?Claim(ClaimTypes.NameIdentifier, userId)}),Issuer = _configuration["JwtSettings:Issuer"],Audience = _configuration["JwtSettings:Audience"],Expires = DateTime.UtcNow.AddDays(7),?// Refresh Token 有效期 7 天SigningCredentials =?new?SigningCredentials(new?SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)};var?refreshToken = tokenHandler.CreateToken(refreshTokenDescriptor);return?(tokenHandler.WriteToken(accessToken), tokenHandler.WriteToken(refreshToken));
}// 根據舊 JWT 令牌換取新 JWT 令牌public?string?ExchangeJwtToken(string?oldToken){if?(!ValidateJwtToken(oldToken)){thrownew?InvalidOperationException("Invalid or expired token");}var?principal = ParseJwtToken(oldToken);var?userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;var?userName = principal.FindFirst(ClaimTypes.Name)?.Value;if?(string.IsNullOrEmpty(userId) ||?string.IsNullOrEmpty(userName)){thrownew?InvalidOperationException("Invalid token claims");}return?GenerateJwtToken(userId, userName);}
AuthController.cs
private?readonly?TokenService _tokenService;
privatereadonly?IAppUser _appUser;public?AuthController(IConfiguration configuration, IAppUser appUser, TokenService tokenService)
{_tokenService = tokenService;_appUser = appUser;
}[HttpPost("login")]
[AllowAnonymous]
public?IActionResult?Login([FromBody] LoginRequest loginRequest)
{var?token = _tokenService.GenerateTokens(loginRequest.Username, loginRequest.Username);return?Ok(new?{ token.RefreshToken,token.AccessToken });
}
2. 前端請求攔截器自動刷新 Token
在前端應用中,可以使用請求攔截器來自動處理 Token 刷新邏輯。當訪問 Token 過期時,攔截器會自動調用刷新接口獲取新的訪問 Token,并重新發起請求
// ?請求攔截器
axios.interceptors.request.use(
config?=>?{const?token =?localStorage.getItem('accessToken');if?(token) {config.headers['Authorization'] =?`Bearer?${token}`;}return?config;},
error?=>?{returnPromise.reject(error);}
);axios.interceptors.response.use(
response?=>?response,
async?error => {const?originalRequest = error.config;if?(error.response.status?===?401?&& !originalRequest._retry) {originalRequest._retry?=?true;const?refreshToken =?localStorage.getItem('refreshToken');const?res =?await?axios.post('/api/auth/refresh-token', { refreshToken });localStorage.setItem('accessToken', res.data.accessToken);returnaxios(originalRequest);}returnPromise.reject(error);}
);
3. 后端提供刷新 Token 的接口
專門的接口來處理刷新 Token 的請求。該接口會驗證刷新 Token 的有效性,并返回新的訪問 Token
?// 刷新 Access Token
[HttpPost("refresh")]
public?IActionResult?RefreshToken([FromBody] RefreshTokenRequest request)
{try{var?validatetoken = _tokenService.ValidateJwtToken(request.RefreshToken);if?(validatetoken){return?Ok(_tokenService.ExchangeJwtToken(request.RefreshToken));}return?Unauthorized("Invalid refresh token");}catch?(Exception ex){return?Unauthorized("Invalid refresh token");}
}
不推薦:中間件自動刷新Token
-
1. 創建中間件:創建一個中間件,用于檢查每個請求的JWT Token過期時間。
-
2. 讀取Token:中間件讀取請求頭中的Authorization字段,獲取JWT Token。
-
3. 檢查過期時間:判斷Token的過期時間,如果距離過期時間在一定范圍內(如30分鐘內),則生成一個新的JWT Token,并通過自定義的響應頭(如X-Refresh-Token)返回給客戶端。
-
4. 客戶端更新Token:客戶端檢查響應頭,如果存在X-Refresh-Token,則用新Token替換舊Token,并在后續請求中使用新Token。
-
5. 弊端:雖然實現起來相對簡單,但安全性相對較低,因為Token的刷新是在客戶端自動進行的,如果Token被盜用,攻擊者可能會在Token過期前一直使用該Token
因為存在上述弊端,不推薦使用中間件自動刷新 Token,故不提供相關代碼實現。