Angular Interceptor для добавления токена и автоматического обновления

Я впервые работаю с угловыми перехватчиками, и у меня почти есть то, что я хочу, но есть кое-что, что я не могу понять, даже после того, как некоторое время погуглил. Я храню токен обновления локально, а срок действия токена доступа истекает каждые 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, так что извините, если это очевидно для других). Подозревая, я задавался вопросом, было ли это наблюдаемым, что все испортило, и ему не нравится ждать, пока наблюдаемое вернется, поэтому я попробовал это:

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

И теперь это, кажется, работает! Это говорит о том, что я, возможно, был прав в своей догадке (или что это что-то еще внутри другого кода, которого я не вижу). В любом случае, работает хорошо, но это оставляет меня с вопросом, как обновить. Первоначальная причина, по которой я использовал наблюдаемую из службы аутентификации, заключалась в том, что ее нужно было обновить. В основном служба аутентификации будет смотреть на свой текущий токен и смотреть, истек ли срок его действия или нет. В противном случае он просто вернет of(token), но если срок его действия истек, он будет обращаться к серверу через http-сообщение, которое можно наблюдать, поэтому строка будет приходить всякий раз, когда сервер отвечает.

Поэтому я думаю, что мой вопрос двоякий:

  1. Может ли кто-нибудь подтвердить или опровергнуть, что я был прав насчет наблюдаемой неисправности перехватчика? Вроде проблема в нем, но хотелось бы удостовериться.
  2. Как мне справиться с обновлением токена для них в фоновом режиме без необходимости повторного входа в систему каждые 15 минут?

РЕДАКТИРОВАТЬ

Вот логика в методе токена авторизации:

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

РЕДАКТИРОВАТЬ

Итак, с помощью ответа ниже, а также вопроса это вот что я придумал:

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

В общем, моя первая попытка была не так уж и далека, «секрет» заключался в том, чтобы использовать конвейер и карту слияния вместо того, чтобы пытаться подписаться.

У вас есть логика обновить, заключенная в функцию? Если да, не могли бы вы добавить подпись?

Jota.Toledo 07.06.2019 08:40
Может ли кто-нибудь подтвердить или опровергнуть, что я был прав насчет наблюдаемой неисправности перехватчика? скорее всего. Как правило, подписка внутри сервисов или http-перехватчиков является подверженной ошибкам практикой. Этого можно избежать, используя правильные операторы rxjs для данного случая.
Jota.Toledo 07.06.2019 08:47

В вашем первом фрагменте кода в случае запроса на авторизацию вы возвращаете HttpEvent вместо Observable<HttpEvent<any>>. Итак, попробуйте обернуть этот первый возврат в эмиттер of() (Learnrxjs.io/operators/creation/of.html), чтобы он выглядел как return of(next.handle(req)); (ТОЛЬКО В ЗАПРОСЕ AUTH)

Andriy 07.06.2019 08:55

@Jota.Toledo Хорошо, я думаю, это имеет смысл, поскольку наблюдаемая будет возвращаться и продолжаться в отдельное время, поэтому перехватчик не сможет сразу вернуть результат? Так что это должно быть довольно распространенной вещью... как тогда вы справляетесь с автоматическим обновлением токена? Я разместил код своих методов получения и обновления токена.

sfaust 07.06.2019 15:53

Однако запрос @Andriy Auth работает ... Глядя на код для «дескриптора», он также возвращает Observable<HttpEvent<any>>, так что не будет ли это действительным и предпочтительнее просто вернуть это? Из примеров, которые я нашел в Интернете до сих пор, они, похоже, не обертывают его таким образом и просто возвращают его напрямую, однако они также не обновляют токен автоматически...

sfaust 07.06.2019 15:57

@ Jota.Toledo спасибо за правки и направление, но не лучше ли оставить оригинал и дать ответ, чтобы его можно было отследить? Также в вашем новом методе GetCurrentToken он всегда либо возвращает текущий токен, либо обновляется; также возможно, что пользователь никогда не входил в систему и не имеет токена обновления (что было бы нулевым состоянием токена в исходном методе). Насколько я могу судить, ваш новый метод не справляется с этим. Наконец, почему вы изменили «handleError» с LoginResult на строку? Метод должен возвращать LoginResult...

sfaust 07.06.2019 17:24

Плохо, я откатил изменения :). Думаю, в моем подходе отсутствует и этот случай.

Jota.Toledo 07.06.2019 17:38
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Angular и React для вашего проекта веб-разработки?
Angular и React для вашего проекта веб-разработки?
Когда дело доходит до веб-разработки, выбор правильного front-end фреймворка имеет решающее значение. Angular и React - два самых популярных...
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Мы провели Twitter Space, обсудив несколько проблем, связанных с последними дополнениями в Angular. Также прошла Angular Tiny Conf с 25 докладами.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
Мое недавнее углубление в Angular
Мое недавнее углубление в Angular
Недавно я провел некоторое время, изучая фреймворк Angular, и я хотел поделиться своим опытом со всеми вами. Как человек, который любит глубоко...
Освоение Observables и Subjects в Rxjs:
Освоение Observables и Subjects в Rxjs:
Давайте начнем с основ и постепенно перейдем к более продвинутым концепциям в RxJS в Angular
0
7
459
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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, которые, по сути, дают наблюдаемые «вещи, которые нужно сделать», когда они возвращаются с результатом, вместо того, чтобы фактически подписываться и пытаться внести изменения и вернуться туда, это правильно? Тогда перехватчик может продолжать свою цепочку, не пытаясь чего-то ждать, но все же выполняет преобразования при вызове? Сейчас попробую это...

sfaust 07.06.2019 18:00

Так что извините за задержку по этому поводу. Я пошел дальше и отметил это как ответ, хотя это немного отличается от того, что я в итоге сделал, поскольку это помогло привести к ответу, и я считаю, что это та же идея...

sfaust 05.08.2019 17:48

Другие вопросы по теме