Node.js确保API登录仅发生一次

时间:2019-06-27 15:00:39

标签: javascript node.js asynchronous promise request

我为Node.js编写了以下代码以连接到CMS的API。 API必须使用用户名和密码登录一次,然后继续使用访问令牌进行请求。但是,访问令牌的有效时间有限-之后,API必须再次登录并接收新令牌。

当我向API发出请求时,我总是尝试使用当前令牌-如果它不再有效,我将收到403并触发新的登录,然后再次执行初始请求。

这里的问题是,如果多个请求同时尝试访问API,它们都会触发登录-这导致CMS中出现多个API会话,这是一件坏事。我以为我已经用Promises处理了这种情况,但是显然我的代码没有我想象的那样。

var api = {
    url: 'servername',
    user: 'user',
    password: 'password',
    token: null
};
var login_in_progress = true;
var loginPromise;
function loginApi() {
    login_in_progress = true;
    return new Promise(function (resolve, reject) {
        request({
            uri: api.url + api.user + ',' + api.password,
            method: 'GET',
            json: true
        }, function (err, res, data) {
            if (!err && res.statusCode === 200) {
                api.token = data.token;
                login_in_progress = false;
                resolve(data);
            } else {
                login_in_progress = false;
                reject(err);
            }
        });
    });
}

var getContent = function (query) {
    // Currently a login is running - wait till it's finished and then execute get
    // at least that was the idea - but does not seem to work
    if (login_in_progress && loginPromise) {
        return new Promise(function (resolve, reject) {
            loginPromise
                .then(function () {
                    getContent(query)
                        .then(function (data) {
                            resolve(data);
                        })
                        .catch(function (err) {
                            reject(err);
                        });
                })
                .catch(function (err) {
                    reject(err);
                });
        });
    } else {
        // Do the actual request
        // case 403 Api is logged out => reLogin
        loginPromise = loginApi();
        loginPromise
            .then(function () {
                getContent(query)
                    .then(function (data) {
                        resolve(data);
                    })
                    .catch(function (err) {
                        reject(err);
                    });
            })
            .catch(function (err) {
                reject(err)
            });
    }
}

显然,没有检查当前是否未运行登录,并且getContent函数始终会运行到else语句中。我什至不确定我的想法是否可以检查正在运行的登录请求。

3 个答案:

答案 0 :(得分:1)

我相信类似的方法可以工作(nb:这确实是“伪代码”,而不是可运行的代码):

let authPromise = undefined;

function getContent(query) {
    if (!authPromise) {
        // no login pending - start one, then restart the whole function
        authPromise = login();
        return authPromise.then(() => getContent(query));
    } else {
        // login pending - wait for it, then make the real call
        // if the login promise already resolved, it won't wait
        return authPromise.then(() => makeAjaxRequest(query)).then((response) => {
            if (response.status === 403) {
                // session expired - remove the state, and restart the whole function
                authPromise = undefined;
                return getContent(query);
            } else {
                // session still valid, return the API response
                return response;
            }
        });
    }
}

唯一的状态是是否存在Promise

答案 1 :(得分:1)

在您的代码中有几件事可以纠正或简化。我在这里用变量testlogged模拟登录和注销,该变量替换了原始代码中的403响应,并用超时替换了登录延迟,只是为了演示如何使用最少的代码来完成该操作。

注意:

  • 您似乎不需要在getContent中嵌套嵌套的Promise。尚不清楚该函数是否使用另一个函数来获取返回承诺的内容,或者是否需要本身返回承诺。这不是同一回事。对于第二种情况,您可以参见示例2。
  • 您需要对getContent返回的内容保持一致,并了解要成为承诺的内容。您可以将回调传递给getContent(示例1),也可以在其上使用then(示例2)。
  • 在示例1中,在其内部调用getContent很简单,但是如果它返回一个Promise我不愿意,因为它带来了额外的复杂性,从而迫使在函数本身内部对其使用then
  • 例如2,如果getContent从一开始返回一个新的Promise(包装所有代码),则更加清楚

示例1

