如何使用拦截器+路由防护自动刷新访问令牌?

时间:2020-08-14 00:11:34

标签: angular jwt

我浏览了很多文章和问题,但是似乎都没有涵盖拦截器中的自动令牌刷新功能,同时还具有一个路由守卫,它等待对服务器的请求完成以验证访问令牌是否有效?

auth.guard.ts

canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {

    return this.authService.isAuthenticated()
      .pipe(
        map(
          res => {
            if (res.success === true) {
              return true;
            } else {
              this.authService.deleteToken();
              this.router.navigateByUrl('/login');
              return false;
            }
          }
        ), catchError(err => {
          // NOTE: If I put the code for refresh-token request here, it works -- user stays
          // logged in after getting new access token

          this.authService.deleteToken();
          this.router.navigate(['/login']);
          return of(false);
        })
      );
  }

auth.interceptor.ts

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (req.headers.get('noauth')) {
      return next.handle(req.clone());
    } else {
      const clonedReq = req.clone({
        headers: req.headers.set('Authorization', 'Bearer ' + this.authService.getToken())
      });

      return next.handle(clonedReq).pipe(
        tap(
          event => {},
          err => {
            if (err.error.code && err.error.code === 'EXPIRED') {
                return this.handle401Error(clonedReq, next);
            }
          }
        )
      );
    }
  }

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

      return this.authService.refreshToken().pipe(
        switchMap((token: any) => {
          this.isRefreshing = false;
          this.refreshTokenSubject.next(token.new_access_token);
          return next.handle(this.addToken(request, token.new_access_token));
        }),
        catchError(err => {
          return throwError(err);
        }))
        .subscribe(r => r);

    } else {
      return this.refreshTokenSubject.pipe(
        filter(token => token != null),
        take(1),
        switchMap(jwt => {
          return next.handle(this.addToken(request, jwt));
        }));
    }
  }

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

auth.service.ts

refreshToken() {
    return this.http.post<any>(`${environment.api}/refresh-token`, {
      'refreshToken': this.getRefreshToken()
    }, this.noAuthHeader).pipe(tap((res) => {
      if (res.success === true) {
        this.setToken(res.new_access_token);
        return true;
      } else {
        this.deleteToken();
        this.router.navigateByUrl('/login');
        return false;
      }
    }));
  }

会发生什么:

  1. 用户尝试访问安全路由
  2. auth.guard.ts 向服务器发送请求以验证访问令牌的有效性
  3. 访问令牌已过期,因此服务器以401响应
  4. 发送刷新令牌的请求,但首先完成用于简单验证访问令牌的初始请求-将用户重定向到登录页面,而不是保持登录状态
  5. 刷新令牌请求已完成

我不确定这是怎么发生的,但是我在想的是获取新访问令牌的请求应该首先完成,然后身份验证请求/auth.guard应该以此为基础?

1 个答案:

答案 0 :(得分:0)

卡住一天半后,我想我终于自己修复了:

auth.guard.ts

return this.authService.isAuthenticated()
      .pipe(
        map(
          res => {
            if (res.success === true) {
              return true;
            } else {
              this.authService.deleteToken();
              this.router.navigateByUrl('/login');
              return false;
            }
          }
        ), catchError(
          err => {

            // Absolutely needed this handler, 
            // but removed the code for redirection to login and deletion of tokens
            return of(false);
          }
        )
      );

auth.interceptor.ts

return next.handle(clonedReq)
        .pipe(
          catchError((err: HttpErrorResponse) => {
            if (err.error.code === 'EXPIRED') {
                  return this.handle401Error(clonedReq, next);
            } else if (err.error.status === 401) {
                  this.router.navigateByUrl('/login');
                  return throwError(err.error.message);
            }

            // Redirect to an error landing page
            return throwError(err.error.message);
          })
        );

private handle401Error(request: HttpRequest<any>, next: HttpHandler) {

    if (!this.isRefreshing) {
      this.isRefreshing = true;
      this.refreshTokenSubject.next(null);

      return this.authService.refreshToken()
        .pipe(
          switchMap((token: any) => {
          this.refreshTokenSubject.next(token.new_access_token);
          this.authService.setToken(token.new_access_token);

          return next.handle(this.addToken(request, token.new_access_token));
        }), catchError(err => { 

          // .subscribe() not needed!Just an error handler
          if (err.error.code === 'EXPIRED') {
                this.authService.deleteToken();
                this.router.navigateByUrl('/login');
                return throwError(err.error.message);
          }
          
          // Redirect to error landing page
          return throwError(err.error.message);
      }),
      finalize(() => {
        this.isRefreshing = false;
      })
    );
...

希望您对此有帮助!