我有一个现有的单页应用程序,我已将其转换为 JWT 而不是 PHP 会话。它运行良好,但我想弄清楚如何在 AJAX 请求之前刷新 JWT(如有必要)。
一点背景:用户登录后,我存储一个 JWT(10 分钟到期)和一个刷新令牌(2 小时到期)。我有以下函数,我想在对 API 的每次 AJAX 调用之前运行。该函数检查 JWT 是否已过期,如果已过期,它会通过刷新令牌检索新的 JWT。显然,在 API 中,我在允许数据发回之前检查了 JWT 和刷新令牌的有效性。这部分工作完美。
function checkTokenExp(){
var validToken = false; // DEFAULT TO EXPIRED
var apiClaims = localStorage.getItem('apiToken').split('.');
var api = JSON.parse(atob(apiClaims[1]));
var apiExpiration = new Date(api['exp'] * 1000);
var now = new Date();
if (apiExpiration < now) {
// API TOKEN EXPIRED - CHECK REFRESH TOKEN
var refreshClaims = localStorage.getItem('refreshToken').split('.');
var refresh = JSON.parse(atob(refreshClaims[1]));
var refreshExpiration = new Date(refresh['exp'] * 1000);
if (refreshExpiration > now) {
// REFRESH TOKEN NOT EXPIRED - NEED NEW API TOKEN
$.ajax({
type: "GET",
url: 'api/session/token',
contentType: 'application/json',
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization", 'Bearer '+localStorage.getItem('refreshToken'));
}
}).done(function (data) {
var data = jQuery.parseJSON(data);
if (data.status == 'success'){
localStorage.setItem('apiToken', data.apiToken);
}
validToken = true;
});
}else{
// REFRESH TOKEN EXPIRED - FORCE LOG OUT
$("#SessionExpiredModal").modal('show');
}
}else{
// API TOKEN NOT EXPIRED
validToken = true;
}
return validToken;
}
我遇到麻烦的地方是下面的代码(以及以后的所有其他 AJAX 调用)。如您所见,我正在尝试在此 AJAX 调用之前运行上面的函数并验证我的 JWT(如果它无效,则请求并存储一个新的),然后使用新的 JWT 运行 AJAX 调用。一切正常,除了 AJAX 返回 401 未经授权,因为它仍在发送以前的 JWT(显然已过期)。因此,似乎在上述功能完成并存储新的 JWT 之前发送了 AJAX。但我知道我的功能运行正常并且 JWT 已更新,因为我第二次单击按钮时它运行正常并且我从 API 得到了正确的回复。
$('#BtnTest').on('click', function(){
$.ajax({
type: "GET",
url: 'api/random',
contentType: 'application/json',
beforeSend: function (xhr) {
checkTokenExp(); // SHOULD BE SAVING NEW TOKEN
xhr.setRequestHeader("Authorization", 'Bearer '+ localStorage.getItem('apiToken')); // SHOULD BE SENDING NEW TOKEN
}
}).done(function (response) {
alert(response);
})
});
我希望这是有道理的,并提前感谢您的帮助。另外,有没有一种简单的方法可以让我的函数在每次 ajax 调用之前运行,而不必在每个 AJAX 语句中对其进行编码。
您需要向
checkTokenExp()
添加回调,以便在刷新 API 令牌后完成 API 调用。
现在发生了什么:
api/random
beforeSend
执行checkTokenExp()
api/random
的实际调用可能在服务器用新令牌回复第3步之前你可以尝试这样的事情:
var currentRequests = {};
$.ajaxPrefilter(function( options, originalOptions, jqXHR ) {
// Skip URLs that don't need a token
if( options.url == 'your api/session/token URL' ) {
console.log("No ajax prefilter on api/session/token");
return;
}
successCb = function() {
currentRequests[ options.url ] = jqXHR;
};
errorCb = function() {
console.error("Could not refresh token, aborting Ajax call to " + options.url);
currentRequests[ options.url ].abort();
};
checkTokenExp(successCb, errorCb);
});
function checkTokenExp(successCb, errorCb){
// ...
if (refreshExpiration > now) {
// REFRESH TOKEN NOT EXPIRED - NEED NEW API TOKEN
$.ajax({
type: "GET",
url: 'api/session/token',
contentType: 'application/json',
}).done(function (data) {
var data = jQuery.parseJSON(data);
if (data.status == 'success'){
localStorage.setItem('apiToken', data.apiToken);
successCb(); // the new token is set, you can make ajax calls
}
// TODO: log error here, don't do validToken = true
}).fail(errorCb(data));
}else{
// REFRESH TOKEN EXPIRED - FORCE LOG OUT
$("#SessionExpiredModal").modal('show');
}
}else{
// API TOKEN NOT EXPIRED
successCb();
}
}
在 JQuery 中,使用 Ajax Prefilter
function setUpAjaxPrefilter() {
if (!ajaxPrefiltered) {
ajaxPrefiltered = true; // once
const origOptsList = [];
$.ajaxPrefilter(function onPrefilter(opts, ajaxOpts, jqXHR) {
if (opts.authRefresh) {
return; // don't do it again
}
const dfd = $.Deferred();
// if the non refresh request was done just resolve our deferred
jqXHR.done(dfd.resolve);
// if the request failed, handle
jqXHR.fail(function onAjaxFail(...args) {
const { status, url } = jqXHR;
// keep list of failed requests while refresh token request still outstanding
origOptsList.push(ajaxOpts);
window.XHR = {
...jqXHR,
...opts,
};
// keep copy of XHR in window global for logging purpose
const { browserSession } = APPLICATION;
if (!url && !refreshing && browserSession && status === 401) {
refreshing = true;
return browserSession
.refresh()
.then((response) => {
refreshing = false;
const { access_token: accessToken } = response;
// retry failed requests
const retries = [];
while (origOptsList.length && accessToken) {
const origOpts = origOptsList.shift();
const newOpts = {
...origOpts,
authRefresh: true, // prevent infinite loop in case of bad token
headers: { Authorization: `Bearer ${accessToken}` },
};
retries.push($.ajax(newOpts).then(dfd.resolve, dfd.reject));
}
return Promise.allSettled(retries);
})
.catch((error) => {
refreshing = false;
jqXHR.status = error.status || 401;
jqXHR.statusText = error.message || 'token expired';
dfd.rejectWith(jqXHR, [...args]);
});
} else {
if (jqXHR.status === 200) {
return dfd.resolve(args);
// status cancel happens when beforeSend cancels $.ajax request
} else if (jqXHR.statusText && !jqXHR.statusText.includes('cancel')) {
// eslint-disable-next-line no-console
console.error(jqXHR, jqXHR.responseText);
}
return dfd.rejectWith(jqXHR, [...args]);
}
});
});
}
}