[Angular] HttpInterceptor를 사용하여 Token 만료 검증하기
안녕하세요. 남산돈가스입니다.
이번 포스팅에서는 Angular 프로젝트 환경에서 API를 요청하실 경우, 가장 보편적으로 사용하시는 HttpClientModule에서 Http 요청을 가로채 특정 처리를 할 수 있도록 도와주는 HttpInterceptor 인터페이스에 대해서 작성해보겠습니다.
우선 HttpInterceptor는 Angular 4.3부터 추가 된 기능으로,
위에서 말한 것처럼, HttpClientModule을 이용한 API 호출 시 그 중간의 요청을 가로채어 특정 처리를 진행할 경우에 사용합니다.
저희 팀에서 Angular를 진행하는 프로젝트에서는 이 Interceptor를 API 호출 시 OAuth Token을 만료인지 유효한 지 검증하기 위한 용도로 사용되고 있습니다.
이번 포스팅에서 예제로 설명할 상황은 이렇습니다.
1. LocalStorage에 저장 된 OAuth Token을 가져온다.
2. OAuth Token을 요구하는 API를 HttpClient 모듈을 이용하여 호출한다.
3. HttpInterceptor에서 API 결과 응답이 Error이며, 401에러인 경우를 확인한다.
4. 상황이 3 과 같은 경우, 토큰 갱신 API를 호출한다.
5. 토큰 갱신 이후 정상 토큰을 가지고 위에서 호출해야하는 API 다시 요청한다.
6. 정상 응답을 확인한다.
예제 코드를 보면서 설명하겠습니다.
import {Injectable} from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import {Observable} from 'rxjs';
@Injectable()
export class TokenCheckInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req)
}
}
저는 TokenCheckInterceptor라는 서비스를 하나 생성하였습니다.
이 서비스는 기본적으로 HttpInterceptor 인터페이스를 implements 하여 만들 수 있습니다.
그리고 HttpInterceptor는 HttpRequest와 HttpHandler를 인자로 하는 intercept 함수를 가지고 있습니다.
- req: HttpClientModule을 이용하여 요청 HttpRequest정보를 가지고 있습니다.
- next: next.handle에 req를 담아 호출하면 일반적으로 호출하는 HTTP 요청이 발생하게 됩니다. HTTP 요청 발생 직전 공통적으로 수행하고 싶은 내용(토큰 셋팅) 들이 있으시면 위 next.handle 전에 request 정보를 clone하여 처리하시면 됩니다.
import {NgModule} from '@angular/core';
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {TokenInterceptor} from './services/interceptor/token.Interceptor';
@NgModule({
imports: [
HttpClientModule,
],
providers : [
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi : true
}
]
})
export class CoreModule {
}
이제 Http API를 호출하는 모든 서비스 요청에 Interceptor를 거쳐서 요청되게 됩니다.
import {Injectable} from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor, HttpErrorResponse
} from '@angular/common/http';
import {Observable} from 'rxjs';
import {catchError, flatMap} from 'rxjs/operators';
import {AuthService} from '../auth.service';
@Injectable()
export class TokenCheckInterceptor implements HttpInterceptor {
constructor(public authService: AuthService) { // 1
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe( // 2
catchError((err, caught) => { // 3
if (err instanceof HttpErrorResponse && err.status === 401) { // 4
return this.authService.refreshToken().pipe( // 5
flatMap(() => {
req = req.clone({ // 6
setHeaders: {
'Authorization': `Bearer ${this.authService.getToken()}` // 7
}
});
return next.handle(req); // 8
}));
}
return caught;
}));
}
}
그럼 이제 실제 Http 요청 시 결과가 401 토큰 에러로 확인이 되었을 경우, refresh 하여 다시 요청하는 로직을 작성해보았습니다.
1. 프로젝트 내의 토큰을 관리하는 service를 주입합니다. 여기서는 저희 프로젝트에서 사용하는 AuthService를 주입하였습니다.
2. 우리는 API 요청이 발생하기 전 처리가 아닌 API 요청에 의한 응답이 발생 후를 확인하여 갱신을 하도록 처리해줄 예정이니, next.handle(req)를 호출하여 기존 요청정보로 일단 API요청을 발생시킵니다.
3. handle 뒤에 pipe를 달고 에러를 먼저 catch 해내야하니, rxjs에서 catchError 로 Error를 잡아냅니다.
4. catchError를 통해 들어온 error 이벤트가 HttpErrorResponse이고, error 의 status가 401에러인지 확인합니다.
5. 401에러를 확인한 경우, 토큰을 갱신하는 API를 호출합니다. 여기서 authService.refreshToken()는 내부적으로 refreshToken을 가지고 OAuth Token을 갱신하여 새로운 유효한 Token을 받아와 LocalStorage 또는 기타 영역에 저장하는 역할을 합니다.
6. 토큰을 refresh한 이후, 기존 API를 다시 요청하기 위하여 기존 유효하지 않았던 Token을 새로 갱신 된 유효한 Token 정보로 교체해주는 부분입니다.
여기서 유의할 점은, intercept 함수의 인자값인 HttpRequest(req)는 Immutable(불변)한 변수이기 때문에, clone이라는 함수를 사용하여 기존 request정보를 복제한 뒤 요청정보를 변경하여 새로운 HttpRequest를 만들 수 있습니다.
7. 기존 request를 clone 한 뒤 새로 변경 된 Token 정보를 Set 합니다.
8. 새로운 request 정보로 API를 요청합니다.
여기까지, HttpInterceptor를 사용하여 API 요청 시 만료 된 토큰을 갱신하여 새로운 토큰정보로 실패한 API를 재요청해보았습니다.
이외에도 HttpInterceptor는 HttpClientModule을 사용하시면서 공통적으로 수행해야하는 처리(에러, 토큰 Set, 로딩 등)들을 쉽게 Handling 할 수 있다는 점에서 강력한 장점을 가지는 인터페이스라고 생각합니다.
감사합니다.
댓글
댓글 쓰기