Laravel,WebSockets - 验证服务器上的用户

时间:2015-05-25 13:13:57

标签: php session laravel eloquent ratchet

我目前遇到了使用 Laravel RachetPHP 处理服务器上用户身份验证的问题。

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

  • 我将会话的驱动程序类型更改为数据库,为我提供了id和payload列。使用\Session::getId()返回40个字符的字符串。 由WebSocket-Connection发送的cookie信息确实包含XSRF-TOKEN和laravel_session,两者都包含> 200个字符串。用户会话的数据库ID与\Session::getId()返回的ID不同。

  • 我已经通过websocket消息发送了当前的CSRF令牌,但我不知道如何验证它(内置验证器使用请求 - 我在websocket服务器中没有这些请求范围)。

通用用例:

用户在帖子中发表评论。然后,发送对象的有效负载为:

  • 用于验证用户的内容(ID或令牌)。
  • 评论本身

如果你要发送用户ID,任何人都可以调整数据包并在另一个用户下发送消息。

我的用例:

用户可以拥有 n 字符。角色有头像,身份证,姓名等。 用户仅用于:

  • 在服务器上进行身份验证。
  • 访问他的角色,从而对他的角色进行基本的CRUD操作。

我还有一个表位置 - 一个"虚拟地点",一个角色可以... ...所以我在之间建立了一对一的关系角色位置。然后,用户(角色)可以通过websocket在一个位置发送消息。消息将插入服务器上的数据库中。此时,我需要知道:

  • 如果用户已通过身份验证(csrf-token?)
  • 如果用户是角色的所有者(使用其他用户的角色ID欺骗请求非常简单)

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

3 个答案:

答案 0 :(得分:3)

所以这就是我不久前解决这个问题的方法。在我的例子中,我使用的是Socket.IO,但我非常确定你可以轻松地重写Socket.IO部分以使其与RachetPHP一起使用。

套接字服务器

套接字服务器依赖于文件cookie.jsarray.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响应)。它与您在your answer中描述的机制相同。它不是处理控制器中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,但我认为你很清楚如何解决这个问题。

答案 1 :(得分:0)

我想我找到了解决方案。虽然不是很干净,但它可以做它应该做的事情(我猜......)

WebSocket-Server由Artisan Command(by mmochetti@github)启动。我将这些类注入命令:

  • Illuminate\Contracts\Encryption\Encrypter
  • App\Contracts\CsrfTokenVerifier - 一个自定义的CsrfTokenVerifier,它只是比较2个字符串(将更多的跟随逻辑代码放在那里)

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

  • 用户的CSRF令牌
  • 用户的字符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的模型

答案 2 :(得分:0)

对于Laravel&gt; 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',