我正在开发一个 Angular 应用程序,该应用程序使用accessToken 和refreshToken 进行身份验证。我的目标是即使用户长时间不活动也能保持登录状态。这是预期行为与实际行为:
预期行为:当用户长时间不活动后返回时,应使用refreshToken来获取新的accessToken,而无需手动重新登录。 实际行为:refreshToken 仅在第一次有效。长时间不活动(例如几个小时或几天)后,它会自动停止刷新,迫使用户手动注销并重新登录。 我怀疑自动刷新逻辑存在问题,在一定时间后会停止正常运行。相关代码如下:
interface CustomJwtPayload {
exp: number; // Timestamp de l'expiration
userId?: number; // ID utilisateur, optionnel
}
export class AuthService {
private baseUrl = 'http://localhost:8080/api';
private readonly ACCESS_TOKEN_EXPIRATION = 1000 * 60 * 15; // 15 minutes
private readonly REFRESH_TOKEN_EXPIRATION = 1000 * 60 * 60 * 24 * 7; // 7 jours
constructor(
private http: HttpClient,
@Inject(PLATFORM_ID) private platformId: Object,
private ngZone: NgZone
) {
this.setupAutoRefresh();
}
private getToken(): string | null {
if (isPlatformBrowser(this.platformId)) {
return localStorage.getItem('accessToken'); // Utilisez localStorage
}
return null;
}
private getRefreshToken(): string | null {
return localStorage.getItem('refreshToken'); // Utilisez localStorage
}
private storeNewAccessToken(token: string) {
if (isPlatformBrowser(this.platformId)) {
localStorage.setItem('accessToken', token); // Utilisez localStorage
}
}
public refreshToken(refreshToken: string): Observable<any> {
return this.http.post(`${this.baseUrl}/refresh-token`, { refreshToken }, {
headers: this.getHeaders()
}).pipe(
catchError(error => {
console.error('Token refresh failed', error);
return EMPTY; // Retourne un Observable vide pour éviter tout blocage
})
);
}
private setupAutoRefresh() {
if (isPlatformBrowser(this.platformId)) {
this.ngZone.runOutsideAngular(() => {
setInterval(() => {
if (this.isAccessTokenCloseToExpiration()) {
const refreshToken = this.getRefreshToken(); // Récupère le token de rafraîchissement
if (refreshToken) {
this.refreshToken(refreshToken).subscribe(newAccessToken => {
this.storeNewAccessToken(newAccessToken.token); // Stocke le nouveau token d'accès
});
}
}
}, 13 * 60 * 1000); // Appelle toutes les 13 minutes
});
}
}
private isAccessTokenCloseToExpiration(): boolean {
const decodedToken = this.decodeToken();
if (!decodedToken) return false;
const expirationTime = decodedToken.exp * 1000;
const currentTime = new Date().getTime();
return expirationTime - currentTime < 2 * 60 * 1000; // Moins de 2 minutes restantes
}
private decodeToken(): CustomJwtPayload | null {
const token = this.getToken();
if (!token) return null;
try {
return jwtDecode<CustomJwtPayload>(token);
} catch (error) {
console.error('Token decoding failed', error);
return null;
}
}
SPR环启动
private final long ACCESS_TOKEN_EXPIRATION = 1000 * 60 * 15;
private final long REFRESH_TOKEN_EXPIRATION = 1000 * 60 * 60 * 24 * 7;
public String extractUsername(String token) {
return extractClaim(token, Claims:: getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims:: getExpiration);
}
public < T > T extractClaim(String token, Function < Claims, T > claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
// Génération d'un Token d'Accès
public String generateAccessToken(String username, Long userId) {
Map < String, Object > claims = new HashMap<>();
claims.put("userId", userId); // Ajoutez l'ID utilisateur aux revendications
return createToken(claims, username, ACCESS_TOKEN_EXPIRATION, SECRET_KEY);
}
// Génération d'un Refresh Token
public String generateRefreshToken(String username, Long userId) {
Map < String, Object > claims = new HashMap<>();
claims.put("userId", userId); // Ajoutez l'ID utilisateur aux revendications
return createToken(claims, username, REFRESH_TOKEN_EXPIRATION, REFRESH_SECRET_KEY);
}
// Extraction de l'ID utilisateur à partir du token
public Long extractUserId(String token) {
final Claims claims = extractAllClaims(token);
return (Long) claims.get("userId");
}
// Création d'un JWT avec expiration personnalisée et clé donnée
private String createToken(Map < String, Object > claims, String subject, long expirationTime, Key key) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(key) // Utilisez la clé appropriée
.compact();
}
// Validation du Token d'Accès
public Boolean validateAccessToken(String token, String username) {
final String extractedUsername = extractUsername(token);
return (extractedUsername.equals(username) && !isTokenExpired(token));
}
// Validation du Refresh Token
public Boolean validateRefreshToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(REFRESH_SECRET_KEY)
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
// Vérification si le Token d'Accès est expiré
public Boolean isAccessTokenExpired(String token) {
return isTokenExpired(token);
}
@覆盖 protected void doFilterInternal(HttpServletRequest请求,HttpServletResponse响应,FilterChain链) 抛出 ServletException、IOException {
final String authorizationHeader = request.getHeader("Authorization");
final String refreshTokenHeader = request.getHeader("Refresh-Token");
String jwt = null;
String username = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
try {
username = jwtUtil.extractUsername(jwt);
} catch (ExpiredJwtException e) {
// Si le token d'accès est expiré, essayons de générer un nouveau token à partir du refresh token
if (refreshTokenHeader != null && jwtUtil.validateRefreshToken(refreshTokenHeader)) {
// Extraire le nom d'utilisateur à partir du refresh token
String refreshTokenUsername = jwtUtil.extractUsername(refreshTokenHeader);
String newAccessToken = jwtTokenRefreshService.refreshAccessToken(refreshTokenHeader, refreshTokenUsername);
response.setHeader("New-Access-Token", newAccessToken); // Retourne le nouveau token d'accès dans les en-têtes de réponse
} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Refresh token is invalid or not provided.");
return;
}
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("JWT token parsing failed.");
return;
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
try {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateAccessToken(jwt, username)) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid JWT token.");
return;
}
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Exception in setting authentication: " + e.getMessage());
return;
}
}
}
chain.doFilter(request, response);
}
写一个答案,因为我还不能发表评论......
通常在刷新访问令牌时,库会发出新的刷新令牌以及新的访问令牌,并使之前的刷新令牌失效。因此,您可能还需要替换存储的刷新令牌。