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


Laravel, WebSockets - Verify user on server

我目前遇到了在服务器上处理用户身份验证的问题,使用LaravelRachetPHP

我试过了:

  • 我将会话的驱动程序类型更改为数据库,给我一个id和有效负载列。使用'Session::getId()返回一个40个字符的字符串。由WebSocket-Connection发送的cookie信息确实包含一个XSRF-TOKEN和一个laravel_session,它们都包含> 200个字符串。用户会话的数据库ID与'Session::getId()返回的ID不一致

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

通用用例:

用户在线程中发表评论。发送对象的有效载荷将是:

  • 验证用户(ID或令牌)的东西。
  • 注释本身

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

我的用例:

一个用户可以有n个字符。角色有头像、id、名字等。用户只习惯:

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

我也有一个表位置 -一个"虚拟的地方",一个字符可以在…所以我得到了字符位置之间的一对一关系。然后,用户(字符)可以通过websocket在某个位置发送消息。消息被插入到服务器上的数据库中。此时,我需要知道:

  • 用户通过认证(csrf-token ?)
  • 如果用户是角色的所有者(很容易用另一个用户的角色id来欺骗请求)
如果你需要更多的信息,请告诉我。

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

套接字服务器

socket服务器依赖于cookie.js和array.js文件,节点模块表示,http, socket。Io,请求和请求。我不是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);
            });
        }
    });
});

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

SOCKET_SERVER_HOST = localhost
SOCKET_SERVER_PORT = 1337

使用Laravel通过会话cookie验证用户

这是SocketController,解析cookie并响应用户是否可以被认证(JSON响应)。这和你在回答中描述的机制是一样的。在控制器中处理cookie解析并不是最好的设计,但在这种情况下应该是可以的,因为控制器只处理一件事,它的功能不会在应用程序的另一点使用。

/app/Http/控制器/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/中间件/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-file:

本地测试(任意IP):

INTERNAL_MIDDLEWARE_IP = /^.*$/

生产env:

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

我很抱歉我不能帮助你与RachetPHP,但我认为你得到一个很好的想法,如何解决这个问题。

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

WebSocket-Server通过一个Artisan命令(通过mmochetti@github)启动。我将这些类注入Command:

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

我将这些实例从命令传递给服务器。在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());
}

代码当然可以更干净,但作为一个快速的hack,它是有效的。我认为,而不是使用一个模型的会话,我应该使用Laravel DatabaseSessionHandler .

对于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',
相关文章: