QR コード ログインは、アカウントとパスワードによるログインよりも便利、高速、柔軟であるため、実際に使用されているユーザーの間でより人気があります。
この記事では、QRコードをスキャンしてログインする原理と、QRコードの生成・取得、有効期限・無効化処理、ログイン状況の監視などの全体的な流れを中心に紹介します。
理解を容易にするために、QR コード ログインの一般的なプロセスを説明する UML シーケンス図を簡単に作成しました。
コアプロセスを要約します:
ビジネスサーバーにログイン用の QR コードと UUID を取得するように要求します。
WebSocket を介してソケット サーバーに接続し、定期的にハートビートを送信し (時間間隔はサーバーの構成時間に応じて調整されます)、接続を維持します。
ユーザーは APP を通じて QR コードをスキャンし、ログインを処理するためのリクエストをビジネス サーバーに送信します。 UUIDに基づいてログイン結果を設定します。
ソケット サーバーはモニタリングを通じてログイン結果を取得し、セッション データを確立し、UUID に基づいてログイン データをユーザーのブラウザにプッシュします。
ユーザーは正常にログインし、サーバーは接続プールから Socker 接続を積極的に削除し、QR コードは無効になりました。
これは UUID でもあります。これはプロセス全体にわたるリンクです。これは閉ループのログイン プロセスです。ビジネスの各ステップ処理は UUD に基づいて行われます。 UUID は、session_id またはクライアント IP アドレスに基づいて生成されます。個人的には、より幅広いシナリオに適用できるように、各 QR コードに個別の UUID を持たせることをお勧めします。
フロントエンドは、ログイン結果と QR コードのステータスを取得するために、サーバーとの通信を継続的に維持する必要があります。インターネット上でいくつかの実装ソリューションを調べたところ、ポーリング、ロング ポーリング、ロング リンク、WebSocket など、基本的にすべてのソリューションが役に立ちます。どちらのソリューションが優れており、どのソリューションが劣っているかを明確に言うことはできませんが、現在のアプリケーション シナリオにより適しているとしか言えません。個人的には、サーバーのパフォーマンスを節約できるロングポーリングと WebSocket の使用をお勧めします。
QR コードをスキャンしてログインする利点は明らかで、1 つは人間化、もう 1 つはパスワードの漏洩を防ぐことです。ただし、新しいアクセス方法には新たなリスクが伴うことがよくあります。したがって、プロセス全体に適切な安全機構を追加する必要があります。例:
コードの実装とソースコードについては後述します。
ユーザーが要求した QR コード リソースを確認し、qid
を取得できます。
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 サイトの他の関連記事を参照してください。