Laravel、WebSockets - 在服务器上验证用户
Laravel, WebSockets - Verify user on server
我目前 运行 使用 Laravel 和 RachetPHP[来处理服务器上用户的身份验证问题。
到目前为止我尝试了什么:
我将会话的驱动程序类型更改为数据库,给了我一个 id 和 payload 列。使用 \Session::getId()
returns 一个 40 个字符的字符串。
WebSocket-Connection 发送的 cookie 信息确实包含一个 XSRF-TOKEN 和一个 laravel_session,两者都包含 > 200 个字符的字符串。用户会话的数据库 ID 与 \Session::getId()
.
返回的 ID 不同
我已经通过 websocket 消息发送当前的 CSRF 令牌,但我不知道如何验证它(内置验证器使用请求 - 我在websocket 服务器范围)。
通用用例:
一位用户在话题中发表评论。发送对象的有效负载将是:
- 验证用户的东西(ID 或令牌)。
- 评论本身
如果您要发送用户 ID,任何人都可以修改数据包并以其他用户身份发送消息。
我的用例:
一个用户可以有 n 个字符。角色有头像、id、名字等。
该用户仅用于:
- 在服务器上进行身份验证。
- 访问他的角色,从而对他的角色执行基本的 CRUD 操作。
我还有一个table locations - 一个"virtual place",一个角色可以在...所以我得到了一对一角色和位置之间的关系。然后用户(角色)可以通过 websocket 在某个位置发送消息。消息被插入到服务器上的数据库中。此时,我需要知道:
- 如果用户通过身份验证 (csrf-token ?)
- 如果用户是角色的拥有者(用其他用户的角色id来欺骗请求很简单)
如果您需要更多信息,请告诉我。
我想我找到了解决办法。虽然不是很干净,但它做了它应该做的(我猜...)
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_HOST
和 SOCKET_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',
我目前 运行 使用 Laravel 和 RachetPHP[来处理服务器上用户的身份验证问题。
到目前为止我尝试了什么:
我将会话的驱动程序类型更改为数据库,给了我一个 id 和 payload 列。使用
\Session::getId()
returns 一个 40 个字符的字符串。 WebSocket-Connection 发送的 cookie 信息确实包含一个 XSRF-TOKEN 和一个 laravel_session,两者都包含 > 200 个字符的字符串。用户会话的数据库 ID 与\Session::getId()
. 返回的 ID 不同
我已经通过 websocket 消息发送当前的 CSRF 令牌,但我不知道如何验证它(内置验证器使用请求 - 我在websocket 服务器范围)。
通用用例:
一位用户在话题中发表评论。发送对象的有效负载将是:
- 验证用户的东西(ID 或令牌)。
- 评论本身
如果您要发送用户 ID,任何人都可以修改数据包并以其他用户身份发送消息。
我的用例:
一个用户可以有 n 个字符。角色有头像、id、名字等。 该用户仅用于:
- 在服务器上进行身份验证。
- 访问他的角色,从而对他的角色执行基本的 CRUD 操作。
我还有一个table locations - 一个"virtual place",一个角色可以在...所以我得到了一对一角色和位置之间的关系。然后用户(角色)可以通过 websocket 在某个位置发送消息。消息被插入到服务器上的数据库中。此时,我需要知道:
- 如果用户通过身份验证 (csrf-token ?)
- 如果用户是角色的拥有者(用其他用户的角色id来欺骗请求很简单)
如果您需要更多信息,请告诉我。
我想我找到了解决办法。虽然不是很干净,但它做了它应该做的(我猜...)
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
.
这就是我刚才解决这个问题的方法。在我的示例中,我正在使用 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_HOST
和 SOCKET_SERVER_PORT
添加到我的 Laravel .env 文件中,以便能够在不编辑代码和 运行 服务器的情况下更改主机和端口在不同的环境中。
SOCKET_SERVER_HOST = localhost
SOCKET_SERVER_PORT = 1337
使用 Laravel
通过会话 cookie 验证用户这是 SocketController,它解析 cookie 并响应用户是否可以通过身份验证(JSON 响应)。它与您在
/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',