在现有的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端添加和保存信息,因此这些内容可以在另一个机会再讨论。