使用 Angular 拦截未经授权的 API 呼叫
Intercept Unathorized API calls with Angular
我试图拦截 401
和 403
错误来刷新用户令牌,但我无法正常工作。我所取得的只是这个拦截器:
app.config(function ($httpProvider) {
$httpProvider.interceptors.push(function ($q, $injector) {
return {
// On request success
request: function (config) {
var deferred = $q.defer();
if ((config.url.indexOf('API URL') !== -1)) {
// If any API resource call, get the token firstly
$injector.get('AuthenticationFactory').getToken().then(function (token) {
config.headers.Authorization = token;
deferred.resolve(config);
});
} else {
deferred.resolve(config);
}
return deferred.promise;
},
response: function (response) {
// Return the promise response.
return response || $q.when(response);
},
responseError: function (response) {
// Access token invalid or expired
if (response.status == 403 || response.status == 401) {
var $http = $injector.get('$http');
var deferred = $q.defer();
// Refresh token!
$injector.get('AuthenticationFactory').getToken().then(function (token) {
response.config.headers.Authorization = token;
$http(response.config).then(deferred.resolve, deferred.reject);
});
return deferred.promise;
}
return $q.reject(response);
}
}
});
});
问题是 responseError
执行 'refreshes' 的无限循环,因为通过授权 header 使用更新的令牌,$http(response.config)
调用未接收到.
1.- App has an invalid token stored.
2.- App needs to do an API call
2.1 Interceptor catch the `request`.
2.2 Get the (invalid) stored token and set the Authorization header.
2.3 Interceptor does the API call with the (invalid) token setted.
3.- API respond that used token is invalid or expired (403 or 401 statuses)
3.1 Interceptor catch the `responseError`
3.2 Refresh the expired token, get a new VALID token and set it in the Authorization header.
3.3 Retry the point (2) with the valid refreshed token `$http(response.config)`
循环发生在 (3.3) 点,因为授权 header 从来没有新的刷新有效令牌,而是过期的令牌。我不知道为什么,因为它应该设置在 responseError
身份验证工厂
app.factory('AuthenticationFactory', function($rootScope, $q, $http, $location, $log, URI, SessionService) {
var deferred = $q.defer();
var cacheSession = function(tokens) {
SessionService.clear();
// Then, we set the tokens
$log.debug('Setting tokens...');
SessionService.set('authenticated', true);
SessionService.set('access_token', tokens.access_token);
SessionService.set('token_type', tokens.token_type);
SessionService.set('expires', tokens.expires);
SessionService.set('expires_in', tokens.expires_in);
SessionService.set('refresh_token', tokens.refresh_token);
SessionService.set('user_id', tokens.user_id);
return true;
};
var uncacheSession = function() {
$log.debug('Logging out. Clearing all');
SessionService.clear();
};
return {
login: function(credentials) {
var login = $http.post(URI+'/login', credentials).then(function(response) {
cacheSession(response.data);
}, function(response) {
return response;
});
return login;
},
logout: function() {
uncacheSession();
},
isLoggedIn: function() {
if(SessionService.get('authenticated')) {
return true;
}
else {
return false;
}
},
isExpired: function() {
var unix = Math.round(+new Date()/1000);
if (unix < SessionService.get('expires')) {
// not expired
return false;
}
// If not authenticated or expired
return true;
},
refreshToken: function() {
var request_params = {
grant_type: "refresh_token",
refresh_token: SessionService.get('refresh_token')
};
return $http({
method: 'POST',
url: URI+'/refresh',
data: request_params
});
},
getToken: function() {
if( ! this.isExpired()) {
deferred.resolve(SessionService.get('access_token'));
} else {
this.refreshToken().then(function(response) {
$log.debug('Token refreshed!');
if(angular.isUndefined(response.data) || angular.isUndefined(response.data.access_token))
{
$log.debug('Error while trying to refresh token!');
uncacheSession();
}
else {
SessionService.set('access_token', response.data.access_token);
SessionService.set('token_type', response.data.token_type);
SessionService.set('expires', tokens.expires);
SessionService.set('expires_in', response.data.expires_in);
deferred.resolve(response.data.access_token);
}
}, function() {
// Error
$log.debug('Error while trying to refresh token!');
uncacheSession();
});
}
return deferred.promise;
}
};
});
笨蛋
我做了一个 plunker & backend 来尝试重现这个问题。
您的拦截器需要跟踪它是否请求新的身份验证令牌 "in flight"。如果是这样,您需要等待正在进行的请求的结果,而不是启动一个新请求。您可以通过缓存 AuthRequest
返回的 promise
并使用缓存的承诺而不是为每个 API 请求创建一个新承诺来做到这一点。
Here is an answer to a similar question that demonstrates this.
以您为例 - 这是一个示例实现:
app.config(function ($httpProvider) {
$httpProvider.interceptors.push(function ($q, $injector) {
var inFlightRequest = null;
return {
// On request success
request: function (config) {
var deferred = $q.defer();
if ((config.url.indexOf('API URL') !== -1)) {
// If any API resource call, get the token firstly
$injector.get('AuthenticationFactory').getToken().then(function (token) {
config.headers.Authorization = token;
deferred.resolve(config);
});
} else {
deferred.resolve(config);
}
return deferred.promise;
},
response: function (response) {
// Return the promise response.
return response || $q.when(response);
},
responseError: function (response) {
// Access token invalid or expired
if (response.status == 403 || response.status == 401) {
var $http = $injector.get('$http');
var deferred = $q.defer();
// Refresh token!
if(!inFlightRequest){
inFlightRequest = $injector.get('AuthenticationFactory').refreshToken();
}
//all requests will wait on the same auth request now:
inFlightRequest.then(function (token) {
//clear the inFlightRequest so that new errors will generate a new AuthRequest.
inFlightRequest = null;
response.config.headers.Authorization = token;
$http(response.config).then(deferred.resolve, deferred.reject);
}, function(err){
//error handling omitted for brevity
});
return deferred.promise;
}
return $q.reject(response);
}
}
});
});
更新:
从你的胡言乱语中我不清楚到底是什么问题,但你的 AuthenticationService 有问题。推荐的更改如下 and here is a Plunkr 更完整(包括跟踪飞行请求):
app.factory('AuthenticationFactory', function($rootScope, $q, $http, $location, $log, URI, SessionService) {
//this deferred declaration should be moved. As it is, it's created once and re-resolved many times, which isn't how promises work. Subsequent calls to resolve essentially are noops.
//var deferred = $q.defer();
var cacheSession = function(tokens) {
SessionService.clear();
// Then, we set the tokens
$log.debug('Setting tokens...');
SessionService.set('authenticated', true);
SessionService.set('access_token', tokens.access_token);
SessionService.set('token_type', tokens.token_type);
SessionService.set('expires', tokens.expires);
SessionService.set('expires_in', tokens.expires_in);
SessionService.set('refresh_token', tokens.refresh_token);
SessionService.set('user_id', tokens.user_id);
return true;
};
var uncacheSession = function() {
$log.debug('Logging out. Clearing all');
SessionService.clear();
};
return {
login: function(credentials) {
var login = $http.post(URI+'/login', credentials).then(function(response) {
cacheSession(response.data);
}, function(response) {
return response;
});
return login;
},
logout: function() {
uncacheSession();
},
isLoggedIn: function() {
if(SessionService.get('authenticated')) {
return true;
}
else {
return false;
}
},
isExpired: function() {
var unix = Math.round(+new Date()/1000);
if (unix < SessionService.get('expires')) {
// not expired
return false;
}
// If not authenticated or expired
return true;
},
refreshToken: function() {
var request_params = {
grant_type: "refresh_token",
refresh_token: SessionService.get('refresh_token')
};
return $http({
method: 'POST',
url: URI+'/refresh',
data: request_params
});
},
getToken: function() {
//It should be moved here - a new defer should be created for each invocation of getToken();
var deferred = $q.defer();
if( ! this.isExpired()) {
deferred.resolve(SessionService.get('access_token'));
} else {
this.refreshToken().then(function(response) {
$log.debug('Token refreshed!');
if(angular.isUndefined(response.data) || angular.isUndefined(response.data.access_token))
{
$log.debug('Error while trying to refresh token!');
uncacheSession();
}
else {
SessionService.set('access_token', response.data.access_token);
SessionService.set('token_type', response.data.token_type);
SessionService.set('expires', tokens.expires);
SessionService.set('expires_in', response.data.expires_in);
deferred.resolve(response.data.access_token);
}
}, function() {
// Error
$log.debug('Error while trying to refresh token!');
uncacheSession();
});
}
return deferred.promise;
}
};
});
最后一点,跟踪进行中的 getToken 请求和进行中的 refreshToken 请求将防止您对服务器进行过多调用。在高负载下,您可能会创建比您需要的更多的访问令牌。
更新 2:
此外,查看代码,当您收到 401 错误时,您正在调用 refreshToken()。但是refreshToken并没有将新的token信息放到session缓存中,所以新的请求会继续使用旧的token。更新了 Plunkr。
我试图拦截 401
和 403
错误来刷新用户令牌,但我无法正常工作。我所取得的只是这个拦截器:
app.config(function ($httpProvider) {
$httpProvider.interceptors.push(function ($q, $injector) {
return {
// On request success
request: function (config) {
var deferred = $q.defer();
if ((config.url.indexOf('API URL') !== -1)) {
// If any API resource call, get the token firstly
$injector.get('AuthenticationFactory').getToken().then(function (token) {
config.headers.Authorization = token;
deferred.resolve(config);
});
} else {
deferred.resolve(config);
}
return deferred.promise;
},
response: function (response) {
// Return the promise response.
return response || $q.when(response);
},
responseError: function (response) {
// Access token invalid or expired
if (response.status == 403 || response.status == 401) {
var $http = $injector.get('$http');
var deferred = $q.defer();
// Refresh token!
$injector.get('AuthenticationFactory').getToken().then(function (token) {
response.config.headers.Authorization = token;
$http(response.config).then(deferred.resolve, deferred.reject);
});
return deferred.promise;
}
return $q.reject(response);
}
}
});
});
问题是 responseError
执行 'refreshes' 的无限循环,因为通过授权 header 使用更新的令牌,$http(response.config)
调用未接收到.
1.- App has an invalid token stored.
2.- App needs to do an API call
2.1 Interceptor catch the `request`.
2.2 Get the (invalid) stored token and set the Authorization header.
2.3 Interceptor does the API call with the (invalid) token setted.
3.- API respond that used token is invalid or expired (403 or 401 statuses)
3.1 Interceptor catch the `responseError`
3.2 Refresh the expired token, get a new VALID token and set it in the Authorization header.
3.3 Retry the point (2) with the valid refreshed token `$http(response.config)`
循环发生在 (3.3) 点,因为授权 header 从来没有新的刷新有效令牌,而是过期的令牌。我不知道为什么,因为它应该设置在 responseError
身份验证工厂
app.factory('AuthenticationFactory', function($rootScope, $q, $http, $location, $log, URI, SessionService) {
var deferred = $q.defer();
var cacheSession = function(tokens) {
SessionService.clear();
// Then, we set the tokens
$log.debug('Setting tokens...');
SessionService.set('authenticated', true);
SessionService.set('access_token', tokens.access_token);
SessionService.set('token_type', tokens.token_type);
SessionService.set('expires', tokens.expires);
SessionService.set('expires_in', tokens.expires_in);
SessionService.set('refresh_token', tokens.refresh_token);
SessionService.set('user_id', tokens.user_id);
return true;
};
var uncacheSession = function() {
$log.debug('Logging out. Clearing all');
SessionService.clear();
};
return {
login: function(credentials) {
var login = $http.post(URI+'/login', credentials).then(function(response) {
cacheSession(response.data);
}, function(response) {
return response;
});
return login;
},
logout: function() {
uncacheSession();
},
isLoggedIn: function() {
if(SessionService.get('authenticated')) {
return true;
}
else {
return false;
}
},
isExpired: function() {
var unix = Math.round(+new Date()/1000);
if (unix < SessionService.get('expires')) {
// not expired
return false;
}
// If not authenticated or expired
return true;
},
refreshToken: function() {
var request_params = {
grant_type: "refresh_token",
refresh_token: SessionService.get('refresh_token')
};
return $http({
method: 'POST',
url: URI+'/refresh',
data: request_params
});
},
getToken: function() {
if( ! this.isExpired()) {
deferred.resolve(SessionService.get('access_token'));
} else {
this.refreshToken().then(function(response) {
$log.debug('Token refreshed!');
if(angular.isUndefined(response.data) || angular.isUndefined(response.data.access_token))
{
$log.debug('Error while trying to refresh token!');
uncacheSession();
}
else {
SessionService.set('access_token', response.data.access_token);
SessionService.set('token_type', response.data.token_type);
SessionService.set('expires', tokens.expires);
SessionService.set('expires_in', response.data.expires_in);
deferred.resolve(response.data.access_token);
}
}, function() {
// Error
$log.debug('Error while trying to refresh token!');
uncacheSession();
});
}
return deferred.promise;
}
};
});
笨蛋
我做了一个 plunker & backend 来尝试重现这个问题。
您的拦截器需要跟踪它是否请求新的身份验证令牌 "in flight"。如果是这样,您需要等待正在进行的请求的结果,而不是启动一个新请求。您可以通过缓存 AuthRequest
返回的 promise
并使用缓存的承诺而不是为每个 API 请求创建一个新承诺来做到这一点。
Here is an answer to a similar question that demonstrates this.
以您为例 - 这是一个示例实现:
app.config(function ($httpProvider) {
$httpProvider.interceptors.push(function ($q, $injector) {
var inFlightRequest = null;
return {
// On request success
request: function (config) {
var deferred = $q.defer();
if ((config.url.indexOf('API URL') !== -1)) {
// If any API resource call, get the token firstly
$injector.get('AuthenticationFactory').getToken().then(function (token) {
config.headers.Authorization = token;
deferred.resolve(config);
});
} else {
deferred.resolve(config);
}
return deferred.promise;
},
response: function (response) {
// Return the promise response.
return response || $q.when(response);
},
responseError: function (response) {
// Access token invalid or expired
if (response.status == 403 || response.status == 401) {
var $http = $injector.get('$http');
var deferred = $q.defer();
// Refresh token!
if(!inFlightRequest){
inFlightRequest = $injector.get('AuthenticationFactory').refreshToken();
}
//all requests will wait on the same auth request now:
inFlightRequest.then(function (token) {
//clear the inFlightRequest so that new errors will generate a new AuthRequest.
inFlightRequest = null;
response.config.headers.Authorization = token;
$http(response.config).then(deferred.resolve, deferred.reject);
}, function(err){
//error handling omitted for brevity
});
return deferred.promise;
}
return $q.reject(response);
}
}
});
});
更新:
从你的胡言乱语中我不清楚到底是什么问题,但你的 AuthenticationService 有问题。推荐的更改如下 and here is a Plunkr 更完整(包括跟踪飞行请求):
app.factory('AuthenticationFactory', function($rootScope, $q, $http, $location, $log, URI, SessionService) {
//this deferred declaration should be moved. As it is, it's created once and re-resolved many times, which isn't how promises work. Subsequent calls to resolve essentially are noops.
//var deferred = $q.defer();
var cacheSession = function(tokens) {
SessionService.clear();
// Then, we set the tokens
$log.debug('Setting tokens...');
SessionService.set('authenticated', true);
SessionService.set('access_token', tokens.access_token);
SessionService.set('token_type', tokens.token_type);
SessionService.set('expires', tokens.expires);
SessionService.set('expires_in', tokens.expires_in);
SessionService.set('refresh_token', tokens.refresh_token);
SessionService.set('user_id', tokens.user_id);
return true;
};
var uncacheSession = function() {
$log.debug('Logging out. Clearing all');
SessionService.clear();
};
return {
login: function(credentials) {
var login = $http.post(URI+'/login', credentials).then(function(response) {
cacheSession(response.data);
}, function(response) {
return response;
});
return login;
},
logout: function() {
uncacheSession();
},
isLoggedIn: function() {
if(SessionService.get('authenticated')) {
return true;
}
else {
return false;
}
},
isExpired: function() {
var unix = Math.round(+new Date()/1000);
if (unix < SessionService.get('expires')) {
// not expired
return false;
}
// If not authenticated or expired
return true;
},
refreshToken: function() {
var request_params = {
grant_type: "refresh_token",
refresh_token: SessionService.get('refresh_token')
};
return $http({
method: 'POST',
url: URI+'/refresh',
data: request_params
});
},
getToken: function() {
//It should be moved here - a new defer should be created for each invocation of getToken();
var deferred = $q.defer();
if( ! this.isExpired()) {
deferred.resolve(SessionService.get('access_token'));
} else {
this.refreshToken().then(function(response) {
$log.debug('Token refreshed!');
if(angular.isUndefined(response.data) || angular.isUndefined(response.data.access_token))
{
$log.debug('Error while trying to refresh token!');
uncacheSession();
}
else {
SessionService.set('access_token', response.data.access_token);
SessionService.set('token_type', response.data.token_type);
SessionService.set('expires', tokens.expires);
SessionService.set('expires_in', response.data.expires_in);
deferred.resolve(response.data.access_token);
}
}, function() {
// Error
$log.debug('Error while trying to refresh token!');
uncacheSession();
});
}
return deferred.promise;
}
};
});
最后一点,跟踪进行中的 getToken 请求和进行中的 refreshToken 请求将防止您对服务器进行过多调用。在高负载下,您可能会创建比您需要的更多的访问令牌。
更新 2:
此外,查看代码,当您收到 401 错误时,您正在调用 refreshToken()。但是refreshToken并没有将新的token信息放到session缓存中,所以新的请求会继续使用旧的token。更新了 Plunkr。