HapiJS 代理问题

HapiJS Proxy Trouble

TL;DR

我如何拦截一个请求,ping 一个 ID 的不同路由,将 ID 存储在 session,然后使用我的 ID 继续原始请求(尤其是 PUT/POST 和负载)刚拿到?

背景

我正在使用 HapiJS (8) 将来自客户端的请求代理到现有的 API(我无法控制 processes/logic)。 API 要求每个请求在查询字符串或负载中包含一个 'session ID'(取决于 http 方法)。为了获得一个 session ID,我所要做的就是索要一个……不需要 username/pwd(它在 headers 中使用基本身份验证)。如果不更新,session ID 每 24 小时过期一次。每个客户都有自己的 session ID。

我目前正在使用 hapi-auth-cookie 来存储 session ID 的值,在需要 ID 时会查询该值。如果 ID 已过期或为空,我需要在客户端请求成功代理到 API.

之前请求一个新 ID

当前解决方案

当客户端的请求方法是 'GET' 时,我正在利用 hapi-auth-cookie docs. The request is intercepted by hapi-auth-cookie, if a new session ID is needed, a request is sent to that particular route to get it, the API returns the ID which is then assigned to the Hapi session, and then (using Wreck) a reply.redirect [=66 中描述的 appendNext 配置非常优雅地处理这个挑战=] 到它完成的原始 GET 请求。无缝优雅。

但是,我不知道如何使用包含数据有效负载的不同 http 方法来完成相同的流程。

除了 reply.redirect 之外,还有什么东西可以在保持原始有效负载和方法的同时实现相同的目标吗?或者一般来说有更好的方法来做到这一点吗?

代码(目前适用于 'GET' 个请求)

主应用程序文件(hapi-auth-cookie 配置)

# register plugins
server.register require('./plugins')
, (err) ->
    throw err if err

    # set authentication (NLS) configs
    server.auth.strategy 'session', 'cookie',
        password: 'session_pwd'
        cookie: 'ghsid'
        redirectTo: '/api/SessionID' #get new session ID
        isSecure: config.get 'ssl'
        appendNext: true
        ttl: config.get 'session_length'

利用 session 身份验证并调用 hapi-auth-cookie 插件的控制器:

simpleRequest:
    auth: 'session'
    handler: (request, reply) ->
        qs = Qs.stringify request.query
        request.papi_url = "/api/route/sample?#{qs}"

        reply.proxy
            mapUri: (request, reply) ->
                auth = config.get 'basic_auth'
                api_host = config.get 'api_host'
                papi_url = request.papi_url
                path = api_host + papi_url
                next null, path, {authorization: auth}

获取新 session ID 的路径

module.exports = [

    {
        path: '/api/SessionID'
        method: 'GET'
        config: SessionController.session
    }

]

Session 控制器

Wreck       = require 'wreck'
config      = require 'config'

module.exports =
    session:
        description: 'Get new session ID'

        auth:
            mode: 'try'
            strategy: 'session'

        plugins:
            'hapi-auth-cookie':
                redirectTo: false

        handler: (request, reply) ->

            # request configs
            papi_url = "/Session"
            api_host = config.get 'api_host'
            url = api_host + papi_url
            opts =
                headers:
                    'Authorization': config.get 'basic_auth'
                    'content-type': 'application/json;charset=UTF-8'

            # make request to PAPI
            Wreck.post url, opts, (err, res, body) ->
                throw new Error err if err

                try
                    bdy = JSON.parse body
                    sess =
                        nls: bdy.SessionId

                    if bdy.SessionId
                        # authenticate user with NLS
                        request.auth.session.set sess

                        # redirect to initial route
                        reply.redirect request.url.query.next

                    else
                        return throw new Error

                catch err
                    throw new Error err

最终解决方案

根据 Matt Harrison 的回答,我创建了一个自定义插件,该插件被注册为身份验证方案,因此我可以按路由控制它。

插件代码如下:

Wreck           = require 'wreck'
config          = require 'config'

