【问题标题】:Laravel, WebSockets - Verify user on serverLaravel,WebSockets - 在服务器上验证用户
【发布时间】:2015-05-25 13:13:56
【问题描述】:

我目前遇到了使用 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 操作。

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

  • 如果用户通过了身份验证(csrf-token ?)
  • 如果用户是角色的所有者(很容易用其他用户的角色 id 来欺骗请求)

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

【问题讨论】:

  • 你的 Socket-Server (Rachet) 是 Laravel 应用程序的一部分吗?
  • 是的。事实上,它是由 Artisan CLI 命令启动的。

标签: php session laravel eloquent ratchet


【解决方案1】:

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

套接字服务器

socket服务器依赖于文件cookie.jsarray.js,节点模块express、http、socket.io、request和dotenv。我不是 cookie.js 的原作者,但是 cmets 中没有提到作者,所以我无法为此给予任何功劳,对不起。

这是启动服务器的 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);
            });
        }
    });
});

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

SOCKET_SERVER_HOST = localhost
SOCKET_SERVER_PORT = 1337

使用 Laravel 通过会话 cookie 对用户进行身份验证

这是解析 cookie 并响应用户是否可以通过身份验证的 SocketController(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,但我认为您知道如何解决这个问题。

【讨论】:

  • 在授权函数中,如何注入Guard实例?
  • Laravel 自动注入它,因为控制器/方法由 IoC 容器初始化。框架本身通过反射来做到这一点。您可以在the documentation了解更多关于自动解析的信息。
【解决方案2】:

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

WebSocket-Server 由 Artisan Command (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 使用模型。

【讨论】:

  • 这基本上就是我所做的!就我而言,我有一个 nodejs + socket.io 服务器正在运行(我不希望 PHP 处理套接字连接)。每当客户端连接到套接字服务器时,我都会向我的 Laravel 应用程序 (SocketAuthController) 发出请求,该应用程序解析 laravel_session 字符串并验证用户。如果您有兴趣,我可以发布我的解决方案!
  • 请。看看你的方法会很有趣。
  • 当我开始使用 websockets 时,我也偶然发现了这个问题。我没有推出自己的产品并将头撞在墙上,而是使用了推杆。 pusher 有一个开源替代方案:github.com/stevegraham/slanger
【解决方案3】:

对于 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',

【讨论】:

    猜你喜欢
    • 2020-09-08
    • 2017-07-05
    • 1970-01-01
    • 1970-01-01
    • 2014-12-06
    • 2020-06-11
    • 2019-03-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多