Étant donné que la connexion par code QR est plus pratique, plus rapide et plus flexible que la connexion par compte et mot de passe, elle est plus populaire parmi les utilisateurs réels.
Cet article présente principalement le principe et le processus global de numérisation du code QR pour se connecter, y compris la génération/obtention du code QR, le traitement de l'expiration et de l'invalidation et la surveillance de l'état de connexion.
Pour faciliter la compréhension, j'ai simplement dessiné un diagramme de séquence UML pour décrire le processus général de connexion par code QR !
Résumez le processus principal :
Demandez au serveur d'entreprise d'obtenir le code QR et l'UUID pour la connexion.
Connectez-vous au serveur socket via websocket, et envoyez régulièrement des battements de cœur (l'intervalle de temps est ajusté en fonction de l'heure de configuration du serveur) pour maintenir la connexion.
L'utilisateur scanne le code QR via l'APP et envoie une demande au serveur de l'entreprise pour traiter la connexion. Définissez les résultats de connexion en fonction de l'UUID.
Le serveur socket obtient le résultat de connexion via la surveillance, établit les données de session et transmet les données de connexion au navigateur de l'utilisateur en fonction de l'UUID.
L'utilisateur s'est connecté avec succès, le serveur a activement supprimé la connexion socker du pool de connexions et le code QR est devenu invalide.
est l'UUID. Il s'agit du lien tout au long du processus. Il s'agit d'un processus de connexion en boucle fermée. basé sur l'UUD traitée. L'UUID est généré en fonction de l'identifiant de session ou de l'adresse IP du client. Personnellement, je recommande toujours que chaque code QR ait un UUID distinct, applicable à un plus large éventail de scénarios !
Le front-end doit maintenir une communication constante avec le serveur pour obtenir les résultats de connexion et l'état du code QR. Après avoir examiné quelques solutions d'implémentation sur Internet, pratiquement toutes les solutions sont utiles : sondages, sondages longs, liens longs et websockets. Nous ne pouvons pas dire avec certitude quelle solution est la meilleure et laquelle est la pire. Nous pouvons seulement dire quelle solution est la plus adaptée au scénario d'application actuel. Personnellement, je recommande d'utiliser des interrogations longues et un websocket, qui permettent d'économiser les performances du serveur.
Les avantages de scanner le code QR pour se connecter sont évidents, l'un est l'humanisation et l'autre est d'empêcher la fuite de mot de passe. Cependant, les nouvelles méthodes d’accès s’accompagnent souvent de nouveaux risques. Il est donc nécessaire d’ajouter des mécanismes de sécurité appropriés au processus global. Par exemple :
L'implémentation du code et le code source seront donné plus tard.
Vous pouvez voir la ressource de code QR demandée par l'utilisateur et l'obtenir qid
.
Lors de l'obtention du QR code, le cache correspondant sera établi et le délai d'expiration sera fixé :
Ensuite, le serveur socket sera connecté et des battements de coeur seront envoyés régulièrement.
À ce moment-là, le serveur socket aura une sortie de journal de connexion correspondante :
Le serveur vérifie et traite la connexion , crée la session et établit le cache correspondant :
Le serveur Socket lit le cache, commence à pousser les informations et ferme la connexion d'élimination :
Le front-end obtient les informations et traite la connexion :
Remarque : cette démo n'est qu'un test d'apprentissage personnel, elle n'a donc pas beaucoup de mécanismes de sécurité !
Utilisez Nginx comme serveur proxy socket. Les noms de domaine peuvent être utilisés pour faciliter l'équilibrage de charge. Nom de domaine pour ce test : 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; } }
Serveur de socket construit en PHP. Dans les projets réels, vous pouvez envisager d’utiliser des applications tierces, qui offriront une meilleure stabilité !
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();
<!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='box'> <p class='box-form'> <p class='box-login-tab'></p> <p class='box-login-title'> <p class='i i-login'></p><h2>登录</h2> </p> <p class='box-login'> <p 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='登录' /> </p> </p> </p> <p class='box-info'> <p><button onclick="closeLoginInfo();" class='b b-info i i-left' title='Back to Sign In'></button><h3>扫码登录</h3> </p> <p class='line-wh'></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='./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>
Test d'utilisation, traitement de connexion simulé, pas d'authentification de sécurité ! !
<?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);
Pour plus de connaissances connexes, veuillez visiter le Site Web PHP chinois !
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!