簡體   English   中英

Angular JWT 刷新令牌

[英]Angular JWT refresh token

我正在嘗試使用基於外部 API 和 Angular 的刷新令牌來實現 JWT。 我寫了以下代碼

令牌攔截器

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, first } from 'rxjs/operators';
import {AuthenticationService} from '../services/authentication.service'

@Injectable()
export class TokenInterceptor implements HttpInterceptor {

  constructor(public authService : AuthenticationService ) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
      console.log(`AddTokenInterceptor - ${request.url}`);      

      return next.handle(this.addToken(request, localStorage.getItem('access_token')))
      .pipe(catchError(error => {
        if (error instanceof HttpErrorResponse && error.status === 401) {
          this.refreshToken()
          .pipe(first())
          .subscribe(
              data => {
                return next.handle(this.addToken(request, localStorage.getItem('access_token')))
              },
          ) 
        } else {
          return throwError(error);
        }
      }));
  }

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

  private refreshToken(){
    return this.authService.refreshToken()
  }
}

身份驗證服務

import { Injectable } from '@angular/core';

import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class AuthenticationService {
  public currentUser: string

  constructor(
    private http: HttpClient, 
    ) { }

  login(username:string, password:string){
    return this.http.post<any>('http://localhost:8000/api/token/', {username: username, password: password})
      .pipe(
        map(data => {
          localStorage.setItem('access_token', data.access)
          localStorage.setItem('refresh_token', data.refresh)
        })
      )
  }

  logout(){
    localStorage.removeItem('access_token')
    localStorage.removeItem('refresh_token')
  }

  getJWToken(){
    return localStorage.getItem('access_token')
  }

  getRefreshToken(){
    return localStorage.getItem('refresh_token')
  }

  refreshToken(){
    let refreshToken : string = localStorage.getItem('refresh_token'); 

    return this.http.post<any>('http://localhost:8000/api/token/refresh/', {"refresh": refreshToken}).pipe(
      map(data => {
        localStorage.setItem('access_token', data.access)
      })
    )
  }
}

主頁組件

import { Component, OnInit } from '@angular/core';
import { TeamService} from '../../services/team.service'
import { first } from 'rxjs/operators';


@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
  teams; 
  constructor(private teamService : TeamService) { }

  ngOnInit(): void {
    this.teamService.getTeams().pipe(first()).subscribe(
      data => {
        this.teams = data.results
      },
      error => {
        console.log(error.error)
      }
    )        

  }

  login() : void {
    console.log(this.teams)
  }
}

當返回 401 響應時,我正在嘗試刷新令牌,現在發生以下情況:

  • 訪問令牌已過期;
  • 新的是請求;
  • 使用 'teams' 變量渲染主組件時,控制台顯示以下錯誤: "You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable."

之后,當我刷新頁面時,團隊變量已正確加載並且可以使用。 我的問題:如何在發出請求之前刷新令牌,以便始終可以使用有效的訪問令牌發出請求? 似乎錯誤出在TokenInterceptor但我似乎無法弄清楚如何解決這個問題

一切看起來都很好,除了在攔截器內訂閱而不是嘗試 map pipe 中的響應。

更新

正如@ionut-t 在評論中指出的那樣,必須有兩個變化:

  • switchMap操作符替換subscription
  • catchError運算符中返回 observable
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
  console.log(`AddTokenInterceptor - ${request.url}`);      

  return next.handle(this.addToken(request, localStorage.getItem('access_token')))
  .pipe(catchError(error => {
    if (error instanceof HttpErrorResponse && error.status === 401) {
      return this.refreshToken()
      .pipe(
        first(),
        switchMap(        // <-- map the response instead of subscribing here
          data => next.handle(this.addToken(request, localStorage.getItem('access_token')))
        )
      )
      ...

您無法刷新所有 401 響應的令牌,因為如果用戶嘗試使用無效憑據登錄您的系統,也會有 401 響應。

通常,來自 API 的 HTTP 響應 header 有一些東西表明該客戶端曾經通過身份驗證,但現在有一個過期的令牌。 通常,響應 header 具有稱為 token-expired 或 www-authenticate 的屬性; 您必須在開始刷新令牌過程之前檢查這一點。

代碼示例:

身份驗證攔截器

import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpErrorResponse
} from '@angular/common/http';
import { AuthService } from '../services/auth.service';
import { Observable, BehaviorSubject, throwError } from 'rxjs';
import { environment } from 'src/environments/environment';
import { filter, switchMap, take, catchError } from 'rxjs/operators';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  private tryingRefreshing = false;
  private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);

  constructor(public authService: AuthService) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = this.authService.getToken();
    request = this.addAuthorization(request, token);
    return next.handle(request).pipe(catchError(error => {
      if (error instanceof HttpErrorResponse && error.status === 401) {
        const tokenExpired = error.headers.get('token-expired');
        if (tokenExpired) {
          return this.handle401Error(request, next);
        }

        this.authService.logout();
        return throwError(error);
      } else {
        return throwError(error);
      }
    }));
  }

  private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
    if (!this.tryingRefreshing) {
      this.tryingRefreshing = true;
      this.refreshTokenSubject.next(null);
      
     return this.authService.refreshToken().pipe(
        switchMap((token: any) => {
          this.tryingRefreshing = false;
          this.refreshTokenSubject.next(token);
          return next.handle(this.addAuthorization(request, token));
        }));

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

  addAuthorization(httpRequest: HttpRequest<any>, token: string) {
    return httpRequest = httpRequest.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
  }
}

刷新令牌

這只是展示 share() 方法的示例方法。

  refreshToken(): Observable<string> {
    return this.http.post<any>(`${this.baseUrl}/auth/token/refresh-token`, {}, { withCredentials: true })
      .pipe(
        share(),
        map((authResponse) => {
          this.currentAuthSubject.next(authResponse);
          this.addToLocalStorage(authResponse);
          return authResponse.token;
        }));
  }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM