首页 > 解决方案 > 使用来自 Spring Boot 后端的 Angular 11 拦截器刷新 JWT 过期令牌

问题描述

我正在开发一个 Spring Boot + Angular 项目,其中用户从 Angular 前端登录到 Spring Boot 上的身份验证 API,该 API 返回一个 JWT 令牌。我还在 Angular 上设置了一个拦截器,它为所有请求附加了带有 JWT 令牌的 Authorization 标头。

我正在寻找一种拦截角度请求的方法,以便当 JWT 令牌过期后 Spring Boot 引发 401 错误时,Angular 前端将尝试使用过期的 JWT 和新的“isRefreshToken”标头集联系新的 refreshtoken 端点为 true 以接收新的 JWT。

这是我当前的 AuthService

    @Injectable({
  providedIn: 'root',
})
export class AuthService {

  constructor(private http: HttpClient) {}

  login(username: string, password: string) {
    return this.http
      .post<iUser>('http://localhost:8080/authenticate', { username, password }).pipe(
        tap(res => this.setSession(res)),
        shareReplay()
      )      
  }

  refreshToken(){
    return this.http.post<iUser>('http://localhost:8080/refreshtoken', {responseType: 'text' as 'json'}).pipe(
      tap(res => this.setSession(res)),
      shareReplay()
    )
  }

  private setSession(authResult) {

    let tokenInfo = this.getDecodedAccessToken(authResult.token);   
    const expiresAt = moment(tokenInfo.exp);

    localStorage.setItem('id_token', authResult.token);
    localStorage.setItem('expires_at', JSON.stringify(expiresAt.valueOf()));
    localStorage.setItem('userId', tokenInfo.userId);

  }

  logout() {
    localStorage.removeItem('id_token');
    localStorage.removeItem('expires_at');
    localStorage.removeItem('userId');
  }

  public isLoggedIn() {
    return moment().isBefore(this.getExpiration());
  }

  isLoggedOut() {
    return !this.isLoggedIn();
  }

  getExpiration() {
    const expiration = localStorage.getItem('expires_at');
    const expiresAt = JSON.parse(expiration);
    return moment.unix(expiresAt);
  }

  getDecodedAccessToken(token: string): any {
    try{
        return jwt_decode(token);
    }
    catch(Error){
        return null;
    }
  }
}

虽然这是我正在使用的拦截器:

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private router: Router, private authService: AuthService){}


  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {

    let url = req.url.includes('localhost');

    const idToken = localStorage.getItem('id_token');

    if (idToken && url) {  

      const cloned = req.clone({
        headers: req.headers.set('Authorization', 'Bearer ' + idToken),
      });

      console.log(cloned);

      return next.handle(cloned);

    } else {
      return next.handle(req);
    }
  }
}

标签: javaangularspringspring-bootjwt

解决方案


我可以向您建议另一种执行此操作的方法,我最近使用该方法在令牌过期时注销用户。先分享一下我的做法:

loginUser(email: string, password: string) {
    const authData: AuthData = { email: email, password: password };
    this.http.post<{ token: string, expiresIn: number }>('http://localhost:3000/api/users/login', authData).subscribe( response => {
      const token = response.token;
      this.token = token;
      if(token) {
        const expiresInDuration = response.expiresIn;
        this.tokenTimer = setTimeout(() => {
          this.logout();
        }, expiresInDuration*1000);
        this.isAuthenticated = true;
        this.authStatusListener.next(true);
        const now = new Date();
        const expirationDate = new Date(now.getTime() + (expiresInDuration * 1000));
        this.saveAuthData(token, expirationDate);
        this.router.navigate(['']);
      }
    });
  }

logout() {
    this.token = null;
    this.isAuthenticated = false;
    this.authStatusListener.next(false);
    clearTimeout(this.tokenTimer);
    this.clearAuthData();
    this.router.navigate(['']);
  }

  private saveAuthData(token: string, expirationDate: Date) {
    localStorage.setItem('token', token);
    localStorage.setItem('expirationDate', expirationDate.toISOString());
  }

所以我在这里所做的是我收到了一个expireIn以秒为单位的令牌过期的值。然后我设置了一个timeout回调方法,当到达该时间时将调用该方法。在这里,我已在您的情况下注销,您可以调用刷新令牌的 API 并根据您的要求进行所需的更改。所以回到这一点,我已经设置了与当前登录日期时间和持续时间相关的到期日期和expiresIn时间。还添加了一个authStatusListener Subject将监听身份验证状态直到它过期的功能。

在拦截器/守卫中没有额外的事情可做令牌过期

要控制标题中的注销按钮,只需执行以下操作:

 userIsAuthenticated = false;
  private authListenerSubs: Subscription;
  constructor(private authService: AuthService) { }

  onLogout() {
    this.authService.logout();
  }

  ngOnInit() {
    this.authListenerSubs = this.authService.getAuthStatusListener().subscribe(isAuthenticated => {
      this.userIsAuthenticated = isAuthenticated;
    });
  }

  ngOnDestroy() {
    this.authListenerSubs.unsubscribe();
  }

userIsAuthenticated ngIfHTML 中使用。

对于一个真实的场景,你可以借助这个github repo


推荐阅读