PHPコードスキャンログインの原理と実装方法を共有する

藏色散人
リリース: 2023-04-09 07:08:02
転載
4476 人が閲覧しました

QR コード ログインは、アカウントとパスワードによるログインよりも便利、高速、柔軟であるため、実際に使用されているユーザーの間でより人気があります。

この記事では、QRコードをスキャンしてログインする原理と、QRコードの生成・取得、有効期限・無効化処理、ログイン状況の監視などの全体的な流れを中心に紹介します。

QR コード ログインの原理

全体プロセス

理解を容易にするために、QR コード ログインの一般的なプロセスを説明する UML シーケンス図を簡単に作成しました。

コアプロセスを要約します:

  1. ビジネスサーバーにログイン用の QR コードと UUID を取得するように要求します。

  2. WebSocket を介してソケット サーバーに接続し、定期的にハートビートを送信し (時間間隔はサーバーの構成時間に応じて調整されます)、接続を維持します。

  3. ユーザーは APP を通じて QR コードをスキャンし、ログインを処理するためのリクエストをビジネス サーバーに送信します。 UUIDに基づいてログイン結果を設定します。

  4. ソケット サーバーはモニタリングを通じてログイン結果を取得し、セッション データを確立し、UUID に基づいてログイン データをユーザーのブラウザにプッシュします。

  5. ユーザーは正常にログインし、サーバーは接続プールから Socker 接続を積極的に削除し、QR コードは無効になりました。

クライアントの識別について

これは UUID でもあります。これはプロセス全体にわたるリンクです。これは閉ループのログイン プロセスです。ビジネスの各ステップ処理は UUD に基づいて行われます。 UUID は、session_id またはクライアント IP アドレスに基づいて生成されます。個人的には、より幅広いシナリオに適用できるように、各 QR コードに個別の UUID を持たせることをお勧めします。

フロントエンドとサーバーの通信について

フロントエンドは、ログイン結果と QR コードのステータスを取得するために、サーバーとの通信を継続的に維持する必要があります。インターネット上でいくつかの実装ソリューションを調べたところ、ポーリング、ロング ポーリング、ロング リンク、WebSocket など、基本的にすべてのソリューションが役に立ちます。どちらのソリューションが優れており、どのソリューションが劣っているかを明確に言うことはできませんが、現在のアプリケーション シナリオにより適しているとしか言えません。個人的には、サーバーのパフォーマンスを節約できるロングポーリングと WebSocket の使用をお勧めします。

セキュリティについて

QR コードをスキャンしてログインする利点は明らかで、1 つは人間化、もう 1 つはパスワードの漏洩を防ぐことです。ただし、新しいアクセス方法には新たなリスクが伴うことがよくあります。したがって、プロセス全体に適切な安全機構を追加する必要があります。例:

  • 強制 HTTPS プロトコル
  • 有効期限の短いトークン
  • データ署名
  • データ暗号化

スキャンコードログインプロセスのデモ

コードの実装とソースコードについては後述します。

Socket サーバーを開きます

ログイン ページにアクセスします

ユーザーが要求した QR コード リソースを確認し、qid を取得できます。

QR コードを取得すると、対応するキャッシュが確立され、有効期限が設定されます。

ソケット サーバーに接続され、定期的にハートビートが送信されます。

この時点で、ソケット サーバーには対応する接続​​ログ出力が含まれます:

ユーザーは APP を使用してコードをスキャンし、認証します

サーバーはログインを検証して処理します、セッションを作成し、対応するキャッシュを確立します:

ソケット サーバーはキャッシュを読み取り、情報のプッシュを開始し、排除接続を閉じます:

フロントエンドは情報を取得し、ログインを処理します:

QR コード ログインのスキャンの実装

注: このデモは単なる個人的な学習テストであるため、多くの安全メカニズムはありません。

ソケット プロキシ サーバー

プロキシ ソケット サーバーとして Nginx を使用します。ドメイン名を使用すると、負荷分散を容易にすることができます。このテストのドメイン名: loc.websocket.net

websocker.conf

