Log Principle and scan code

As the scan code to log more convenient, fast, flexible, more users are welcome in actual use than the account password.

This paper describes the principles and sweep the whole process login code, including a two-dimensional code generation / acquisition, monitoring expired deal, login state.

Principle scan code to log

Overall process

To facilitate understanding, I simply drew a UML sequence diagram to describe the general flow scan code login!

Summary The following core processes:

  1. Requests the service server to obtain two-dimensional code and UUID to login.

  2. Socket server connected via websocket, and timing (time interval adjusted according to the configuration server) transmits the heartbeat remain connected.

  3. APP user scanning two-dimensional code, sends a request to the service server logon. Set to log results based UUID.

  4. socket server get by listening to log results, the establishment of session data and push data to log into the user's browser according to UUID.

  5. User login is successful, the server actively socker removed from the connection pool, the two-dimensional code fails.

About Client Identity

That is UUID, which is run through the entire process of link, a closed-loop login process, every step of the business process are handled around the times of UUD. UUID is generated according to the session_id have also ip address based on the client. It is recommended that each individual has a separate two-dimensional code UUID, applicable scene is broader!

On front-end and server communication

Front-end server and is sure to keep the communication has to obtain two-dimensional code to log the results and status. Read some implementations under the Internet, various programs have basically used: polling, long polling, long link, websocket. We can not say for sure what a good program which program is not good, can only say which program is more suitable for the current application scenario. Personally recommend the use of long-polling, websocket this comparison save server performance solution.

About Security

The obvious benefit of scan code login, one human, then there are passwords to prevent leakage. But new ways of access, often accompanied by new risks. Therefore, appropriate security mechanism and then the whole process is necessary to add. E.g:

  • Forced HTTPS protocol
  • Short-lived token
  • Data signature
  • data encryption

Login procedure demonstrates scan code

And source code implementation will be given later.

Socket server open

Visit the login page

We can see the two-dimensional code resources requested by the user, and to obtain qid.

When will obtain two-dimensional code to establish the corresponding cache, and set an expiration time:

After connect socket server, regularly send heartbeat?.

In this case there will be a corresponding connector socket server log output:

APP scan code and user authorization

Server authentication and logon, create session, create a corresponding cache:

Socket server reads to the cache, information began to push and close excluding connection:

Front-end access to information, logon:

Login code to achieve sweep

Note: This Demo only individual learning test, so I did not do too much security!

Socket Proxy Server

Socke using Nginx as a proxy server. Domain name can be used to facilitate load balancing. The test domain: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;
    }
}

Socket Server

Socket server using PHP build. The actual project we can consider using third-party applications, better stability of some!

QRServer.php

<?php

require_once dirname(dirname(__FILE__)) . '/Config.php';
require_once dirname(dirname(__FILE__)) . '/lib/RedisUtile.php';
require_once dirname(dirname(__FILE__)) . '/lib/Common.php';

/**
 * 扫码登陆服务端
 * 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('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();

log in page

<!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">

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

        restQRCode();
        openLoginInfo();
        $('#qrcode-exp').click(function () {
            restQRCode();
            $(this).hide();
        });
    });

    /**
     * 打开二维码
     */
    function openLoginInfo() {
        $(document).ready(function () {
            $('.b-form').css("opacity", "0.01");
            $('.box-form').css("left", "-100px");
            $('.box-info').css("right", "-100px");
        });
    }

    /**
     * 关闭二维码
     */
    function closeLoginInfo() {
        $(document).ready(function () {
            $('.b-form').css("opacity", "1");
            $('.box-form').css("left", "0px");
            $('.box-info').css("right", "-5px");
        });
    }

    /**
     * 刷新二维码
     */
    var ws, wsTid = null;
    function restQRCode() {

        $.ajax({
            url: 'http://localhost/qrcode/code.php',
            type:'post',
            dataType: "json",
            async: false,
            success:function (result) {
                $('#qrcode').attr('src', result.img);
                $('#qid').val(result.qid);
            }
        });

        if ("WebSocket" in window) {
            if (typeof ws != 'undefined'){
                ws.close();
                null != wsTid && window.clearInterval(wsTid);
            }

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

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

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

            ws.onclose = function() {
                console.log('websocket 连接已关闭!');
                $('#qrcode-exp').show();
                null != wsTid && window.clearInterval(wsTid);
            };

            // 发送心跳
            wsTid = window.setInterval( function () {
                if (typeof ws != 'undefined') ws.send('1');
            }, 50000 );

        } else {

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

Login processing

Use test, simulate the login process, without making safety certification! !

<?php

require_once dirname(__FILE__) . '/lib/RedisUtile.php';
require_once dirname(__FILE__) . '/lib/Common.php';

/**
 * -------  登录逻辑模拟 --------
 * 请根据实际编写登录逻辑并处理安全验证
 */

$qid = $_GET['qid'];
$uid = $_GET['uid'];

$data = array();
switch ($uid)
{
    case '1':
        $data['uid']  = 1;
        $data['name'] = '张三';
        break;

    case '2':
        $data['uid']  = 2;
        $data['name'] = '李四';
        break;
}

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

Kansei Demo Minamoto码: BNDong / Demo / ScanCodeLogin

Guess you like

Origin www.cnblogs.com/bndong/p/12607579.html
Recommended