exports.register = (server, options, next) ->
    server.auth.scheme 'cookie', internals.implementation
    next()

exports.register.attributes =
    name: 'Hapi Session Interceptor'
    version: '1.0.0'

internals = {}

internals.implementation = (server, options, next) ->

    scheme = authenticate: (request, reply) ->

        validate = ->

            session = request.state.sessionID
            unless session
                return unauthenticated()

            reply.continue(credentials: {'session': session})

        unauthenticated = ->

            api_url = "/SessionID"
            api_host = config.get 'api_host'
            url = api_host + api_url
            opts =
                headers:
                    'Authorization': config.get 'basic_auth'
                    'content-type': 'application/json;charset=UTF-8'

            # make request to API
            Wreck.post url, opts, (err, res, body) ->
                throw new Error err if err

                bdy = JSON.parse body
                sess =
                    session: bdy.SessionId

                if bdy.SessionId
                    reply.state 'sessionID', bdy.SessionId
                    reply.continue(credentials: sess)

                else
                    return throw new Error

        validate()

    return scheme

虽然不完全忠实于您的代码,但我已经整理了一个示例,其中包含我认为您正在使用的所有部分。

我制作了一个 service 插件来代表 你的 API。 upstream 插件代表您要代理的实际上游 API。

所有请求都通过 service 并代理到 upstream,它只打印出所有 headers 和收到的有效负载。

如果原始请求不包含带有 sessionId 的 cookie,则会在 upstream 上命中一条路由以获取一个。然后,当响应返回下游时,使用此值设置 cookie。

代码在这里:https://github.com/mtharrison/hapijs-proxy-trouble

使用 curl 和您的浏览器试试看。

获取: curl http://localhost:4000

POST W/PAYLOAD: curl -X POST -H "content-type: application/json" -d '{"example":"payload"}' http://localhost:4000

index.js

var Hapi = require('hapi');

var server = new Hapi.Server();

server.connection({ port: 4000, labels: ['service'] }); // Your service
server.connection({ port: 5000, labels: ['upstream']}); // Pretend upstream API

server.state('session', {
    ttl: 24 * 60 * 60 * 1000,
    isSecure: false,
    path: '/',
    encoding: 'base64json'
});

server.register([{
    register: require('./service')
}, {
    register: require('./upstream')
}], 
function (err) {

    if (err) {
        throw err;
    }

    server.start(function () {

        console.log('Started!');
    });

});

service.js

var Wreck = require('wreck');

exports.register = function (server, options, next) {

    // This is where the magic happens!

    server.select('service').ext('onPreHandler', function (request, reply) {

        var sessionId = request.state.session;

        var _done = function () {

            // Set the cookie and proceed to the route

            request.headers['X-Session-Id'] = sessionId;
            reply.state('session', sessionId);
            reply.continue();
        }

        if (typeof sessionId !== 'undefined')
            return _done();

        // We don't have a sessionId, let's get one

        Wreck.get('http://localhost:5000/sessionId', {json: true}, function (err, res, payload) {

            if(err) {
                throw err;
            }

            sessionId = payload.id;

            _done();
        });
    });

    server.select('service').route({
        method: '*',
        path: '/{p*}',  // Proxies all routes and methods
        handler: {
            proxy: {
                host: 'localhost',
                port: 5000,
                protocol: 'http',
                passThrough: true
            }
        }
    });

    next();
};

exports.register.attributes = {
    name: 'your-service'    
};

upstream.js

exports.register = function (server, options, next) {

    server.select('upstream').route([{
        method: '*',
        path: '/{p*}',
        handler: function (request, reply) {

            // Just prints out what it received for headers and payload
            // To prove we got send the original payload and the sessionID header

            reply({
                originalHeaders: request.headers,
                originalPayload: request.payload,
            })
        }
    }, {
        method: 'GET',
        path: '/sessionId',
        handler: function (request, reply) {

            // Returns a random session id

            reply({ id: (Math.floor(Math.random() * 1000)) });
        }
    }]);

    next();    
};

exports.register.attributes = {
    name: 'upstream'    
};