Quando chamamos a interface no serviço de API tradicional, geralmente usamos
Token
o método para verificar a outra parte. Então, se o usarmoswebsocket
para desenvolvimento, como carregamos tokens para verificar as identidades uns dos outros?
Como resolver o problema do token
- Meu pensamento inicial era unir um token após a conexão ws, como:
const socket = new WebSocket('wss://example.com/path?token=your_token')
- Minha segunda ideia é carregar o token como parâmetro na mensagem, mas desta forma, o token não pode ser verificado quando a conexão é estabelecida, o que é muito hostil e desperdiça recursos do servidor
- A solução que escolhi no final é adicionar o token ao cabeçalho do protocolo WebSocket. No
WebSocket
protocolo são definidos alguns cabeçalhos padrão, comoSec-WebSocket-Key
eSec-WebSocket-Protocol
, basta colocar o token nele e pode ser utilizado.
// 使用hyperf websocket服务的sec-websocket-protocol协议前
// 需要在config/autoload/server.php中补充配置
[
'name' => 'ws',
'type' => Server::SERVER_WEBSOCKET,
'host' => '0.0.0.0',
'port' => 9502,
'sock_type' => SWOOLE_SOCK_TCP,
'callbacks' => [
Event::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'],
Event::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'],
Event::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'],
],
'settings' => [
Constant::OPTION_OPEN_WEBSOCKET_PROTOCOL => true, // websocket Sec-WebSocket-Protocol 协议
Constant::OPTION_HEARTBEAT_CHECK_INTERVAL => 150,
Constant::OPTION_HEARTBEAT_IDLE_TIME => 300,
],
],
复制代码
Como gerar tokens em PHP
- Como no desenvolvimento tradicional, primeiro é necessário um método de login, que é gerado após a verificação da senha da conta.
Token
- Se este método for usado, devemos ter uma solicitação HTTP para chamar a interface de login e, após obter o token, devemos usar o WS para estabelecer uma conexão
Hyperf
AcabeiJWT
não conseguindo, um tanto desconcertante, então simplesmente comecei do PHParmazémEncontrei umaJWT
biblioteca de classes mais usada em- InstalarJWTO comando é:
- Implemente o método de login, geração de token e métodos de análise de token
// 常用方法 app/Util/functions.php
if (! function_exists('jwtEncode')) {
/**
* 生成令牌.
*/
function jwtEncode(array $extraData): string
{
$time = time();
$payload = [
'iat' => $time,
'nbf' => $time,
'exp' => $time + config('jwt.EXP'),
'data' => $extraData,
];
return JWT::encode($payload, config('jwt.KEY'), 'HS256');
}
}
if (! function_exists('jwtDecode')) {
/**
* 解析令牌.
*/
function jwtDecode(string $token): array
{
$decode = JWT::decode($token, new Key(config('jwt.KEY'), 'HS256'));
return (array) $decode;
}
}
复制代码
// 登录控制器 app/Controller/UserCenter/AuthController.php
<?php
declare(strict_types=1);
namespace App\Controller\UserCenter;
use App\Constants\ErrorCode;
use App\Service\UserCenter\ManagerServiceInterface;
use App\Traits\ApiResponse;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Di\Container;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Hyperf\Validation\Contract\ValidatorFactoryInterface;
class AuthController
{
// HTTP 格式化返回,这部分代码在第7条补充
use ApiResponse;
/**
* @Inject
* @var ValidatorFactoryInterface
*/
protected ValidatorFactoryInterface $validationFactory; // 验证器 这部分代码在第6条补充
/**
* @Inject
* @var ManagerServiceInterface
*/
protected ManagerServiceInterface $service; // 业务代码
/**
* @Inject
* @var Container
*/
private Container $container; // 注入的容器
public function signIn(RequestInterface $request, ResponseInterface $response)
{
$args = $request->post();
$validator = $this->validationFactory->make($args, [
'email' => 'bail|required|email',
'password' => 'required',
]);
if ($validator->fails()) {
$errMes = $validator->errors()->first();
return $this->fail(ErrorCode::PARAMS_INVALID, $errMes);
}
try {
$manager = $this->service->checkPassport($args['email'], $args['password']);
$token = jwtEncode(['uid' => $manager->uid]);
$redis = $this->container->get(\Hyperf\Redis\Redis::class);
$redis->setex(config('jwt.LOGIN_KEY') . $manager->uid, (int) config('jwt.EXP'), $manager->toJson());
return $this->success(compact('token'));
} catch (\Exception $e) {
return $this->fail(ErrorCode::PARAMS_INVALID, $e->getMessage());
}
}
}
复制代码
- No código acima é utilizado o validador, aqui está a instalação e configuração do validador
// 安装组件
composer require hyperf/validation
// 发布配置
php bin/hyperf.php vendor:publish hyperf/translation
php bin/hyperf.php vendor:publish hyperf/validation
复制代码
- No código acima, meu retorno amigável HTTP personalizado é usado, aqui está o código
<?php
declare(strict_types=1);
namespace App\Traits;
use App\Constants\ErrorCode;
use Hyperf\Context\Context;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Utils\Codec\Json;
use Hyperf\Utils\Contracts\Arrayable;
use Hyperf\Utils\Contracts\Jsonable;
use Psr\Http\Message\ResponseInterface;
trait ApiResponse
{
private int $httpCode = 200;
private array $headers = [];
/**
* 设置http返回码
* @param int $code http返回码
* @return $this
*/
final public function setHttpCode(int $code = 200): self
{
$this->httpCode = $code;
return $this;
}
/**
* 成功响应.
* @param mixed $data
*/
public function success($data): ResponseInterface
{
return $this->respond([
'err_no' => ErrorCode::OK,
'err_msg' => ErrorCode::getMessage(ErrorCode::OK),
'result' => $data,
]);
}
/**
* 错误返回.
* @param null|int $err_no 错误业务码
* @param null|string $err_msg 错误信息
* @param array $data 额外返回的数据
*/
public function fail(int $err_no = null, string $err_msg = null, array $data = []): ResponseInterface
{
return $this->setHttpCode($this->httpCode == 200 ? 400 : $this->httpCode)
->respond([
'err_no' => $err_no ?? ErrorCode::SERVER_ERROR,
'err_msg' => $err_msg ?? ErrorCode::getMessage(ErrorCode::SERVER_ERROR),
'result' => $data,
]);
}
/**
* 设置返回头部header值
* @param mixed $value
* @return $this
*/
public function addHttpHeader(string $key, $value): self
{
$this->headers += [$key => $value];
return $this;
}
/**
* 批量设置头部返回.
* @param array $headers header数组:[key1 => value1, key2 => value2]
* @return $this
*/
public function addHttpHeaders(array $headers = []): self
{
$this->headers += $headers;
return $this;
}
/**
* 获取 Response 对象
* @return null|mixed|ResponseInterface
*/
protected function response(): ResponseInterface
{
$response = Context::get(ResponseInterface::class);
foreach ($this->headers as $key => $value) {
$response = $response->withHeader($key, $value);
}
return $response;
}
/**
* @param null|array|Arrayable|Jsonable|string $response
*/
private function respond($response): ResponseInterface
{
if (is_string($response)) {
return $this->response()->withAddedHeader('content-type', 'text/plain')->withBody(new SwooleStream($response));
}
if (is_array($response) || $response instanceof Arrayable) {
return $this->response()
->withAddedHeader('content-type', 'application/json')
->withBody(new SwooleStream(Json::encode($response)));
}
if ($response instanceof Jsonable) {
return $this->response()
->withAddedHeader('content-type', 'application/json')
->withBody(new SwooleStream((string) $response));
}
return $this->response()->withAddedHeader('content-type', 'text/plain')->withBody(new SwooleStream((string) $response));
}
}
复制代码
Como passar token em JS
// 中括号不能省略
const ws = new WebSocket('ws://0.0.0.0:9502/ws/', ['eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2ODExMTg5MjMsIm5iZiI6MTY4MTExODkyMywiZXhwIjoxNjgxMjA1MzIzLCJkYXRhIjp7InVpZCI6MTAwMTR9fQ.k1xHAtpnfSvamAUzP2i3-FZvTnsNDn7I9AmKUWsn1rI']);
复制代码
Middleware para validar tokens
// app/Middleware/TokenAuthenticator.php
<?php
namespace App\Middleware;
use App\Constants\ErrorCode;
use App\Constants\Websocket;
use App\Model\UserCenter\HsmfManager;
use Exception;
use Firebase\JWT\ExpiredException;
use Hyperf\Redis\Redis;
use Hyperf\Utils\ApplicationContext;
use Hyperf\WebSocketServer\Context;
use Swoole\Http\Request;
class TokenAuthenticator
{
public function authenticate(Request $request): string
{
$token = $request->header[Websocket::SecWebsocketProtocol] ?? '';
$redis = ApplicationContext::getContainer()->get(Redis::class);
try {
$tokenData = jwtDecode($token);
if (! isset($tokenData['data'])) {
throw new Exception('', ErrorCode::ILLEGAL_TOKEN);
}
$data = (array) $tokenData['data'];
$identifier = (new HsmfManager())->getJwtIdentifier();
if (! isset($data[$identifier])) {
throw new Exception('', ErrorCode::ILLEGAL_TOKEN);
}
Context::set(Websocket::MANAGER_UID, $data[$identifier]);
$tokenStr = (string) $redis->get(config('jwt.LOGIN_KEY') . $data[$identifier]);
if (empty($tokenStr)) throw new Exception('', ErrorCode::EXPIRED_TOKEN);
return $tokenStr;
}catch (ExpiredException $exception) {
throw new Exception('', ErrorCode::EXPIRED_TOKEN);
}catch (Exception $exception) {
throw new Exception('', ErrorCode::ILLEGAL_TOKEN);
}
}
}
复制代码
Como usar este middleware para verificar o token?
// 此处摘抄app/Controller/WebSocketController.php中的部分代码,详细内容请看昨天的文章
public function onOpen($server, $request): void
{
try {
$token = $this->authenticator->authenticate($request); // 验证令牌
if (empty($token)) {
$this->sender->disconnect($request->fd);
return;
}
$this->onOpenBase($server, $request);
}catch (\Exception $e){
$this->logger->error(sprintf("\r\n [message] %s \r\n [line] %s \r\n [file] %s \r\n [trace] %s", $e->getMessage(), $e->getLine(), $e->getFile(), $e->getTraceAsString()));
$this->send($server, $request->fd, $this->failJson($e->getCode()));
$this->sender->disconnect($request->fd);
return;
}
}
复制代码
Como estabelecer um mecanismo de pulsação entre o cliente e o servidor?
- Na verdade, esse problema me incomoda há muito tempo. No
websocket
protocolo, existe um conceito chamado "quadro de controle". É lógico que um$frame->opcode
batimento cardíaco pode ser estabelecido enviando um quadro de controle. No entanto, consultei muito de informações e consultadoChatGPT
. Depois de fazer muitos testes, descobri que essa estrada não funciona (principalmente porque o front-end não pode ser realizado, mas o back-end pode ser realizado), pode ser que minha capacidade de front-end é insuficiente, e espero que alguns especialistas front-end possam dar dicas.
// 以下是控制帧的值
class Opcode
{
public const CONTINUATION = 0;
public const TEXT = 1;
public const BINARY = 2;
public const CLOSE = 8;
public const PING = 9; // 客户端发送PING
public const PONG = 10; // 服务端发送PONG
}
复制代码
- 于是我只好退而求其次,使用定时发送
PING
与PONG
的方案,来检测与服务端的连接是否正常
// 此处摘抄app/Controller/WebSocketController.php中的部分代码,详细内容请看昨天的文章
public function onMessage($server, $frame): void
{
if ($this->opened) {
if ($frame->data === 'ping') {
$this->send($server, $frame->fd, 'pong');
}else{
$this->onMessageBase($server, $frame);
}
}
}
复制代码