ホームページ >バックエンド開発 >PHPチュートリアル >PHPコードスキャンログインの原理と実装方法を共有する

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

藏色散人
藏色散人転載
2020-07-09 14:07:564623ブラウズ

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

<?php

require_once dirname(dirname(__FILE__)) . &#39;/Config.php&#39;;
require_once dirname(dirname(__FILE__)) . &#39;/lib/RedisUtile.php&#39;;
require_once dirname(dirname(__FILE__)) . &#39;/lib/Common.php&#39;;/**
 * 扫码登陆服务端
 * Class QRServer
 * @author BNDong */class QRServer {    private $_sock;    private $_redis;    private $_clients = array();    /**
     * socketServer constructor.     */
    public function __construct()
    {        // 设置 timeout
        set_time_limit(0);        // 创建一个套接字(通讯节点)
        $this->_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(&#39;failed to accept socket: &#39;.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,&#39;Sec-WebSocket-Key:&#39;) + 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] : &#39;&#39;;        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 &#39;[&#39;. date(&#39;Y-m-d H:i:s&#39;) .&#39;] &#39; . $msg . "\n";
    }
}

$server = new QRServer();
$server->run();

ログインページ

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>扫码登录 - 测试页面</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="./public/css/main.css">
</head>
<body translate="no">

<p class=&#39;box&#39;>
    <p class=&#39;box-form&#39;>
        <p class=&#39;box-login-tab&#39;></p>
        <p class=&#39;box-login-title&#39;>
            <p class=&#39;i i-login&#39;></p><h2>登录</h2>
        </p>
        <p class=&#39;box-login&#39;>
            <p class=&#39;fieldset-body&#39; id=&#39;login_form&#39;>
                <button onclick="openLoginInfo();" class=&#39;b b-form i i-more&#39; title=&#39;Mais Informações&#39;></button>
                <p class=&#39;field&#39;>
                    <label for=&#39;user&#39;>用户账户</label>
                    <input type=&#39;text&#39; id=&#39;user&#39; name=&#39;user&#39; title=&#39;Username&#39; placeholder="请输入用户账户/邮箱地址" />
                </p>
                <p class=&#39;field&#39;>
                    <label for=&#39;pass&#39;>用户密码</label>
                    <input type=&#39;password&#39; id=&#39;pass&#39; name=&#39;pass&#39; title=&#39;Password&#39; placeholder="情输入账户密码" />
                </p>
                <label class=&#39;checkbox&#39;>
                    <input type=&#39;checkbox&#39; value=&#39;TRUE&#39; title=&#39;Keep me Signed in&#39; /> 记住我                </label>
                <input type=&#39;submit&#39; id=&#39;do_login&#39; value=&#39;登录&#39; title=&#39;登录&#39; />
            </p>
        </p>
    </p>
    <p class=&#39;box-info&#39;>
        <p><button onclick="closeLoginInfo();" class=&#39;b b-info i i-left&#39; title=&#39;Back to Sign In&#39;></button><h3>扫码登录</h3>
        </p>
        <p class=&#39;line-wh&#39;></p>
        <p style="position: relative;">
            <input type="hidden" id="qid" value="">
            <p id="qrcode-exp">二维码已失效<br>点击重新获取</p>
            <img id="qrcode" src="" />
        </p>
    </p>
</p>
<script src=&#39;./public/js/jquery.min.js&#39;></script>
<script src=&#39;./public/js/modernizr.min.js&#39;></script>
<script id="rendered-js">
    $(document).ready(function () {

        restQRCode();
        openLoginInfo();
        $(&#39;#qrcode-exp&#39;).click(function () {
            restQRCode();
            $(this).hide();
        });
    });    /**
     * 打开二维码     */
    function openLoginInfo() {
        $(document).ready(function () {
            $(&#39;.b-form&#39;).css("opacity", "0.01");
            $(&#39;.box-form&#39;).css("left", "-100px");
            $(&#39;.box-info&#39;).css("right", "-100px");
        });
    }    /**
     * 关闭二维码     */
    function closeLoginInfo() {
        $(document).ready(function () {
            $(&#39;.b-form&#39;).css("opacity", "1");
            $(&#39;.box-form&#39;).css("left", "0px");
            $(&#39;.box-info&#39;).css("right", "-5px");
        });
    }    /**
     * 刷新二维码     */
    var ws, wsTid = null;
    function restQRCode() {

        $.ajax({
            url: &#39;http://localhost/qrcode/code.php&#39;,
            type:&#39;post&#39;,
            dataType: "json",            async: false,
            success:function (result) {
                $(&#39;#qrcode&#39;).attr(&#39;src&#39;, result.img);
                $(&#39;#qid&#39;).val(result.qid);
            }
        });        if ("WebSocket" in window) {            if (typeof ws != &#39;undefined&#39;){
                ws.close();                null != wsTid && window.clearInterval(wsTid);
            }

            ws = new WebSocket("ws://loc.websocket.net?qid=" + $(&#39;#qid&#39;).val());

            ws.onopen = function() {
                console.log(&#39;websocket 已连接上!&#39;);
            };

            ws.onmessage = function(e) {                // todo: 本函数做登录处理,登录判断,创建缓存信息!                console.log(e.data);                var result = JSON.parse(e.data);
                console.log(result);
                alert(&#39;登录成功:&#39; + result.name);
            };

            ws.onclose = function() {
                console.log(&#39;websocket 连接已关闭!&#39;);
                $(&#39;#qrcode-exp&#39;).show();                null != wsTid && window.clearInterval(wsTid);
            };            // 发送心跳
            wsTid = window.setInterval( function () {                if (typeof ws != &#39;undefined&#39;) ws.send(&#39;1&#39;);
            }, 50000 );

        } else {            // todo: 不支持 WebSocket 的,可以使用 js 轮询处理,这里不作该功能实现!
            alert(&#39;您的浏览器不支持 WebSocket!&#39;);
        }
    }</script>
</body>
</html>

ログイン処理

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

<?php

require_once dirname(__FILE__) . &#39;/lib/RedisUtile.php&#39;;
require_once dirname(__FILE__) . &#39;/lib/Common.php&#39;;/**
 * -------  登录逻辑模拟 --------
 * 请根据实际编写登录逻辑并处理安全验证 */$qid = $_GET[&#39;qid&#39;];
$uid = $_GET[&#39;uid&#39;];

$data = array();switch ($uid)
{    case &#39;1&#39;:
        $data[&#39;uid&#39;]  = 1;
        $data[&#39;name&#39;] = &#39;张三&#39;;        break;    case &#39;2&#39;:
        $data[&#39;uid&#39;]  = 2;
        $data[&#39;name&#39;] = &#39;李四&#39;;        break;
}

$data  = json_encode($data);
$redis = \lib\RedisUtile::getInstance();
$redis->setex(\lib\Common::getQidLoginKey($qid), 1800, $data);

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

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

声明:
この記事はcnblogs.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。