繁体   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