【问题标题】:How to automatically refresh access token with interceptor + route guard?如何使用拦截器+路由保护自动刷新访问令牌?
【发布时间】:2020-12-03 20:55:49
【问题描述】:

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

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 应该以此为基础?

【问题讨论】:

  • 当您在refreshToken() 方法中处理删除旧令牌并导航到登录页面时,为什么您的isAuthenticated() 中又完全有catchError
  • 嗨@roya,如果我理解正确的话,你是否暗示我不需要auth.guard 中的catchError?我实际上只是尝试过,但我收到一个错误:ERROR Error: Uncaught (in promise): HttpErrorResponse:...,包括来自服务器的响应,说访问令牌已过期。顺序:1)发送/user/auth -> 验证访问令牌 2)401 响应,发送/refresh-token 3)错误(上) 4)/refresh-token 用新令牌响应 5)/user/auth 用新令牌重新发送 -> 成功6) 没有任何反应,用户无法继续安全路线
  • 你能准备一个plunker或类似的东西吗?否则这篇文章可能会对你有所帮助link
  • @roya 我会查看该链接并尝试在 stackblitz 上重现我的问题,谢谢!

标签: angular jwt


【解决方案1】:

卡了一天半,终于自己修好了:

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;
      })
    );
...

希望对您有所帮助!

【讨论】:

    猜你喜欢
    • 2019-01-09
    • 2020-05-12
    • 2020-02-10
    • 2019-10-22
    • 2014-06-24
    • 2023-03-23
    • 1970-01-01
    • 2019-07-09
    • 1970-01-01
    相关资源
    最近更新 更多