Como usar o WebSocket para desenvolver um sistema (2. Adicionar token e verificar a conexão ws)

Quando chamamos a interface no serviço de API tradicional, geralmente usamos Tokeno método para verificar a outra parte. Então, se o usarmos websocketpara desenvolvimento, como carregamos tokens para verificar as identidades uns dos outros?

Como resolver o problema do token
  1. 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')
  2. 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
  3. A solução que escolhi no final é adicionar o token ao cabeçalho do protocolo WebSocket. No WebSocketprotocolo são definidos alguns cabeçalhos padrão, como Sec-WebSocket-Keye Sec-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
  1. Como no desenvolvimento tradicional, primeiro é necessário um método de login, que é gerado após a verificação da senha da conta.Token
  2. 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
  3. HyperfAcabei JWTnão conseguindo, um tanto desconcertante, então simplesmente comecei do PHParmazémEncontrei uma JWTbiblioteca de classes mais usada em
  4. InstalarJWTO comando é:
  5. 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());  
        }  
    }  
}

复制代码
  1. 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
复制代码
  1. 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?
  1. Na verdade, esse problema me incomoda há muito tempo. No websocketprotocolo, existe um conceito chamado "quadro de controle". É lógico que um $frame->opcodebatimento cardíaco pode ser estabelecido enviando um quadro de controle. No entanto, consultei muito de informações e consultado ChatGPT. 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
}
复制代码
  1. 于是我只好退而求其次,使用定时发送PINGPONG的方案,来检测与服务端的连接是否正常
// 此处摘抄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);  
        }  
    }  
}
复制代码
此时,我们已经成功的、完善的建立了客户端与服务端的websocket连接

Acho que você gosta

Origin juejin.im/post/7229520942669791289
Recomendado
Clasificación