我目前遇到了在服务器上处理用户身份验证的问题,使用Laravel和RachetPHP。
我试过了:
-
我将会话的驱动程序类型更改为数据库,给我一个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_HOST
和SOCKET_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',