我有一些 Express 中间件处理来自客户端应用程序的 GET 请求,以向使用 OAuth2 token 的单独 API 服务器发出后续请求,我还使用 express-session
来存储这些 token .
在发出传出请求的中间件中,我添加了处理以应对访问 token 过期的情况(API 服务器发回 403)并请求刷新 token ,之后它将发出相同的原始请求向 API 服务器发出请求,因此客户端不知道这一切。检索到的新 token 随后通过 express-session
持久保存回 session 存储,以供后续请求使用。这些 token 还用于设置授权不记名 token header ,您将在下面进一步看到。
这是我的 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 服务器获取。
应用程序中的某些页面还会在页面加载时获取数据,因此需要接受访问 token 检查。
在正常使用期间,当用户登录并开始点击页面时,我可以看到 token 被换出并通过 express-session
保存在 session 存储中他们过期了。根据我编写的中间件,新的访问 token 已正确用于后续请求。
我现在遇到的情况是我的中间件无法正常工作。
假设我在一个在页面加载时加载数据的页面上,假设它是一个订单页面。如果我等到 API 服务器上配置的 token 过期时间过去然后刷新浏览器,客户端应用程序将首先请求用户信息,成功后将请求页面所需的订单数据(使用AngularJS promise )
在我的 Express 应用程序中,用户信息请求从 API 服务器获取 403,因此 token 通过我上面的中间件得到刷新,req.session.accessToken
得到更新,我可以通过控制台看到登录我的服务器应用程序。但是下一次获取订单数据最终会使用之前设置的访问 token ,这会导致 API 服务器进一步发生未经授权的错误,因为请求是使用无效 token 发出的。
如果我再次刷新浏览器,用户信息和订单都会使用之前中间件流程中正确更新的 token 获取。
所以我不确定这里发生了什么,我想知道这是否是 req.session
对象没有及时返回 session 存储以供下一次请求的时间问题捡起来?
有人知道这里可能发生了什么吗?
谢谢
更新 1
按照评论中的要求,这里是两个请求的请求和响应 header 。
第一个请求(使用更新的 token 服务器端)
请求 header
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
响应 header
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
第二个请求(使用旧 token 服务器端)
请求 header
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
响应 header
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
作为我的 session 存储,我尝试使用默认内存存储,但存在相同的行为。
最佳答案
这听起来像是一个竞争条件客户端,如果您正在执行 2 个请求(检查身份验证 - 然后获取数据),第二个(获取数据)是否嵌套到第一个调用成功?还是同时线性调用两者?
我的想法是:
客户端——发送用户信息请求(sessionid 1)——服务器处理
客户端——获取订单信息请求(sessionid 1)——服务器处理
服务器 - 响应用户信息 - 403 - 客户端更新 session ID
服务器 - 响应订单信息 - 403
你真正想要的是:
客户端——发送用户信息请求(session 1)——服务器处理
服务器 - 获取用户信息请求 (403) - 客户端更新 session ID
客户端 - 获取订单信息请求( session 2) - 服务器处理
服务器-响应订单信息-实际结果
关于angularjs - 自定义 req.session 属性值的更新似乎持续得不够快,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37132634/