Comment utiliser WebSocket pour développer un système (2. Ajouter un jeton et vérifier la connexion ws)

Lorsque nous appelons l'interface dans le service API traditionnel, nous utilisons souvent Tokenla méthode pour vérifier l'autre partie. Donc, si nous websocketl'utilisons pour le développement, comment transportons-nous des jetons pour vérifier l'identité de chacun ?

Comment résoudre le problème des jetons
  1. Ma première pensée était d'épisser un jeton après la connexion ws, par exemple :const socket = new WebSocket('wss://example.com/path?token=your_token')
  2. Ma deuxième idée est de porter le jeton en tant que paramètre dans le message, mais de cette façon, le jeton ne peut pas être vérifié lorsque la connexion est établie, ce qui est très hostile et gaspille les ressources du serveur.
  3. La solution que j'ai finalement choisie est d'ajouter le jeton à l'en-tête du protocole WebSocket. Dans WebSocketle protocole, certains en-têtes standard sont définis, tels que Sec-WebSocket-Keyet Sec-WebSocket-Protocol, j'ai juste besoin d'y mettre le jeton et il peut être utilisé.
// 使用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,  
    ],  
],
复制代码
Comment générer des jetons en PHP
  1. Comme pour le développement traditionnel, une méthode de connexion est d'abord requise, qui est générée après vérification du mot de passe du compte.Token
  2. Si cette méthode est utilisée, nous devons avoir une requête HTTP pour appeler l'interface de connexion, et après avoir obtenu le jeton, nous devons utiliser WS pour établir une connexion
  3. HyperfJ'ai JWTfini par ne pas passer, un peu déconcertant, alors j'ai simplement commencé à partir de PHPentrepôtJWTJ'ai trouvé une bibliothèque de classes la mieux utilisée dans
  4. InstallerJWTLa commande est :
  5. Implémenter la méthode de connexion, la génération de jeton et les méthodes d'analyse de jeton
// 常用方法 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. Dans le code ci-dessus, le validateur est utilisé, voici l'installation et la configuration du validateur
// 安装组件
composer require hyperf/validation
// 发布配置
php bin/hyperf.php vendor:publish hyperf/translation
php bin/hyperf.php vendor:publish hyperf/validation
复制代码
  1. Dans le code ci-dessus, mon retour convivial HTTP personnalisé est utilisé, voici le code
<?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));  
        }  
}
复制代码
Comment passer un jeton dans JS
// 中括号不能省略
const ws = new WebSocket('ws://0.0.0.0:9502/ws/', ['eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2ODExMTg5MjMsIm5iZiI6MTY4MTExODkyMywiZXhwIjoxNjgxMjA1MzIzLCJkYXRhIjp7InVpZCI6MTAwMTR9fQ.k1xHAtpnfSvamAUzP2i3-FZvTnsNDn7I9AmKUWsn1rI']);
复制代码
Intergiciel de validation des jetons
// 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);  
        }  
    }  
}
复制代码
Comment utiliser ce middleware pour vérifier le 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;  
    }  
}
复制代码
Comment établir un mécanisme de heartbeat entre le client et le serveur ?
  1. En fait, ce problème me préoccupe depuis longtemps. Dans websocketle protocole, il existe un concept appelé "trame de contrôle". Il va de soi qu'un $frame->opcodebattement de coeur peut être établi en envoyant une trame de contrôle. Cependant, j'ai beaucoup consulté d'informations et consultées ChatGPT. Après avoir fait beaucoup de tests, j'ai constaté que cette voie ne fonctionnait pas (principalement parce que le front-end ne peut pas être réalisé, mais le back-end peut être réalisé), il se peut que ma capacité de front-end est insuffisant, et j'espère que certains experts du front-end pourront donner des indications.
// 以下是控制帧的值
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连接

Je suppose que tu aimes

Origine juejin.im/post/7229520942669791289
conseillé
Classement