Angular Interceptor: 더 나은 대안

따라서 각도 프로젝트에서 작업 중이며 백엔드 서비스와 통신하기 위해 HTTP 요청을 사용해야 합니다. Angulars의 HTTPClient는 요청을 구현하기 위한 선택이며 훌륭하게 작동합니다.

그런 다음 일반적으로 요청에 권한 부여 헤더를 추가하는 것을 의미하는 보안 리소스와 통신해야 하는 부분이 있습니다. 한 가지 방법은 아래와 같이 모든 개별 요청에 헤더를 추가하는 것이지만 많은 요청에 수동으로 헤더를 추가하는 것은 금세 귀찮아집니다.

initGetUserData(): any {
  // Get the token & create headers
  const token = this.authService.GetAccessToken();
  const headers = new HttpHeaders(
    { Authorization: `Bearer ${token}` }
  );

  this.httpClient.get('Secure_Url', { headers }).subscribe(response => {

  });
}

중복을 줄이기 위한 솔루션이 있습니다.



이것은 Angulars의 요청 인터셉터를 확장하는 일반적인 선택으로, 요청에 권한 부여 헤더를 추가하는 것과 같은 사전 처리 논리를 추가할 수 있습니다. 인터셉터에 토큰 새로 고침 논리를 추가하는 것도 좋은 방법이므로 사용자 경험이 원활하고 토큰이 새로 고쳐지면 원래 요청을 완료할 수 있습니다.

  intercept(request: HttpRequest<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {

    // Get token & add to request headers
    let token = this.authService.GetAccessToken();
    request = request.clone({
      headers: request.headers
        .set('Authorization', `Bearer ${token}`)
    });

    return next.handle(request).pipe(
      catchError(err => {
        if (err.status === 401) {
          // Refresh tokens
          return this.authService.InitRefresh().pipe(
            switchMap((response) => {
              // Get new token
              token = this.authService.GetAccessToken();
              request = request.clone({
                headers: request.headers
                  .set('Authorization', `Bearer ${token}`)
              });
              // Continue original request
              return next.handle(request);
            })
          );
        }
      }));

    // Omitting error handling etc. for brevity
  }

보라, 우리는 모든 설정을 가졌으니 피치는 무엇을 위한 것인가?



보안 리소스가 아닌 공개 API와 상호 작용하는 보안 모듈 내부에 구성 요소가 있을 때까지 모든 것이 잘 작동하고 예상대로 작동합니다. 일반적으로 발생하는 일은 인터셉터가 해당 요청에 권한 부여 헤더를 가로채서 추가하려고 시도하는 것입니다. 또한 토큰 새로 고침의 전체 오버헤드는 공용 리소스에 대해 실행됩니다.

더 나쁜 것은 사용자가 로그인하지 않고 구성 요소에 액세스하려고 하면 공개 보기로 작동해야 하고 로그인이 필요하지 않아야 하며 인터셉터가 추가/새로 고침을 시도할 때 오류(처리되지 않은 경우)가 발생한다는 것입니다. 토큰이지만 사용자가 로그인하지 않았기 때문에 사용할 수 있는 토큰이 없습니다.

그러나 그것을 해결할 방법도 있습니다.



사실입니다. 무시하고 싶은 요청을 처리하기 위한 솔루션이 있습니다. 요청에 사용자 정의 헤더를 추가하거나 인터셉터 인증 논리에서 생략해야 하는 URL 배열을 정의할 수 있습니다. 다시 말하지만, 우리는 이러한 모든 비정상적 구현을 ​​추적하기 어려운 지점에 곧 도달합니다.

    // Check for skip header
    const isSkipHeader = request.headers.has('skip');
    if (isSkipHeader) {
      // Remove unnecessary header & proceed
      request = request.clone({
        headers: request.headers.delete('skip')
      });
      return next.handle(request);
    }

따라서 제안된 솔루션



다음 시나리오를 처리할 Angulars의 HTTP 클라이언트 주위에 사용자 지정 래퍼를 만드는 것으로 시작합니다.
  • 인터셉터 내부에서 401 응답을 받은 후 대신 호출을 실행하기 전에 토큰 만료를 확인하면 1번 호출의 오버헤드가 줄어듭니다(원래 호출이 두 번 대신 한 번 실행되기 때문에).
  • 사용자 정의 헤더 대신 간단한 방법으로 인증을 재정의할 수 있음
  • 모든 요청을 수정하기 위한 중심점 제공(인터셉터도 수행하지만 비동기 방식의 경우 연산자를 사용해야 하므로 전처리에 적합하지 않음)
  • 다른 응용 프로그램 영역에 영향을 주지 않고 HTTP 클라이언트를 다른 타사 클라이언트로 교체하는 기능 제공
  • 요구 사항에 따라 요청을 사용자 정의하고 확장하는 더 쉬운 방법

  • 데이터 전송 및 검색을 위한 일반 공용 메서드를 사용하여 클래스를 만듭니다. 특정 시나리오에 매우 도움이 될 인증을 재정의하는 방법을 제공하고 호출을 실행하기 전에 토큰 만료를 확인하고 그에 따라 진행합니다.

    A lot of code have been omitted for brevity (Such as HTTP post, put, delete methods & error handling but it should be very easy to extend this



    /**
     * Interface for HTTP options
     */
    export interface AppHttpOptions<T = any> {
        Headers?: HttpHeaders;
        Body?: T;
        RequestUrl: string;
        QueryParams?: object;
    }
    
    /**
     * Application HTTP Client wrapper to provide authorization mechanism 
     * or any customization of requests
     */
    @Injectable({
        providedIn: 'root'
    })
    export class AppHttpClient {
        // Pass this from environment variable
        private baseUrl = 'baseUrl';
        /**
         * Constructor for client class, can be used to inject
         * required resources
         * @param httpClient Angular HTTP Client
         */
        constructor(private httpClient: HttpClient,
            private authService: AuthService) {
        }
    
        /**
         * Initiates authorized Get request to the api
         * @param httpOptions HttpOptions containing request data
         */
        public GetAuthorized<ResponseType>(httpOptions: AppHttpOptions):
            Promise<ResponseType> {
            return this.getResponsePromise(httpOptions, 'post');
        }
    
        /**
         * Initiates Get request to the api
         * @param httpOptions HttpOptions containing request data 
         */
        public Get<ResponseType>(httpOptions: AppHttpOptions):
            Promise<ResponseType> {
            return this.getResponsePromise(httpOptions, 'get', false);
        }
    
        /**
         *  Creates a promise that resolves into HTTP response body
         * @param httpOptions HttpOptions containing request data
         * @param requestType Type of request i.e Get, Post, Put, Delete
         */
        private getResponsePromise<ResponseType>
            (httpOptions: AppHttpOptions,
             requestType: 'post' | 'get' | 'delete' | 'put',
             isAuth: boolean = true):
            Promise<ResponseType> {
            return new Promise((resolve, reject) => {
                // Process the subscription & resolve the response
                // i.e the request body response
                this.getProcessedSubscription(httpOptions, requestType, isAuth).
                    then((response: ResponseType) => {
                        resolve(response);
                    }).catch(err => reject(err));
            });
        }
    
        /**
         * Subscribes to http request & returns the response as promise
         * @param httpOptions HttpOptions containing request data
         * @param requestType Type of request i.e Get, Post, Put, Delete
         */
        private getProcessedSubscription<ResponseType>
            (httpOptions: AppHttpOptions,
             requestType: 'post' | 'get' | 'delete' | 'put',
             isAuth: boolean):
            Promise<ResponseType> {
            return new Promise((resolve, reject) => {
                this.getHttpRequest<ResponseType>
                    (httpOptions, requestType, isAuth).then(response => {
                        // Subscribe to HTTP request & resolve with the result
                        response.subscribe(result => {
                            resolve(result);
                        },
                            err => reject(err)
                        );
                    }).catch(err => reject(err));
            });
        }
    
        /**
         * Creates a promise to get the HTTP request observable
         * @param httpOptions HttpOptions containing request data
         * @param requestType Type of request i.e Get, Post, Put, Delete
         */
        private getHttpRequest<ResponseType>
            (httpOptions: AppHttpOptions,
             requestType: 'post' | 'get' | 'delete' | 'put',
             isAuth: boolean):
            Promise<Observable<ResponseType>> {
            return this.getAuthHeaders(httpOptions.Headers, isAuth).
                then((headers: HttpHeaders) => {
                    // Append the query parameters
                    const options = this.addQueryParams(httpOptions);
                    // Create a HTTP request with angular HTTP Client
                    const request = this.httpClient.request<ResponseType>
                        (requestType,
                            this.baseUrl + options.RequestUrl,
                            { body: options.Body, headers });
    
                    return request;
                }).catch(err => Promise.reject(err));
        }
    
        /**
         * Creates a promise that adds the authentication header
         * to the request headers. Token retrieve & refresh logic can
         * be easily handled as it is async operation
         * @param headers Headers passed in with request
         */
        private getAuthHeaders(headers: HttpHeaders, isAuth: boolean):
            Promise<HttpHeaders> {
            return new Promise((resolve) => {
                // Only add authentication headers if required
                if (isAuth) {
                    const token = this.authService.GetAccessToken();
                    if (headers) {
                        // Append authorization header
                        // * This is the core portions.
                        //  We can apply all logics for checking token expiry,
                        //  refreshing it & appending it to the headers
                        //  without worrying about any side effects as we can 
                        //  resolve promise after all the other actions
                        headers.append('Authorization', `Bearer ${token}`);
                    }
                    else {
                        // Create new headers object if not passed in
                        headers = new HttpHeaders({
                            Authorization: `Bearer ${token}`
                        });
                    }
                }
                resolve(headers);
            });
        }
    
        /**
         * @param httpOptions HttpOptions containing request data 
         * @param httpOptions Add
         */
        private addQueryParams(httpOptions: AppHttpOptions): AppHttpOptions {
            if (httpOptions.QueryParams) {
                // Create the parameters string from the provided parameters
                const query = Object.keys(httpOptions.QueryParams)
                    .map(k => k + '=' + httpOptions.QueryParams[k])
                    .join('&');
                // Append the parameters to the request URL
                httpOptions.RequestUrl = `${httpOptions.RequestUrl}?${query}`;
            }
            return httpOptions;
        }
    }
    



    그리고 우리는 끝났습니다! 메소드를 사용하려면 클래스를 삽입하고 최소한의 구성으로 적절한 메소드를 호출하기만 하면 됩니다.

      constructor(private httpClient: AppHttpClient) { }
      initGetData(): any {
        // Public resource request
        this.httpClient.Get({ RequestUrl: 'Public_Url'}).
          then(response => {
    
          });
        // Secured resource request
        this.httpClient.GetAuthorized({ RequestUrl: 'Secure_Url' }).
          then(response => {
    
          });
      }
    

    위의 구현은 사용 사례에 따라 여러 옵션으로 수정할 수 있습니다. 통화를 시작하기 전에 토큰 만료 및 새로 고침 확인, 많은 번거로움 없이 특정 요청이 있는 사용자 정의 헤더 전달 등

    그러한 시나리오나 더 영향력이 있을 수 있는 다른 대안을 처리하기 위해 무엇을 사용하는지 알려주십시오.

    즐거운 코딩!

    좋은 웹페이지 즐겨찾기