【问题标题】:Angular Interceptor to add Token and Automatically RefreshAngular拦截器添加令牌并自动刷新
【发布时间】:2019-10-22 15:16:55
【问题描述】:

我第一次使用角度拦截器,我几乎拥有了我想要的东西,但是即使在谷歌上搜索了一段时间后我也无法完全弄清楚。我在本地存储刷新令牌,访问令牌每 15 分钟过期一次;我希望能够使用刷新令牌在过期时自动刷新他们的身份验证令牌。

我的第一次尝试是这样的:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.toLowerCase().includes('/auth')) {
  // It's an auth request, don't get a token
  return next.handle(req);
}

// Not an auth endpoint, should have a token
this.authService.GetCurrentToken().subscribe(token => {
  // Make sure we got something
  if (token == null || token === '') {
    return next.handle(req);
  }

  // Have a token, add it
  const request = req.clone({
    setHeaders: {
      Authorization: `Bearer ${token}`
    }
  });

  return next.handle(request);
});
}

这似乎不起作用,我不知道为什么(我是 Angular 的新手,对 JS 也很陌生,如果对其他人来说很明显,我很抱歉)。预感我想知道是不是 observable 搞砸了,它不喜欢等待 observable 返回所以我尝试了这个:

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.toLowerCase().includes('/auth')) {
  // It's an auth request, don't get a token
  return next.handle(req);
}

const token = this.authService.GetAccessTokenWithoutRefresh();
const request = req.clone({
  setHeaders: {
    Authorization: `Bearer ${token}`
  }
});
return next.handle(request);   
}

现在它似乎工作了!这表明我的预感可能是正确的(或者这是我没有看到的其他代码中的其他内容)。无论如何,工作很好,但这给我留下了如何刷新的问题。我使用来自 auth 服务的 observable 的最初原因是为了防止它需要刷新。基本上,身份验证服务会查看它的当前令牌并查看它是否已过期。如果不是,它只会返回of(token),但如果它已过期,它将通过可观察到的 http 帖子返回到服务器,因此只要服务器响应,字符串就会到达。

所以我想我的问题有两个:

  1. 任何人都可以确认或反驳我对可观察到的干扰拦截器是正确的吗?这似乎是问题所在,但想确定一下。
  2. 如何在后台为他们刷新令牌,而不必每 15 分钟重新登录一次?

编辑

这里是auth token方法中的逻辑:

GetCurrentToken(): Observable<string> {
if (this.AccessToken == null) {
  return null;
}
if (this.Expiry > new Date()) {
  return of(this.AccessToken);
}

// Need to refresh
return this.RefreshToken().pipe(
  map<LoginResult, string>(result => {
    return result.Success ? result.AccessToken : null;
  })
);
}

以及刷新方法:

private RefreshToken(): Observable<LoginResult> {
const refreshToken = localStorage.getItem('rt');
if (refreshToken == null || refreshToken === '') {
  const result = new LoginResult();
  // Set other stuff on result object
  return of(result);
}

const refresh = new RefreshTokenDto();
refresh.MachineId = 'WebPortal';
refresh.TokenId = refreshToken;
return this.http.post(ApiData.baseUrl + '/auth/refresh', refresh)
  .pipe(
    tap<AuthResultDto>(authObject => {
      this.SetLocalData(authObject);
    }),
    map<AuthResultDto, LoginResult>(authObject => {
      const result = new LoginResult();
      // Set other stuff on the result object
      return result;
    }),
    catchError(this.handleError<LoginResult>('Refresh'))
  );
}

编辑

好的,在下面的答案以及this 问题的帮助下,这是我想出的:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.toLowerCase().includes('/auth')) {
  // It's an auth request, don't get a token
  return next.handle(req.clone());
}

return this.authService.GetCurrentToken().pipe(
  mergeMap((token: string) => {
    if (token === null || token === '') {
      throw new Error('Refresh failed to get token');
    } else {
      return next.handle(req.clone({setHeaders: {Authorization: `Bearer ${token}`}}));
    }
  }),
  catchError((err: HttpErrorResponse) => {
    if (err.status === 401) {
      this.router.navigateByUrl('/login');
    }
    return throwError(err);
  })
);
}

所以基本上我的第一次尝试并不是很遥远,“秘密”是使用管道和合并地图而不是尝试订阅。

