简体   繁体   English

Angular JWT 刷新令牌

[英]Angular JWT refresh token

I'm trying to implement JWT with refresh tokens based on an external API and Angular.我正在尝试使用基于外部 API 和 Angular 的刷新令牌来实现 JWT。 I wrote the following code我写了以下代码

TokenInterceptor令牌拦截器

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

AuthenticationService身份验证服务

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

HomeComponent主页组件

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

I'm trying to refresh the token when a 401 response is returned, the following happens right now:当返回 401 响应时,我正在尝试刷新令牌,现在发生以下情况:

  • Access token is expired;访问令牌已过期;
  • New one is request;新的是请求;
  • Console shows the following error when rendering the home componenent with the 'teams' variable: "You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable."使用 'teams' 变量渲染主组件时,控制台显示以下错误: "You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable."

After that, when I refresh the page the teams variable is correctly loaded and can be used.之后,当我刷新页面时,团队变量已正确加载并且可以使用。 My question: how can I refresh the token before the request is made so that the request can always be made with a valid access token?我的问题:如何在发出请求之前刷新令牌,以便始终可以使用有效的访问令牌发出请求? It seems that the mistake is in the TokenInterceptor but I can't seem to figure out how to solve this似乎错误出在TokenInterceptor但我似乎无法弄清楚如何解决这个问题

Everything looks fine except instead of subscribing inside the interceptor try to map the response in the pipe.一切看起来都很好,除了在拦截器内订阅而不是尝试 map pipe 中的响应。

Update更新

As @ionut-t pointed in the comments there must be two changes:正如@ionut-t 在评论中指出的那样,必须有两个变化:

  • Replace subscription with a switchMap operatorswitchMap操作符替换subscription
  • Return the observable in the catchError operatorcatchError运算符中返回 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')))
        )
      )
      ...

You can't refresh the token for all 401 responses because if the user tries to login to your system using invalid credentials will also have a 401 response.您无法刷新所有 401 响应的令牌,因为如果用户尝试使用无效凭据登录您的系统,也会有 401 响应。

Usually, the HTTP response header that comes from the API has something that indicates that this client once was authenticated but now has an expired token.通常,来自 API 的 HTTP 响应 header 有一些东西表明该客户端曾经通过身份验证,但现在有一个过期的令牌。 Typically, the response header has a property called token-expired or www-authenticate;通常,响应 header 具有称为 token-expired 或 www-authenticate 的属性; you have to check this before starting the refreshes token process.您必须在开始刷新令牌过程之前检查这一点。

Code sample:代码示例:

AuthInterceptor身份验证拦截器

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

Refresh token刷新令牌

This is just a sample method to show the share() approach.这只是展示 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