You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular2基于JWT的认证实现:用户活跃时Token自动刷新方案咨询

实现基于JWT的Token自动刷新(Angular + Spring)

这需求太普遍了,我在好几个项目里都这么落地过,分享下实际可行的方案,分后端Spring和前端Angular两部分拆解:

一、后端Spring的核心调整

首先得把JWT拆成Access Token和Refresh Token两个角色:

  • Access Token:保持15分钟过期,用来做日常接口的身份校验,过期时间短更安全
  • Refresh Token:过期时间设长一点(比如7天),专门用来申请新的Access Token,相当于用户的"长期通行证"

1. 登录接口返回双Token

登录成功时,同时生成并返回这两个Token,响应体示例:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "accessExpiresIn": 900, // 15分钟(秒)
  "refreshExpiresIn": 604800 // 7天(秒)
}

2. 实现Refresh Token刷新接口

写一个POST接口(比如/api/auth/refresh),接收前端传来的Refresh Token,做这几步校验:

  • 验证Token签名合法性
  • 检查Token是否过期
  • 从数据库/Redis中校验该Refresh Token是否绑定当前用户(防止被盗用后注销仍能使用)

校验通过后,生成新的Access Token(推荐同时生成新的Refresh Token,滚动刷新更安全),返回给前端。

Spring Boot示例代码:

@PostMapping("/refresh")
public ResponseEntity<AuthResponse> refreshToken(@RequestBody RefreshTokenRequest request) {
    // 解析Refresh Token获取用户信息
    Claims claims = jwtUtil.extractAllClaims(request.getRefreshToken());
    String username = claims.getSubject();
    
    // 从数据库查询用户,校验Refresh Token有效性
    User user = userService.findByUsername(username);
    if (!jwtUtil.validateToken(request.getRefreshToken(), user) || 
        !request.getRefreshToken().equals(user.getRefreshToken())) {
        throw new UnauthorizedException("无效的刷新令牌");
    }
    
    // 生成新的双Token
    String newAccessToken = jwtUtil.generateToken(user);
    String newRefreshToken = jwtUtil.generateRefreshToken(user);
    
    // 更新数据库中的Refresh Token
    user.setRefreshToken(newRefreshToken);
    userService.save(user);
    
    return ResponseEntity.ok(new AuthResponse(newAccessToken, newRefreshToken, 900, 604800));
}

3. 后端安全注意事项

  • Refresh Token必须存在持久化存储(数据库/Redis),绑定用户ID,注销时立即清除对应记录
  • Refresh Token过期时间别太长,平衡安全性和用户体验
  • 给刷新接口加限流,防止恶意刷接口攻击

二、前端Angular的自动刷新逻辑

核心是用HttpInterceptor拦截所有请求,自动处理Token过期和刷新,让用户完全无感知。

1. 封装AuthService管理Token

写一个服务来统一存储、获取、更新Token:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';

@Injectable({ providedIn: 'root' })
export class AuthService {
  private readonly ACCESS_TOKEN_KEY = 'access_token';
  private readonly REFRESH_TOKEN_KEY = 'refresh_token';
  private readonly ACCESS_EXPIRES_KEY = 'access_expires';

  constructor(private http: HttpClient, private router: Router) {}

  getAccessToken(): string | null {
    return localStorage.getItem(this.ACCESS_TOKEN_KEY);
  }

  getRefreshToken(): string | null {
    return localStorage.getItem(this.REFRESH_TOKEN_KEY);
  }

