source

AngularJS - http interceptor - 토큰 새로 고침 후 모든 요청을 재발송합니다.

ittop 2023. 2. 26. 10:30
반응형

AngularJS - http interceptor - 토큰 새로 고침 후 모든 요청을 재발송합니다.

가끔 $http를 여러 개 사용할 수 있는 각진 앱을 가지고 있습니다.get requests per state.앱은 사용자 인증에 JWT를 사용하여 토큰을 새로 고칩니다.API 서버는401인증 오류로 인해 실패한 모든 요청에 대해 설명합니다.내가 만든 건http interceptor는, 401 에러시에 리프레시 토큰을 사용해 새로운 토큰을 요구해, 그 후에 원래의 요구를 재발송신합니다.

문제는 상태가 예를 들어 $http로 되어 있는 경우입니다.요청을 받고 둘 다 401 응답을 받은 후 액세스 토큰을 두 번 갱신합니다.토큰을 한 번만 새로 고치고 싶지만 실패한 요청을 모두 다시 보내고 싶습니다.

이것이 달성 가능한가? 달성 가능한 경우, 어떻게 달성되는가?

app.factory('AuthInterceptor', function($q, $injector, RESOURCE_URL, API_BASE, authService) {
    return {
        request: function(config) {
            config.headers = config.headers || {};
            if (authService.getAccessToken()) {
                if (config.url.substring(0, RESOURCE_URL.length) !== RESOURCE_URL) {
                    config.headers.Authorization = 'Bearer ' + authService.getAccessToken();
                }
            }
            return config;
        },
        responseError: function(response) {
            switch (response.status) {
                case 401:
                    var deferred = $q.defer();
                    $injector.get("$http").post(API_BASE + '/api/auth/refresh', {refreshtoken: authService.getRefreshToken()}).then(function(r) {
                        if (r.data.data.accesstoken && r.data.data.refreshtoken && r.data.data.expiresin) {
                            authService.setAccessToken(r.data.data.accesstoken);
                            authService.setRefreshToken(r.data.data.refreshtoken);
                            authService.setExpiresIn(r.data.data.expiresin);
                            $injector.get("$http")(response.config).then(function(resp) {
                                deferred.resolve(resp);
                            },function(resp) {
                                deferred.reject();
                            });
                        } else {
                            deferred.reject();
                        }
                    }, function(response) {
                        deferred.reject();
                        authService.clear();
                        $injector.get("$state").go('guest.login');
                        return;
                    });
                    return deferred.promise;
                    break;
                default:
                    authService.clear();
                    $injector.get("$state").go('guest.login');
                    break;
            }
            return response || $q.when(response);
        }
    };
});

대행 수신자는 인증 요청이 "비행 중"인지 여부를 추적해야 합니다.이를 위해서는 인증요구에 의해 반환된 약속에 대한 참조를 유지할 수 있습니다.비행 중에 요청이 있고 401이 더 있을 경우, 새로운 요청을 시작하는 대신 캐시된 약속을 사용하십시오.또한 '/api/auth/refresh' 자체에서 401이 반환되는 경우를 고려하여 로직을 추가하는 것도 고려해야 합니다.

app.factory('AuthInterceptor', function($q, $injector, RESOURCE_URL, API_BASE, authService) {
    var inflightAuthRequest = null;
    return {
        request: function(config) {
            config.headers = config.headers || {};
            if (authService.getAccessToken()) {
                if (config.url.substring(0, RESOURCE_URL.length) !== RESOURCE_URL) {
                    config.headers.Authorization = 'Bearer ' + authService.getAccessToken();
                }
            }
            return config;
        },
        responseError: function(response) {
            switch (response.status) {
                case 401:
                    var deferred = $q.defer();
                    if(!inflightAuthRequest) {
                        inflightAuthRequest = $injector.get("$http").post(API_BASE + '/api/auth/refresh', {refreshtoken: authService.getRefreshToken()});
                    }
                    inflightAuthRequest.then(function(r) {
                        inflightAuthRequest = null;
                        if (r.data.data.accesstoken && r.data.data.refreshtoken && r.data.data.expiresin) {
                            authService.setAccessToken(r.data.data.accesstoken);
                            authService.setRefreshToken(r.data.data.refreshtoken);
                            authService.setExpiresIn(r.data.data.expiresin);
                            $injector.get("$http")(response.config).then(function(resp) {
                                deferred.resolve(resp);
                            },function(resp) {
                                deferred.reject();
                            });
                        } else {
                            deferred.reject();
                        }
                    }, function(response) {
                        inflightAuthRequest = null;
                        deferred.reject();
                        authService.clear();
                        $injector.get("$state").go('guest.login');
                        return;
                    });
                    return deferred.promise;
                    break;
                default:
                    authService.clear();
                    $injector.get("$state").go('guest.login');
                    break;
            }
            return response || $q.when(response);
        }
    };
});

Joe Enzminger의 솔루션은 훌륭합니다.그러나 콜백이 실행되지 않아 몇 가지 문제가 있었습니다.그때 비행기에서 약간의 오타를 발견했습니다.AuthRequest/inFlightAuthRequest.