var api = {
    url: 'servername',
    user: 'user',
    password: 'password',
    token: null
};
var testlogged = false;
var login_in_progress = true;
var loginPromise;
function loginApi() {
    login_in_progress = true;
    return new Promise(function (resolve, reject) {
        setTimeout(function(){
            console.log('login OK');
            login_in_progress = false;
            testlogged = true;
            resolve();
        }, 10);
    });
}

function getContent(query, callback) {
    // Currently a login is running - wait till it's finished and then execute get
    if (login_in_progress && loginPromise) {
        console.log('login in progress, waiting before getting content.. query: ' + query);
        loginPromise
            .then(function () {
                callback('got content OK (1) query: ' + query);
            })
            .catch(function (err) {
                console.error('error: ', err);
            });
    } else {
        // Do the actual request
        if (!testlogged) {
            // case 403 Api is logged out => reLogin
            console.log('starting login.. query: ' + query);
            loginPromise = loginApi();
            //restarting function to put query in queue, saves code, but you could also do directly loginPromise.then(function () {...
            getContent(query, callback);
        } else {
            console.log('already logged');
            callback('got content OK (2) query: ' + query);
        }
    }
}

var theCallback = function(data){
    console.log(data);
};

//doing first attempt while not logged
getContent('test', theCallback);
//trying with a concurrent attempt while still logging
getContent('test2', theCallback);
//simulating attempt while still being logged then after logout (waiting first that the 2 precendent attempts are finished)
setTimeout(function(){
    getContent('test3', theCallback);
}, 100);
setTimeout(function(){
    console.log('simulate log out');
    testlogged = false;
    getContent('test4', theCallback);
}, 100);

示例2

var api = {
    url: 'servername',
    user: 'user',
    password: 'password',
    token: null
};
var testlogged = false;
var login_in_progress = true;
var loginPromise;
function loginApi() {
    login_in_progress = true;
    return new Promise(function (resolve, reject) {
        setTimeout(function(){
            console.log('login OK');
            login_in_progress = false;
            testlogged = true;
            resolve();
        }, 10);
    });
}

var getContent = function (query) {
    return new Promise(function (resolve, reject) {
        // Currently a login is running - wait till it's finished and then execute get
        if (login_in_progress && loginPromise) {
            console.log('login in progress, waiting before getting content.. query: ' + query);
            loginPromise
                .then(function () {
                    resolve('got content OK (1) query: ' + query);
                })
                .catch(function (err) {
                    reject(err);
                });
        } else {
            // Do the actual request
            if (!testlogged) {
                // case 403 Api is logged out => reLogin
                console.log('starting login.. query: ' + query);
                loginPromise = loginApi();
                loginPromise
                    .then(function () {
                        resolve('got content OK (1) query: ' + query);
                    })
                    .catch(function (err) {
                        reject(err);
                    });
            } else {
                console.log('already logged');
                resolve('got content OK (2) query: ' + query);
            }
        }
    });
}

var theCallback = function(data){
    console.log(data);
};

//doing first attempt while not logged
getContent('test').then(theCallback);
//trying with a concurrent attempt while still logging
getContent('test2').then(theCallback);
//simulating attempt while still being logged then after logout (waiting first that the 2 precendent attempts are finished)
setTimeout(function(){
    getContent('test3').then(theCallback);
}, 100);
setTimeout(function(){
    console.log('simulate log out');
    testlogged = false;
    getContent('test4').then(theCallback);
}, 150);

答案 2 :(得分:0)

因此,虽然答案可以帮助我更多地思考问题,但实际问题却与众不同: 我只在login_inprogress启动时检查getContent,而不是在403返回时检查。 同时启动请求时,这会导致多次API登录。

解决方案实际上非常简单:

// case 403 Api is logged out => reLogin
// check again if login is running NOW, only if not, start one
if(!login_in_progress){
    loginPromise = loginApi();
}
// Always attach to the loginPromise to redo the initial getContent
loginPromise
    .then(function () {
        getContent(query)
            .then(function (data) {
                resolve(data);
            })
            .catch(function (err) {
                reject(err);
            });
    })
    .catch(function (err) {
        reject(err)
    });