【问题讨论】:

  • 您是否将 refresh 逻辑封装在一个函数中?如果你,你能添加它的签名吗?
  • 任何人都可以确认或反驳我关于可观察到的干扰拦截器是正确的吗?很可能。一般来说,订阅内部服务或 http 拦截器是一种容易出错的做法。这可以通过使用正确的 rxjs 运算符来避免。
  • 在您的第一个代码 sn-p 中,如果出现身份验证请求,您将返回 HttpEvent 而不是 Observable&lt;HttpEvent&lt;any&gt;&gt;。因此,尝试将第一个返回值包装在 of() 发射器 (learnrxjs.io/operators/creation/of.html) 中,所以它应该看起来像 return of(next.handle(req)); (ONLY IN AUTH REQUEST)
  • @Jota.Toledo 好的,我想这是有道理的,因为 observable 将返回并在单独的时间继续,因此拦截器将无法立即返回结果?所以这一定是一件很常见的事情......那么你如何处理自动令牌刷新呢?我已经发布了我的令牌获取和刷新方法的代码。
  • 我的错,我回滚了更改:)。猜猜我的方法也缺少这种情况。

标签: angular authentication angular-http-interceptors


【解决方案1】:

您可以尝试以下方法。我可能夸大了其中的 FP 数量:

export class AuthInterceptor {
 ctor(private authService: AuthService){}
 intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

   return of(req.url.toLowerCase().includes('/auth')).pipe(
     mergeMap(isAuthRequest => !isAuthRequest
       // Missing: handle error when accessing the access token
       ? this.authService.accessToken$.pipe(map(addAuthHeader(req)))
       : of(req)
     ),
     mergeMap(nextReq => next.handle(nextReq))
   );
 }
}

function addAuthHeader(req: HttpRequest<any>): (token:string)=> HttpRequest<any> {
  return token => req.clone({setHeaders: {Authorization: `Bearer ${token}`}})
} 

还有身份验证服务:

export class AuthService {
  ctor(private http: HttpClient){}

  get accessToken$(): Observable<string> {
    return of(this.AccessToken).pipe(
       mergeMap(token => token === null
         ? throwError("Access token is missing")
         : of(this.Expiry > new Date())
       ),
       mergeMap(accessTokenValid => accessTokenValid
         ? of(this.AccessToken)
         : this.refreshToken()
       )
    );
  }

  refreshToken(): Observable<string> {
    return of(localStorage.getItem('rt')).pipe(
      mergeMap(refreshToken => !refreshToken 
        ? of(extractAccessTokenFromLogin(createLoginResult())
        : this.requestAccessToken(this.createRefreshToken(refreshToken))
      )
    );
  }

  private requestAccessToken(refreshToken: RefreshTokenDto): Observable<string> {
    return this.http.post<AuthResultDto>(ApiData.baseUrl + '/auth/refresh', refreshToken)
     .pipe(
       tap(auth => this.SetLocalData(auth )),
       map(auth => this.mapAuthObjToLoginRes(auth)),
       map(extractAccessTokenFromLogin)
       catchError(this.handleError<string>('Refresh'))
     )
  }

  private createRefreshToken(tokenId: string): RefreshTokenDto{...}

  private createLoginRes(): LoginResult {...}

  private mapAuthObjToLoginRes(val: AuthResultDto): LoginResult{...}
}

function extractAccessTokenFromLogin(login: LoginResult): string 
     => login.Success ? login.AccessToken : null;

【讨论】:

  • 好的,以确保我理解......基本上,而不是像我那样订阅你正在使用 pipe 和 mergeMap ,当它返回结果而不是实际上订阅并尝试进行修改并返回那里,对吗?然后拦截器能够继续它的链而不试图等待某些东西,但在调用时仍然进行转换?现在要试试这个...
  • 对此延迟表示抱歉。我继续并将其标记为答案,即使它与我最终做的有点不同,因为它有助于导致答案,我相信是相同的想法......
猜你喜欢
  • 1970-01-01
  • 2017-12-31
  • 2017-12-26
  • 2018-06-12
  • 2019-09-03
  • 2019-03-28
  • 1970-01-01
  • 1970-01-01
  • 2020-05-12
相关资源
最近更新 更多