현재 완전한 솔루션은 다음과 같습니다.

(function() {
'use strict';
    angular.module('app.lib.auth', []);
    angular.module('app.lib.auth')
        .factory('authService', authService);
    angular.module('app.lib.auth')
        .factory('AuthInterceptor', AuthInterceptor);

    function authService($window) {
        return {
            getToken: function() {
                return $window.localStorage.getItem('JWT');
            },
            getRefreshToken: function() {
                return $window.localStorage.getItem('Refresh-JWT');
            },
            setRefreshToken: function(token) {
                $window.localStorage.setItem('Refresh-JWT', token);
            },
            setToken: function(token) {
                $window.localStorage.setItem('JWT', token);
            },
            clearAllToken: function(){
                $window.localStorage.removeItem('JWT');
                $window.localStorage.removeItem('Refresh-JWT');
            },
            clearToken: function(){
                $window.localStorage.removeItem('JWT');
            },
            isLoggedIn: function() {
                if ($window.localStorage.getItem('JWT') === null) {
                    return false;
                }
                else {
                    return true;
                }
            },
            toLogin: function(){
                $window.location.href = "http://" + $window.location.host + "/tprt/login";
            }
        }
    }

    function AuthInterceptor($q, $injector, authService) {
        var inFlightAuthRequest = null;
        return {
            request : function(config) {
                config.headers = config.headers || {};
                if(authService.getToken()){
                    config.headers['Authorization'] = authService.getToken();
                }
                return config;
            },
            responseError : function(response) {
                if(response.config.url == URLS.api_refresh_token){
                    console.log(JSON.stringify(response));
                    authService.clearAllToken();
                    authService.toLogin();
                }else{

                    switch (response.status) {
                    case 401:
                        authService.clearToken();
                        var deferred = $q.defer();
                        if (!inFlightAuthRequest) {
                            inFlightAuthRequest = $injector.get("$http").post(
                                    URLS.api_refresh_token, { 
                                        refreshtoken : authService.getRefreshToken()
                                    });
                        }
                        inFlightAuthRequest.then(function(r) {
                            inFlightAuthRequest = null;
                            console.log(JSON.stringify(r));
                            authService.setToken(r.data.accesstoken);
                            $injector.get("$http")(response.config).then(function(resp) {
                                deferred.resolve(resp);
                            }, function(resp) {
                                deferred.reject(resp);
                            });
                        }, function(error) {
                            inFlightAuthRequest = null;
                            deferred.reject();
                            authService.clearAllToken();
                            authService.toLogin();
                            return;
                        });
                        return deferred.promise;
                        break;
                    default:
                        return $q.reject(response);
                    break;
                    }
                    return response || $q.when(response);
                }
            }
        }
    }

})();

토큰 갱신을 위해 인터셉터에 여러 요구가 동시에 착신하는 동안 첫 번째 요구만 전송하여 토큰을 취득하고 첫 번째 요구가 응답과 함께 돌아올 때까지 다른 HTTP 요구를 기다립니다.응답을 가져오면 모든 http 요청 헤더에 새로운 토큰 정보가 설정되고 해당 헤더가 처리됩니다.이 방법에서는 새로운 토큰을 얻기 위해 한 번 요구합니다.

private static accessTokenError$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

인터셉터에서는 스태틱부울 subjectBehaviour를 사용하여 첫 번째 요청을 추적하고 첫 번째 요청을 보낸 후 동일한 코드를 실행하는 다음 요청 전송이 이루어지도록 대상 동작 상태를 업데이트합니다.

if (!JwtInterceptor.accessTokenError$.getValue()) {
          // isRrefreshing = true;
          JwtInterceptor.accessTokenError$.next(true);

          return this.authService.getNewRefreshToken().pipe(
            switchMap((newTokens: any) => {
              const transformedReq = req.clone({
                headers: req.headers.set(
                  "Authorization",
                  `bearer ${newTokens.data.token}`
                ),
              });
              JwtInterceptor.accessTokenError$.next(false);
              return next.handle(transformedReq);
            }), catchError(error => {
              return throwError(error);
            })
          );
        } else {
           // If it's not the firt error, it has to wait until get the access/refresh token
           return this.waitNewTokens().pipe(
            switchMap((event: any) => {
                // Clone the request with new Access Token
                const newRequest = req.clone({
                    setHeaders: {
                        Authorization: `bearer ${localStorage.getItem('accessToken')}`
                    }
                });
                return next.handle(newRequest);
            })
        );
        }

그리고 이것은 첫 번째 응답자가 응답할 때까지 요청을 기다리는 방법입니다.

 private waitNewTokens(): Observable<any> {
const subject = new Subject<any>();
const waitToken$: Subscription = JwtInterceptor.accessTokenError$.subscribe((error: boolean) => {
    if(!error) {
        subject.next();
        waitToken$.unsubscribe();
    }
});
return subject.asObservable();

}

언급URL : https://stackoverflow.com/questions/26552892/angularjs-http-interceptor-resend-all-request-after-token-refresh

반응형