Laravel、WebSockets - 在服务器上验证用户

Laravel, WebSockets - Verify user on server

我目前 运行 使用 LaravelRachetPHP[来处理服务器上用户的身份验证问题

到目前为止我尝试了什么:

通用用例:

一位用户在话题中发表评论。发送对象的有效负载将是:

如果您要发送用户 ID,任何人都可以修改数据包并以其他用户身份发送消息。

我的用例:

一个用户可以有 n 个字符。角色有头像、id、名字等。 该用户仅用于:

我还有一个table locations - 一个"virtual place",一个角色可以在...所以我得到了一对一角色位置之间的关系。然后用户(角色)可以通过 websocket 在某个位置发送消息。消息被插入到服务器上的数据库中。此时,我需要知道:

如果您需要更多信息,请告诉我。

我想我找到了解决办法。虽然不是很干净,但它做了它应该做的(我猜...)

WebSocket 服务器由 Artisan 命令 (by mmochetti@github) 启动。我将这些 类 注入到命令中:

  • Illuminate\Contracts\Encryption\Encrypter
  • App\Contracts\CsrfTokenVerifier - 自定义 CsrfTokenVerifier,它只比较 2 个字符串(将在其中放置更多后续逻辑代码)

我将这些实例从命令传递到服务器。在 onMessage 方法上,我解析发送的消息,包含:

  • 用户的CSRF-Token
  • 用户的角色id

然后我检查令牌是否有效,以及用户是否是角色的所有者。

public function onMessage(ConnectionInterface $from, NetworkMessage $message) {
    if (!$this->verifyCsrfToken($from, $message)) {
        throw new TokenMismatchException;
    }

    if (!$this->verifyUser($from, $message)) {
        throw new \Exception('test');
    }
    ...
}

private function verifyUser(ConnectionInterface $conn, NetworkMessage $message) {
    $cookies         = $conn->WebSocket->request->getCookies();
    $laravel_session = rawurldecode($cookies['laravel_session']);
    $id              = $this->encrypter->decrypt($laravel_session);
    $session         = Session::find($id);
    $payload         = unserialize(base64_decode($session->payload));
    $user_id         = $payload['user_id'];

    $user       = User::find($user_id);
    $characters = $this->characterService->allFrom($user);

    $character_id = $message->getHeader()['character_id'];

    return $characters->contains($character_id);
}

private function verifyCsrfToken($from, NetworkMessage $message) {
    $header = $this->getHeaderToken($from);
    return $this->verifier->tokensMatch($header, $message->getId());
}

代码当然可以更简洁,但作为快速破解,它确实有效。我认为,我应该使用 Laravel DatabaseSessionHandler.

而不是为 Session 使用模型

这就是我刚才解决这个问题的方法。在我的示例中,我正在使用 Socket.IO,但我很确定您可以轻松地重写 Socket.IO 部分以使其也可以与 RachetPHP 一起使用。

套接字服务器

套接字服务器依赖于文件 cookie.js and array.js 和节点模块 express、http、socket.io、request 和 dotenv。我不是 cookie.js 的原作者,但评论中没有提到作者,所以我无法为此做出任何贡献,抱歉。

这是启动服务器的 server.js 文件。它是一个简单的套接字服务器,用于跟踪当前在线的用户。然而,有趣的部分是当服务器向 Laravel 应用程序上的 socket/auth 发出 POST 请求时:

var express = require('express');
var app = express();
var server = require('http').Server(app)
var io = require('socket.io')(server);
var request = require('request');
var co = require('./cookie.js');
var array = require('./array.js');

// This loads the Laravel .env file
require('dotenv').config({path: '../.env'});

server.listen(process.env.SOCKET_SERVER_PORT);

var activeSockets = {};
var disconnectTimeouts = {};

// When a client connects
io.on('connection', function(socket)
{
    console.log('Client connected...');

    // Read the laravel_session cookie.
    var cookieManager = new co.cookie(socket.handshake.headers.cookie);
    var sess = cookieManager.get("laravel_session"); // Rename "laravel_session" to whatever you called it

    // This is where the socket asks the Laravel app to authenticate the user
    request.post('http://' + process.env.SOCKET_SERVER_HOST + '/socket/auth?s=' + sess, function(error, response, body)
    {
        try {
            // Parse the response from the server
            body = JSON.parse(body);
        }
        catch(e)
        {
            console.log('Error while parsing JSON', e);
            error = true;
        }

        if ( ! error && response.statusCode == 200 && body.authenticated)
        {
            // Assign users ID to the socket
            socket.userId = body.user.id;

            if ( ! array.contains(activeSockets, socket.userId))
            {
                // The client is now 'active'
                activeSockets.push(socket.userId);

                var message = body.user.firstname + ' is now online!';
                console.log(message);

                // Tell everyone that the user has joined
                socket.broadcast.emit('userJoined', socket.userId);
            }
            else if (array.hasKey(disconnectTimeouts, 'user_' + socket.userId))
            {
                clearTimeout(disconnectTimeouts['user_' + socket.userId]);
                delete disconnectTimeouts['user_id' + socket.userId];
            }

            socket.on('disconnect', function()
            {
                // The client is 'inactive' if he doesn't reastablish the connection within 10 seconds
                // For a 'who is online' list, this timeout ensures that the client does not disappear and reappear on each page reload
                disconnectTimeouts['user_' + socket.userId] = setTimeout(function()
                {
                    delete disconnectTimeouts['user_' + socket.userId];
                    array.remove(activeSockets, socket.userId);

                    var message = body.user.firstname + ' is now offline.';
                    console.log(message);

                    socket.broadcast.emit('userLeft', socket.userId);
                }, 10000);
            });
        }
    });
});

我在代码中添加了一些注释,所以它应该是不言自明的。请注意,我将 SOCKET_SERVER_HOSTSOCKET_SERVER_PORT 添加到我的 Laravel .env 文件中,以便能够在不编辑代码和 运行 服务器的情况下更改主机和端口在不同的环境中。

SOCKET_SERVER_HOST = localhost
SOCKET_SERVER_PORT = 1337

使用 Laravel

通过会话 cookie 验证用户

这是 SocketController,它解析 cookie 并响应用户是否可以通过身份验证(JSON 响应)。它与您在 中描述的机制相同。在控制器中处理 cookie 解析并不是最好的设计,但在这种情况下应该没问题,因为控制器只处理一件事,它的功能不会在应用程序的另一点使用。

/app/Http/Controllers/SocketController.php

<?php namespace App\Http\Controllers;

use App\Http\Requests;

use App\Users\UserRepositoryInterface;
use Illuminate\Auth\Guard;
use Illuminate\Database\DatabaseManager;
use Illuminate\Encryption\Encrypter;
use Illuminate\Http\Request;
use Illuminate\Routing\ResponseFactory;

/**
 * Class SocketController
 * @package App\Http\Controllers
 */
class SocketController extends Controller {

    /**
     * @var Encrypter
     */
    private $encrypter;

    /**
     * @var DatabaseManager
     */
    private $database;

    /**
     * @var UserRepositoryInterface
     */
    private $users;

    /**
     * Initialize a new SocketController instance.
     *
     * @param Encrypter $encrypter
     * @param DatabaseManager $database
     * @param UserRepositoryInterface $users
     */
    public function __construct(Encrypter $encrypter, DatabaseManager $database, UserRepositoryInterface $users)
    {
        parent::__construct();

        $this->middleware('internal');

        $this->encrypter = $encrypter;
        $this->database = $database;
        $this->users = $users;
    }

    /**
     * Authorize a user from node.js socket server.
     *
     * @param Request $request
     * @param ResponseFactory $response
     * @param Guard $auth
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function authenticate(Request $request, ResponseFactory $response, Guard $auth)
    {
        try
        {
            $payload = $this->getPayload($request->get('s'));
        } catch (\Exception $e)
        {
            return $response->json([
                'authenticated' => false,
                'message'       => $e->getMessage()
            ]);
        }

        $user = $this->users->find($payload->{$auth->getName()});

        return $response->json([
            'authenticated' => true,
            'user'          => $user->toArray()
        ]);
    }

    /**
     * Get session payload from encrypted laravel session.
     *
     * @param $session
     * @return object
     * @throws \Exception
     */
    private function getPayload($session)
    {
        $sessionId = $this->encrypter->decrypt($session);
        $sessionEntry = $this->getSession($sessionId);

        $payload = base64_decode($sessionEntry->payload);

        return (object) unserialize($payload);
    }

    /**
     * Fetches base64 encoded session string from the database.
     *
     * @param $sessionId
     * @return mixed
     * @throws \Exception
     */
    private function getSession($sessionId)
    {
        $sessionEntry = $this->database->connection()
            ->table('sessions')->select('*')->whereId($sessionId)->first();

        if (is_null($sessionEntry))
        {
            throw new \Exception('The session could not be found. [Session ID: ' . $sessionId . ']');
        }

        return $sessionEntry;
    }

}

在构造函数中你可以看到我引用了 internal 中间件。我添加这个中间件只允许套接字服务器向 socket/auth.

发出请求

这是中间件的样子:

/app/Http/Middleware/InternalMiddleware.php

<?php namespace App\Http\Middleware;

use Closure;
use Illuminate\Routing\ResponseFactory;

class InternalMiddleware {

    /**
     * @var ResponseFactory
     */
    private $response;

    /**
     * @param ResponseFactory $response
     */
    public function __construct(ResponseFactory $response)
    {
        $this->response = $response;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if (preg_match(env('INTERNAL_MIDDLEWARE_IP'), $request->ip()))
        {
            return $next($request);
        }

        return $this->response->make('Unauthorized', 401);
    }

}

为了让这个中间件工作,在内核中注册它并添加 INTERNAL_MIDDLEWARE_IP 属性 - 这只是一个定义允许哪些 IP 地址的正则表达式 - 到你的 .env 文件:

本地测试(任意IP):

INTERNAL_MIDDLEWARE_IP = /^.*$/

生产环境:

INTERNAL_MIDDLEWARE_IP = /^192\.168\.0\.1$/

很抱歉我无法帮助您解决 RachetPHP 问题,但我认为您很清楚如何解决这个问题。

对于 Laravel > 5 我使用此代码:

    $cookies = $conn->WebSocket->request->getCookies();
    $laravel_session = rawurldecode($cookies['laravel_session']);
    $id = $this->encrypter->decrypt($laravel_session);

    if(Config::get('session.driver', 'file') == 'file')
    {
        $session = File::get(storage_path('framework/sessions/' . $id));
    }

    $session = array_values(unserialize($session));

    return $session[4]; // todo: Hack, please think another solution

要通过 websocket 从客户端获取 cookie,您必须在会话配置中更改域并将所有 websocket 主机更改为您的域:

'domain' => 'your.domain.com',