我有一些Express中间件处理来自我的客户端应用程序的GET请求,以便后续请求使用OAuth2令牌的单独API服务器,我也使用express-session
来存储这些令牌。
在我发出传出请求的中间件中,我添加了处理以应对访问令牌过期的情况(API服务器发回403)并发出刷新令牌的请求,之后它将发出相同的原始文件对API服务器的传出请求,因此客户端不知道这一切都在继续。然后,检索到的新令牌将通过express-session
持久保存回会话存储,以便在后续请求中使用。令牌也用于设置授权承载令牌标头,您将在下面进一步了解。
以下是我的Express代码所涉及的部分:
routes.controller.js
//Currently handling GET API requests from client
module.exports.fetch = function(req, res) {
var options = helpers.buildAPIRequestOptions(req);
helpers.performOutgoingRequest(req, res, options);
};
helpers.js
module.exports.buildAPIRequestOptions = function(req, url) {
var options = {};
options.method = req.method;
options.uri = 'http://someurl.com' + req.path;
options.qs = req.query;
options.headers = {
'Authorization': 'Bearer ' + req.session.accessToken
};
return options;
};
module.exports.performOutgoingRequest = function(req, res, options) {
request(options, function(err, response, body){
if(response.statusCode === 401){
console.log(chalk.red('\n--- 401 RESPONSE RECEIVED TRY REFRESHING TOKENS ---'));
//Note the third param to call below is a callback and is invoked when calling next() in the refreshToken middleware
authController.refreshToken(req, res, function(){
console.log(chalk.green('\n--- RETRYING ORIGINAL REQUEST WITH UPDATED ACCESS TOKEN ---'));
//Re-use original request options, but making sure we update the Authorization header beforehand
options.headers.Authorization = 'Bearer ' + req.session.accessToken;
retryOutgoingRequest(res, options);
});
} else {
res.status(response.statusCode).send(body);
}
});
};
function retryOutgoingRequest(res, options) {
request(options, function(err, response, body){
if(err) {
console.log(err);
}
res.status(response.statusCode).send(body);
});
};
auth.controller.js
module.exports.refreshToken = function(req, res, next) {
var formData = {
grant_type: 'refresh_token',
refresh_token: req.session.refreshToken
},
headers = {
'Authorization' : 'Basic ' + consts.CLIENT_KEY_SECRET_BASE64
};
request.post({url:consts.ACCESS_TOKEN_REQUEST_URL, form:formData, headers: headers, rejectUnauthorized: false}, function(err, response, body){
var responseBody = JSON.parse(body);
if (response.statusCode === 200) {
req.session.accessToken = responseBody.access_token;
req.session.refreshToken = responseBody.refresh_token;
next();
} else {
console.log(chalk.yellow('A problem occurred refreshing tokens, sending 401 HTTP response back to client...'));
res.status(401).send();
}
});
};
在大多数情况下,上述工作正常
当用户首次登录时,会从API服务器获取一些其他用户个人资料信息,然后再将其转到应用程序的主页面。
应用程序中的某些页面也会在页面加载时获取数据,因此需要进行访问令牌检查。
在正常使用期间,所以当用户登录并开始点击页面时,我可以看到令牌被换出并在会话存储中通过express-session
过期时保存。根据我编写的中间件,新的访问令牌正确地用于后续请求。
我现在有一个我的中间件不起作用的场景。
所以说我在一个加载页面加载数据的页面上,让我们说它是一个订单页面。如果我等到API服务器上配置的令牌到期时间已经过去然后刷新浏览器,则客户端应用程序将首先请求用户信息,然后成功将请求页面所需的订单数据(使用AngularJS承诺)
在我的Express应用程序中,用户信息请求从API服务器获取403,因此令牌通过上面的中间件得到刷新,req.session.accessToken
得到更新,我可以通过我的服务器应用程序中的控制台日志记录看到。但是,下次获取订单数据最终会使用先前设置的访问令牌,这会导致API服务器发生进一步的未经授权的错误,因为请求是使用无效令牌进行的。
如果我再次刷新浏览器,则使用先前中间件流中正确更新的令牌获取用户信息和订单。
所以我不确定这里发生了什么,我想知道req.session
对象的时间问题是否没有被持久化回到会话存储中下次请求接收的时间?
任何人都有任何想法可能会发生在这里?
由于
更新1
根据评论中的要求,以下是两个请求的请求和响应标头。
第一个请求(使用更新的令牌服务器端)
请求标题
GET /api/userinfo HTTP/1.1
Host: localhost:5000
Connection: keep-alive
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36
Referer: https://localhost:5000/
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-GB,en-US;q=0.8,en;q=0.6
Cookie: interact.sid=s%3A0NDG_bn67NeGQAYl1wP1-TmM19ExavFm.Zjv65e9BtSyNBuo%2FDxZEk2Np0963frVur4zHyYw3y5I
响应标题
HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=86400
X-Download-Options: noopen
X-XSS-Protection: 1; mode=block
Content-Type: text/html; charset=utf-8
Content-Length: 364
ETag: W/"16c-4AIbpZmTm3I+Yl+SbZdirw"
set-cookie: interact.sid=s%3A0NDG_bn67NeGQAYl1wP1-TmM19ExavFm.Zjv65e9BtSyNBuo%2FDxZEk2Np0963frVur4zHyYw3y5I; Path=/; Expires=Fri, 13 May 2016 11:54:56 GMT; HttpOnly; Secure
Date: Fri, 13 May 2016 11:24:56 GMT
Connection: keep-alive
第二个请求(使用旧令牌服务器端)
请求标题
GET /api/customers HTTP/1.1
Host: localhost:5000
Connection: keep-alive
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36
Referer: https://localhost:5000/
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-GB,en-US;q=0.8,en;q=0.6
Cookie: interact.sid=s%3A0NDG_bn67NeGQAYl1wP1-TmM19ExavFm.Zjv65e9BtSyNBuo%2FDxZEk2Np0963frVur4zHyYw3y5I
响应标题
HTTP/1.1 401 Unauthorized
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=86400
X-Download-Options: noopen
X-XSS-Protection: 1; mode=block
set-cookie: interact.sid=s%3A0NDG_bn67NeGQAYl1wP1-TmM19ExavFm.Zjv65e9BtSyNBuo%2FDxZEk2Np0963frVur4zHyYw3y5I; Path=/; Expires=Fri, 13 May 2016 11:54:56 GMT; HttpOnly; Secure
Date: Fri, 13 May 2016 11:24:56 GMT
Connection: keep-alive
Content-Length: 0
更新2
我还应该提到我在会话存储中使用connect-mongo
,我尝试使用默认的内存存储但存在相同的行为。
答案 0 :(得分:3)
它听起来像竞争条件客户端,如果你正在执行2个请求(检查auth - 然后获取数据)是第二个(获取数据)嵌套到第一次调用成功?或者你是否同时线性地呼叫?
我的想法是:
客户端 - 发送用户信息请求(sessionid 1) - 服务器处理
客户端 - 获取订单信息请求(sessionid 1) - 服务器处理
服务器 - 响应用户信息 - 403 - 客户端更新会话ID
服务器 - 响应订单信息 - 403
你真正想要的是:
客户端 - 发送用户信息请求(会话1) - 服务器处理
服务器 - 获取用户信息请求(403) - 客户端更新会话ID
客户端 - 获取订单信息请求(会话2) - 服务器处理
服务器 - 响应订单信息 - 实际结果