SPA 中的 ADAL JS 强制注销
ADAL JS Forced Logout in SPA
我已经用 Angular 编写了一个 SPA,并且正在使用基于 Azure AD 令牌的身份验证,以及通过不记名令牌保护的资源(如 API)。据我所知,这允许 SPA 从 Azure 检索续订令牌并永远保持登录状态。
我正在使用 Angular 的 ADAL JavaScript 库来执行此操作:
https://github.com/AzureAD/azure-activedirectory-library-for-js
现在需要将用户会话限制在预定的时间量内,比如说 15 小时。
我写了一个不错的小服务,它在登录时存储一个 cookie,每个请求都会检查 cookie 中的日期以及它们是否超过了最大会话限制。如果用户持续使用该应用程序或关闭浏览器,这会很好地工作 - 但如果他们让浏览器保持打开状态,令牌将在后台更新并且他们保持登录状态。
我现在正在尝试使用静默注销解决方案来解决这个问题。意思是,我希望用户在会话过期后被强制进入安全登录页面。
这似乎是一种常见的情况,但我似乎无法弄清楚如何实现它,因为 ADAL 在后台使用 iFrame。我考虑过使用 timer/interval,但这似乎很不靠谱。
仅供参考,我根据下面的代码示例使用 adalAuthenticationService.logout()。我还尝试清除会话缓存,这有效 - 但 ADAL 会静默刷新令牌。我还尝试将 redirectUri 位置设置为未经身份验证的页面,但它仍然只会在用户采取操作时重定向到那里。如果浏览器只是保持打开状态,令牌将自行重置。
var maxTime = 15; // hours allowed in session
// event to fire check; maybe this can be different, and is my problem?
$rootScope.$on('$viewContentLoaded', function () {
$scope.checkLogoutCookie();
});
$scope.logout = function() {
adalAuthenticationService.logout();
};
function setCookie(c) {} // implementation details don't matter....
function getCookie(c) {} // implementation details don't matter....
$scope.checkLogoutCookie = function () {
var lastLogin = getCookie("lastLogin");
var loginDate = new Date();
if (lastLogin === "") { // is empty
setCookie("lastLogin", loginDate, 365);
} else {
var lastDate = new Date(lastLogin);
var hours = Math.abs(lastDate - loginDate) / 36e5;
if (hours > maxTime) {
setCookie("lastLogin", "", 0);
$scope.logout();
}
}
}
I wrote a nice little service which stores a cookie upon login, every request it checks the date in the cookie and if they have exceeded their maximum session limit. This works fine if the user is continually uses the app, or closes the browser - but if they leave their browser open, the token will simply renew in the background and they remain logged in.
根据描述,检查日期的代码似乎是在adal-angular.js的HTTP请求拦截器之后执行的。
如果可以的话,需要在ADAL库的拦截器之前实现这个函数
如果不行,可以把业务逻辑改成
获取token前检查app session是否未过期。为此,我们需要更改 JavaScript.
的 Active Directory 身份验证库 (ADAL) 的源代码
例如,您可以更改 adal-angular.js 的 HTTP 请求拦截器,用于插入代码以检查您的应用是否在会话中。下面是拦截器代码供大家参考:
AdalModule.factory('ProtectedResourceInterceptor', ['adalAuthenticationService', '$q', '$rootScope', '$templateCache', function (authService, $q, $rootScope, $templateCache) {
return {
request: function (config) {
if (config) {
config.headers = config.headers || {};
// if the request can be served via templateCache, no need to token
if ($templateCache.get(config.url)) return config;
var resource = authService.getResourceForEndpoint(config.url);
authService.verbose('Url: ' + config.url + ' maps to resource: ' + resource);
if (resource === null) {
return config;
}
//add/modify the code here
var tokenStored = authService.getCachedToken(resource);
if (tokenStored) {
authService.info('Token is available for this url ' + config.url);
// check endpoint mapping if provided
config.headers.Authorization = 'Bearer ' + tokenStored;
return config;
}
else {
// Cancel request if login is starting
if (authService.loginInProgress()) {
if (authService.config.popUp) {
authService.info('Url: ' + config.url + ' will be loaded after login is successful');
var delayedRequest = $q.defer();
$rootScope.$on('adal:loginSuccess', function (event, token) {
if (token) {
authService.info('Login completed, sending request for ' + config.url);
config.headers.Authorization = 'Bearer ' + tokenStored;
delayedRequest.resolve(config);
}
});
return delayedRequest.promise;
}
else {
authService.info('login is in progress.');
config.data = 'login in progress, cancelling the request for ' + config.url;
return $q.reject(config);
}
}
else {
// delayed request to return after iframe completes
var delayedRequest = $q.defer();
authService.acquireToken(resource).then(function (token) {
authService.verbose('Token is available');
config.headers.Authorization = 'Bearer ' + token;
delayedRequest.resolve(config);
}, function (error) {
config.data = error;
delayedRequest.reject(config);
});
return delayedRequest.promise;
}
}
}
},
responseError: function (rejection) {
authService.info('Getting error in the response: ' + JSON.stringify(rejection));
if (rejection) {
if (rejection.status === 401) {
var resource = authService.getResourceForEndpoint(rejection.config.url);
authService.clearCacheForResource(resource);
$rootScope.$broadcast('adal:notAuthorized', rejection, resource);
}
else {
$rootScope.$broadcast('adal:errorResponse', rejection);
}
return $q.reject(rejection);
}
}
};
}]);
看看adalAuthenticationService
的方法logOut()
。我认为问题在于您使用 logout()
不正确。请注意logout()
方法中o
的大写
我已经用 Angular 编写了一个 SPA,并且正在使用基于 Azure AD 令牌的身份验证,以及通过不记名令牌保护的资源(如 API)。据我所知,这允许 SPA 从 Azure 检索续订令牌并永远保持登录状态。
我正在使用 Angular 的 ADAL JavaScript 库来执行此操作:
https://github.com/AzureAD/azure-activedirectory-library-for-js
现在需要将用户会话限制在预定的时间量内,比如说 15 小时。
我写了一个不错的小服务,它在登录时存储一个 cookie,每个请求都会检查 cookie 中的日期以及它们是否超过了最大会话限制。如果用户持续使用该应用程序或关闭浏览器,这会很好地工作 - 但如果他们让浏览器保持打开状态,令牌将在后台更新并且他们保持登录状态。
我现在正在尝试使用静默注销解决方案来解决这个问题。意思是,我希望用户在会话过期后被强制进入安全登录页面。
这似乎是一种常见的情况,但我似乎无法弄清楚如何实现它,因为 ADAL 在后台使用 iFrame。我考虑过使用 timer/interval,但这似乎很不靠谱。
仅供参考,我根据下面的代码示例使用 adalAuthenticationService.logout()。我还尝试清除会话缓存,这有效 - 但 ADAL 会静默刷新令牌。我还尝试将 redirectUri 位置设置为未经身份验证的页面,但它仍然只会在用户采取操作时重定向到那里。如果浏览器只是保持打开状态,令牌将自行重置。
var maxTime = 15; // hours allowed in session
// event to fire check; maybe this can be different, and is my problem?
$rootScope.$on('$viewContentLoaded', function () {
$scope.checkLogoutCookie();
});
$scope.logout = function() {
adalAuthenticationService.logout();
};
function setCookie(c) {} // implementation details don't matter....
function getCookie(c) {} // implementation details don't matter....
$scope.checkLogoutCookie = function () {
var lastLogin = getCookie("lastLogin");
var loginDate = new Date();
if (lastLogin === "") { // is empty
setCookie("lastLogin", loginDate, 365);
} else {
var lastDate = new Date(lastLogin);
var hours = Math.abs(lastDate - loginDate) / 36e5;
if (hours > maxTime) {
setCookie("lastLogin", "", 0);
$scope.logout();
}
}
}
I wrote a nice little service which stores a cookie upon login, every request it checks the date in the cookie and if they have exceeded their maximum session limit. This works fine if the user is continually uses the app, or closes the browser - but if they leave their browser open, the token will simply renew in the background and they remain logged in.
根据描述,检查日期的代码似乎是在adal-angular.js的HTTP请求拦截器之后执行的。
如果可以的话,需要在ADAL库的拦截器之前实现这个函数
如果不行,可以把业务逻辑改成 获取token前检查app session是否未过期。为此,我们需要更改 JavaScript.
的 Active Directory 身份验证库 (ADAL) 的源代码例如,您可以更改 adal-angular.js 的 HTTP 请求拦截器,用于插入代码以检查您的应用是否在会话中。下面是拦截器代码供大家参考:
AdalModule.factory('ProtectedResourceInterceptor', ['adalAuthenticationService', '$q', '$rootScope', '$templateCache', function (authService, $q, $rootScope, $templateCache) {
return {
request: function (config) {
if (config) {
config.headers = config.headers || {};
// if the request can be served via templateCache, no need to token
if ($templateCache.get(config.url)) return config;
var resource = authService.getResourceForEndpoint(config.url);
authService.verbose('Url: ' + config.url + ' maps to resource: ' + resource);
if (resource === null) {
return config;
}
//add/modify the code here
var tokenStored = authService.getCachedToken(resource);
if (tokenStored) {
authService.info('Token is available for this url ' + config.url);
// check endpoint mapping if provided
config.headers.Authorization = 'Bearer ' + tokenStored;
return config;
}
else {
// Cancel request if login is starting
if (authService.loginInProgress()) {
if (authService.config.popUp) {
authService.info('Url: ' + config.url + ' will be loaded after login is successful');
var delayedRequest = $q.defer();
$rootScope.$on('adal:loginSuccess', function (event, token) {
if (token) {
authService.info('Login completed, sending request for ' + config.url);
config.headers.Authorization = 'Bearer ' + tokenStored;
delayedRequest.resolve(config);
}
});
return delayedRequest.promise;
}
else {
authService.info('login is in progress.');
config.data = 'login in progress, cancelling the request for ' + config.url;
return $q.reject(config);
}
}
else {
// delayed request to return after iframe completes
var delayedRequest = $q.defer();
authService.acquireToken(resource).then(function (token) {
authService.verbose('Token is available');
config.headers.Authorization = 'Bearer ' + token;
delayedRequest.resolve(config);
}, function (error) {
config.data = error;
delayedRequest.reject(config);
});
return delayedRequest.promise;
}
}
}
},
responseError: function (rejection) {
authService.info('Getting error in the response: ' + JSON.stringify(rejection));
if (rejection) {
if (rejection.status === 401) {
var resource = authService.getResourceForEndpoint(rejection.config.url);
authService.clearCacheForResource(resource);
$rootScope.$broadcast('adal:notAuthorized', rejection, resource);
}
else {
$rootScope.$broadcast('adal:errorResponse', rejection);
}
return $q.reject(rejection);
}
}
};
}]);
看看adalAuthenticationService
的方法logOut()
。我认为问题在于您使用 logout()
不正确。请注意logout()
方法中o
的大写