在开发Kibana 4.x的验证插件模块的时候,遇到一个问题是当使用POST的方式提交表单数据时,会引起Kibana服务器的跨域检查机制,从而收到如下错误:
{"statusCode":400,"error":"Bad Request","message":"Missing kbn-version header"}
当前开发环境下如下:
- kibana 服务器版本:4.6.4
- Kibana Plugin Yeoman Generator 用于生成Plugin插件代码结构
- 依赖的npm模块,ldapjs(AD验证),Q(Promise模式)
kibana 服务器所需要掌握的开发技术:
- hapijs -服务器中间件,需要了解Server的lifecycle概念
- hapi-auth-cookie 基于Session sid的安全cookie验证
- AngularJS前端开发技术
查阅了一下Kibana4.6的源代码 ibana/src/server/http/xsrf.js
const disabled = config.get('server.xsrf.disableProtection');
// COMPAT: We continue to check on the kbn-version header for backwards
// compatibility since all existing consumers have been required to use it.
const versionHeader = 'kbn-version';
const xsrfHeader = 'kbn-xsrf';
server.ext('onPostAuth', function (req, reply) {
if (disabled) {
return reply.continue();
}
const isSafeMethod = req.method === 'get' || req.method === 'head';
const hasVersionHeader = versionHeader in req.headers;
const hasXsrfHeader = xsrfHeader in req.headers;
if (!isSafeMethod && !hasVersionHeader && !hasXsrfHeader) {
return reply(badRequest(`Request must contain an ${xsrfHeader} header`));
}
return reply.continue();
});
明显最简单的方式就是在配置文件中,添加server.xsrf.disableProtection节点,即可关闭相关检查。但显然这个方式也是比较简单粗暴的方法,不是很推荐。
其次我们观察到Kibana服务器主要会检查3个条件,是否是一个Get请求,Http请求头中是否包含 Version版本和xsrf的跨域信息,既然明确了改造方向,我们就来看下如何注入代码来实现功能。
1) 首先把客户端POST请求,在服务器端设置为GET,并且设置头部信息
server.ext('onRequest', function (req, reply) {
if(req.path=='/'||req.path=='/login'){
if(req.method == 'post'&& req.path == '/login'){
//TO compatible kibana xsrf protection detected
req.setMethod('get');
req.headers['kbn-xsrf'] = 'kbn-version: 4.6.4';
req.headers['kbn-version'] = '4.6.4';
readPostData(req).then(function(){
return reply.continue();
});
}
else
{
return reply.continue();
}
}
else
{
return reply.continue();
}
});
2) 由于在Hapi的生命周期内,原先POST的数据无法再通过Request.payload属性来访问,需要通过Node原始对象来取得客户端传入的参数
const readPostData = function(req){
var deferred = Q.defer();
var payloaddata = '';
//post data like: username=admin&password=admin
req.raw.req.on('data',function(chunk){
payloaddata = ''+chunk;
payloaddata.split('&').forEach(function(kv){
var toks = kv.split('=');
req.app[toks[0]] = decodeURI(toks[1]);
//console.log('set '+ toks[0] +': '+ req.app[toks[0]]);
});
deferred.resolve();
});
return deferred.promise;
}
至此再也不需要担心登录提交的信息被显示在URL里面了 :)