server {
    listen       80;
    server_name  loc.websocket.net;
    root   /www/websocket;
    index  index.php index.html index.htm;
    #charset koi8-r;

    access_log /dev/null;
    #access_log  /var/log/nginx/nginx.localhost.access.log  main;
    error_log  /var/log/nginx/nginx.websocket.error.log  warn;

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    location / {
        proxy_pass http://php-cli:8095/;
        proxy_http_version 1.1;
        proxy_connect_timeout 4s;
        proxy_read_timeout 60s;
        proxy_send_timeout 12s;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}
ログイン後にコピー

ソケット サーバー

が構築されますPHPソケットサーバーを使用します。実際のプロジェクトでは、安定性が向上するサードパーティ アプリケーションの使用を検討できます。

QRServer.php

_sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("Could not create socket" . PHP_EOL);
        socket_set_option($this->_sock, SOL_SOCKET, SO_REUSEADDR, 1);        // 绑定地址
        socket_bind($this->_sock, \Config::QRSERVER_HOST, \Config::QRSERVER_PROT) or die("Could not bind to socket" . PHP_EOL);        // 监听套接字上的连接
        socket_listen($this->_sock, 4) or die("Could not set up socket listener" . PHP_EOL);

        $this->_redis  = \lib\RedisUtile::getInstance();
    }    /**
     * 启动服务     */
    public function run()
    {
        $this->_clients = array();
        $this->_clients[uniqid()] = $this->_sock;        while (true){
            $changes = $this->_clients;
            $write   = NULL;
            $except  = NULL;
            socket_select($changes,  $write,  $except, NULL);            foreach ($changes as $key => $_sock) {                if($this->_sock == $_sock){ // 判断是不是新接入的 socket

                    if(($newClient = socket_accept($_sock))  === false){
                        die('failed to accept socket: '.socket_strerror($_sock)."\n");
                    }

                    $buffer   = trim(socket_read($newClient, 1024)); // 读取请求
                    $response = $this->handShake($buffer);
                    socket_write($newClient, $response, strlen($response)); // 发送响应
                    socket_getpeername($newClient, $ip); // 获取 ip 地址
                    $qid = $this->getHandQid($buffer);
                    $this->log("new clinet: ". $qid);                    if ($qid) { // 验证是否存在 qid
                        if (isset($this->_clients[$qid])) $this->close($qid, $this->_clients[$qid]);
                        $this->_clients[$qid] = $newClient;
                    } else {
                        $this->close($qid, $newClient);
                    }

                } else {                    // 判断二维码是否过期
                    if ($this->_redis->exists(\lib\Common::getQidKey($key))) {

                        $loginKey = \lib\Common::getQidLoginKey($key);                        if ($this->_redis->exists($loginKey)) { // 判断用户是否扫码
                            $this->send($key, $this->_redis->get($loginKey));
                            $this->close($key, $_sock);
                        }

                        $res = socket_recv($_sock, $buffer,  2048, 0);                        if (false === $res) {
                            $this->close($key, $_sock);
                        } else {
                            $res && $this->log("{$key} clinet msg: " . $this->message($buffer));
                        }
                    } else {
                        $this->close($key, $this->_clients[$key]);
                    }

                }
            }
            sleep(1);
        }
    }    /**
     * 构建响应
     * @param string $buf
     * @return string     */
    private function handShake($buf){
        $buf    = substr($buf,strpos($buf,'Sec-WebSocket-Key:') + 18);
        $key    = trim(substr($buf, 0, strpos($buf,"\r\n")));
        $newKey = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
        $newMessage = "HTTP/1.1 101 Switching Protocols\r\n";
        $newMessage .= "Upgrade: websocket\r\n";
        $newMessage .= "Sec-WebSocket-Version: 13\r\n";
        $newMessage .= "Connection: Upgrade\r\n";
        $newMessage .= "Sec-WebSocket-Accept: " . $newKey . "\r\n\r\n";        return $newMessage;
    }    /**
     * 获取 qid
     * @param string $buf
     * @return mixed|string     */
    private function getHandQid($buf) {
        preg_match("/^[\s\n]?GET\s+\/\?qid\=([a-z0-9]+)\s+HTTP.*/", $buf, $matches);
        $qid = isset($matches[1]) ? $matches[1] : '';        return $qid;
    }    /**
     * 编译发送数据
     * @param string $s
     * @return string     */
    private function frame($s) {
        $a = str_split($s, 125);        if (count($a) == 1) {            return "\x81" . chr(strlen($a[0])) . $a[0];
        }
        $ns = "";        foreach ($a as $o) {
            $ns .= "\x81" . chr(strlen($o)) . $o;
        }        return $ns;
    }    /**
     * 解析接收数据
     * @param resource $buffer
     * @return null|string     */
    private function message($buffer){
        $masks = $data = $decoded = null;
        $len = ord($buffer[1]) & 127;        if ($len === 126)  {
            $masks = substr($buffer, 4, 4);
            $data = substr($buffer, 8);
        } else if ($len === 127)  {
            $masks = substr($buffer, 10, 4);
            $data = substr($buffer, 14);
        } else  {
            $masks = substr($buffer, 2, 4);
            $data = substr($buffer, 6);
        }        for ($index = 0; $index < strlen($data); $index++) {
            $decoded .= $data[$index] ^ $masks[$index % 4];
        }        return $decoded;
    }    /**
     * 发送消息
     * @param string $qid
     * @param string $msg     */
    private function send($qid, $msg)
    {
        $frameMsg = $this->frame($msg);
        socket_write($this->_clients[$qid], $frameMsg, strlen($frameMsg));
        $this->log("{$qid} clinet send: " . $msg);
    }    /**
     * 关闭 socket
     * @param string $qid
     * @param resource $socket     */
    private function close($qid, $socket)
    {
        socket_close($socket);        if (array_key_exists($qid, $this->_clients)) unset($this->_clients[$qid]);
        $this->_redis->del(\lib\Common::getQidKey($qid));
        $this->_redis->del(\lib\Common::getQidLoginKey($qid));
        $this->log("{$qid} clinet close");
    }    /**
     * 日志记录
     * @param string $msg     */
    private function log($msg)
    {
        echo '['. date('Y-m-d H:i:s') .'] ' . $msg . "\n";
    }
}

$server = new QRServer();
$server->run();
ログイン後にコピー

ログインページ




    
    扫码登录 - 测试页面
    
    

登录

扫码登录

二维码已失效
点击重新获取

ログイン後にコピー

ログイン処理

テスト使用、模擬ログイン処理、セキュリティ認証なし! !

setex(\lib\Common::getQidLoginKey($qid), 1800, $data);
ログイン後にコピー

関連情報の詳細については、PHP 中国語 Web サイト をご覧ください。

以上がPHPコードスキャンログインの原理と実装方法を共有するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
php
ソース:cnblogs.com
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!