  setTokens(accessToken: string, refreshToken: string, accessExpiresIn: number): void {
    const expiresAt = Date.now() + accessExpiresIn * 1000;
    localStorage.setItem(this.ACCESS_TOKEN_KEY, accessToken);
    localStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken);
    localStorage.setItem(this.ACCESS_EXPIRES_KEY, expiresAt.toString());
  }

  isAccessTokenExpiring(): boolean {
    const expiresAt = parseInt(localStorage.getItem(this.ACCESS_EXPIRES_KEY) || '0', 10);
    // 提前3分钟判断是否要刷新,避免请求时刚好过期
    return Date.now() > expiresAt - 3 * 60 * 1000;
  }

  async refreshToken(): Promise<any> {
    const refreshToken = this.getRefreshToken();
    if (!refreshToken) throw new Error('没有可用的刷新令牌');
    
    return this.http.post('/api/auth/refresh', { refreshToken }).toPromise();
  }

  logout(): void {
    localStorage.removeItem(this.ACCESS_TOKEN_KEY);
    localStorage.removeItem(this.REFRESH_TOKEN_KEY);
    localStorage.removeItem(this.ACCESS_EXPIRES_KEY);
    this.router.navigate(['/login']);
  }
}

2. 实现HttpInterceptor拦截请求

拦截所有HTTP请求,自动处理Token过期逻辑:

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpInterceptor,
  HttpErrorResponse
} from '@angular/common/http';
import { catchError, switchMap, filter, take, throwError } from 'rxjs/operators';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  private isRefreshing = false;
  private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);

  constructor(private authService: AuthService) {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
    // 登录和刷新接口直接放行
    if (request.url.includes('/api/auth/login') || request.url.includes('/api/auth/refresh')) {
      return next.handle(request);
    }

    const accessToken = this.authService.getAccessToken();
    if (accessToken) {
      // 检查Token是否快过期
      if (this.authService.isAccessTokenExpiring()) {
        return this.handleRefreshToken(request, next);
      }
      // 给请求加上Authorization头
      request = this.addToken(request, accessToken);
    }

    return next.handle(request).pipe(
      catchError((error: HttpErrorResponse) => {
        // 401说明Access Token已过期,尝试刷新
        if (error.status === 401 && !request.url.includes('/api/auth/refresh')) {
          return this.handleRefreshToken(request, next);
        }
        // 其他错误(比如Refresh Token也过期)直接登出
        this.authService.logout();
        return throwError(() => error);
      })
    );
  }

  private handleRefreshToken(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
    if (!this.isRefreshing) {
      this.isRefreshing = true;
      this.refreshTokenSubject.next(null);

      return this.authService.refreshToken().pipe(
        switchMap((tokens: any) => {
          this.isRefreshing = false;
          this.refreshTokenSubject.next(tokens.accessToken);
          // 更新本地Token
          this.authService.setTokens(tokens.accessToken, tokens.refreshToken, tokens.accessExpiresIn);
          // 重新发送原请求
          return next.handle(this.addToken(request, tokens.accessToken));
        }),
        catchError((error) => {
          this.isRefreshing = false;
          this.authService.logout();
          return throwError(() => error);
        })
      );
    } else {
      // 正在刷新时,等待刷新完成后再发请求
      return this.refreshTokenSubject.pipe(
        filter(token => token !== null),
        take(1),
        switchMap(token => {
          return next.handle(this.addToken(request, token));
        })
      );
    }
  }

  private addToken(request: HttpRequest<any>, token: string): HttpRequest<any> {
    return request.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
  }
}

3. 注册拦截器

在app.module.ts中注册拦截器,让它生效:

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';

@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ]
})
export class AppModule {}

三、关键细节提醒

  • 并发请求处理:拦截器里的isRefreshing标志和BehaviorSubject能避免多个请求同时触发刷新,只发一次刷新请求,其他请求等待结果
  • 存储选择:敏感系统推荐用sessionStorage代替localStorage,浏览器关闭后自动清除,安全性更高
  • 异常兜底:如果Refresh Token也过期了,一定要直接跳转到登录页,不能无限循环刷新
  • 注销同步:用户主动注销时,后端要立即删除数据库里的Refresh Token,防止被恶意使用

内容的提问来源于stack exchange,提问作者syv

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 08:45:26