在现有的PHP网站中添加Node.js+Socket.io聊天机器人,并共享会话

有一个关于在现有的PHP网站上添加聊天机器人的讨论,但是该网站具有用户认证功能,并且在聊天机器人方面也有一个要求,即只允许经过认证的用户访问。

由于PHP网站使用CMS,因此我们希望尽量减少干预并实现它。因此,我们计划在PHP网站中嵌入一个iframe作为与聊天机器人的对话接口,并从Node.js提供加载iframe的页面。我们打算在由Node.js提供的页面中使用Socket.io进行聊天。

在这里出现的问题是如何在PHP和Node.js+Socket.io之间共享会话。由于每个系统都有独立的身份验证功能,这对用户来说非常不便利,所以我想要在PHP中认证用户,Node.js也能够识别。

会议分享策略

我先暂时放下上述要点,尝试研究了一下使用PHP和Node.js来共享会话的策略。

共享会话存储

参考链接:https://simplapi.wordpress.com/2012/04/11/php-nodejs-session-share-memcache/

请使用以下方法的一个来将其汉语化:

参考链接:https://simplapi.wordpress.com/2012/04/11/php-nodejs-session-share-memcache/

PHP将会话信息(在这里是memcache中)存储起来,供Node.js参考。PHP通过对用户进行验证并将会话信息存储到memcache中,Node.js根据用户发送的Cookie获取会话ID,并从memcache中获取会话信息。

其中一个问题是,PHP将会话信息序列化为自己的格式并保存在会话存储中。例如,如下所示。

user_id|s:1:"1";password|s:0:"";firstname|s:7:"Charles";

Node.jsでこれをそのまま扱うのは不便なので、上記サイトではPHPのセッションハンドラを改良し、セッションをJSON形式で保存することで、Node.jsから扱いやすいようにしている。

准备一个供Node.js从PHP访问的API

参考:http://www.slideshare.net/takyam1213/php-meets-nodejs

セッションストアを共有するのではなく、PHPにAPIを用意して、Node.jsがAPIからセッション情報やユーザ情報を取得するという方法。Node.jsはセッションIDをWebsocketハンドシェイク時にCookieから取得し、そのCookieをHTTPヘッダにつけてAPIにアクセスすることで、PHPから見ると認証済みユーザからのアクセスとなる。

除此之外,这篇讨论也对我有所启发。
https://groups.google.com/forum/#!topic/nodejs_jp/gU2347-33PQ

セッション共有を実装してみる

PHPサイトはCMSを利用しており、APIの追加開発等はしたくなかったので、セッションストアを共有する戦略でいこうと思う。

PHP的设置

由于希望将Redis用作会话存储,因此需要在php.ini中进行以下配置。

extension = redis.so
session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379"

上記参考記事で紹介したが、PHPはセッション情報を独自の形式でシリアライズするので、Node.jsからは扱いづらい。上記記事ではPHPのセッションハンドラを改良してJSON形式で保存するようにしていたが、もっと簡単な方法があった。

まず、PHPのシリアライズ形式を変更する。

;session.serialize_handler = php  # コメントアウト
session.serialize_handler = php_serialize

使用php_serialize处理程序,可以将其序列化为以下形式的数据。

$_SESSION["hoge"] = "foo";
$_SESSION["bar"] = "baz";

这会变成这样。

a:2:{s:4:"hoge";s:3:"foo";s:3:"bar";s:3:"baz";}

用Node.js处理序列化的会话字符串。

使用Node.js的php-serialize模块,您可以将上述格式的会话处理为对象。

比如说这个样子。

const phpSerializer = require('php-serialize');

const sessionStr = 'a:2:{s:4:"hoge";s:3:"foo";s:3:"bar";s:3:"baz";}';

const session = phpSerializer.unserialize(sessionStr);

console.log(`hoge: ${session.hoge}`);  // hoge: foo
console.log(`bar: ${session.bar}`);    // bar: baz

创建会话中间件

实现一个从Redis获取会话的中间件,该中间件在客户端访问时被调用。假设使用的是Node.js的Express框架,在下面的例子中模块导入和Redis客户端创建等部分省略。

function session(req, res, next) {
  if (!req.cookies) {
    console.error('must use cookie-parser middleware');
    return next();
  }

  const sessionId = req.cookies['PHPSESSID'];
  if (!sessionId) {
    return next();
  }

  const sessionKey = 'PHPREDIS_SESSION:' + sessionId;
  redis.getAsync(sessionKey)
    .then((sessionStr) => {
      if (sessionStr) {
        req.session = new Session(sessionKey, sessionStr);
      }
      return next();
    })
    .catch((err) => {
      return next(new Error('Server error'));
    });
  };
}

function Session(sessionId, sessionStr) {
  this.id = sessionId;

  let data;
  try {
    data = phpSerializer.unserialize(sessionStr);
  } catch(e) {
    return;
  }

  for (const prop in data) {
    if (!(prop in this)) {
      this[prop] = data[prop];
    }
  }
}

req.cookiesにはクライアントが送信してきたCookieが入っていて、ここにPHPサイトで発行されたセッションIDが入っているのでそれを取得する。なお、req.cookiesが存在するためにはcookie-parserミドルウェアが前段に必要。

次に、Redisからセッション文字列を取得する。デフォルトだとPHPREDIS_SESSION:というプレフィックスがついたキーで保存されている。例えばPHPREDIS_SESSION:30bn9bgoii4ndkii9grr128vf4のような感じ。

セッション文字列が取得できたら、php-serializeでデシリアライズしてオブジェクトとしてreq.sessionに格納する。

检查是否已登录

如果使用Socket.io,可以像下面这样配置中间件。

const cookieParser = require('cookie-parser');
const io = require('socket.io')();

io.use((socket, next) => {
  cookieParser()(socket.request, socket.request.res, next);
});

io.use((socket, next) => {
  session(socket.request, socket.request.res, next);
});

io.use((socket, next) => {
  if (socket.request.session.user_id) {
    next();
  } else {
    next(new Error('authentication required');
  }
});

io.on('connection', (socket) => {
  ...
});

判断登录状态的部分在不同的网站上可能有所不同,但在这里我们将用户ID的存在与否作为判断条件。如果用户ID存在,则继续前进认定为已登录,如果不存在,则设置Websocket握手失败。

总结

PHPとNode.jsでセッションを共有したいという要件はまあまああるようで、php nodejs sessionなどでググるといくつか記事がヒットした。情報の多くは海外ブログやStackOverflowで、ガチャガチャとPHPをいじる感じの内容が多いが、PHPの設定を少し変えるのと、php-serializeという便利なモジュールのおかげで結構簡単に実装できた。

日本ではあまり事例がないのか、あるけどブログに上がっていないのかよくわからないが、同じようなことをしようとしてる人に少しでも参考になればと。

另外,本次只需获取会话,但由于有时希望在Node.js端添加和保存信息,因此这些内容可以在另一个机会再讨论。

广告
将在 10 秒后关闭
bannerAds