QR코드 로그인은 계정 및 비밀번호 로그인보다 더 편리하고 빠르며 유연하기 때문에 실제 사용하는 사용자들 사이에서 더 인기가 높습니다.
본 글에서는 QR코드 생성/획득, 만료 및 무효화 처리, 로그인 상태 모니터링 등 QR코드를 스캔하여 로그인하는 원리와 전반적인 과정을 주로 소개합니다.
이해를 돕기 위해 QR코드 로그인의 일반적인 과정을 간단하게 UML 시퀀스 다이어그램으로 그려보았습니다!
핵심 프로세스 요약:
비즈니스 서버에 로그인을 위한 QR 코드와 UUID를 요청하세요.
웹소켓을 통해 소켓 서버에 접속하고, 정기적으로 하트비트를 보내(시간 간격은 서버 구성 시간에 따라 조정됨) 연결을 유지합니다.
사용자는 앱을 통해 QR 코드를 스캔하고 비즈니스 서버에 로그인 처리 요청을 보냅니다. UUID를 기반으로 로그인 결과를 설정합니다.
소켓 서버는 모니터링을 통해 로그인 결과를 얻어 세션 데이터를 설정하고 UUID를 기반으로 로그인 데이터를 사용자의 브라우저에 푸시합니다.
사용자가 성공적으로 로그인하면 서버가 연결 풀에서 소켓 연결을 적극적으로 제거하고 QR 코드가 유효하지 않게 됩니다.
(UUID라고도 함)는 전체 프로세스, 폐쇄 루프 로그인 프로세스 및 비즈니스 처리의 각 단계에 대한 링크인 UUD를 중심으로 처리됩니다. UUID는 session_id 또는 클라이언트 IP 주소를 기반으로 생성됩니다. 개인적으로 저는 여전히 각 QR 코드에 별도의 UUID가 있으므로 더 넓은 범위의 시나리오에 적용할 수 있는 것을 권장합니다!
프런트엔드는 로그인 결과 및 QR 코드 상태를 얻기 위해 서버와 지속적인 통신을 유지해야 합니다. 인터넷에서 일부 구현 솔루션을 살펴본 후 기본적으로 폴링, 롱 폴링, 롱 링크 및 웹소켓과 같은 모든 솔루션이 유용합니다. 어떤 솔루션이 더 좋고 어떤 솔루션이 더 나쁘다고 확실히 말할 수는 없습니다. 현재 애플리케이션 시나리오에 어떤 솔루션이 더 적합한지는 말할 수 있습니다. 개인적으로는 서버 성능을 절약할 수 있는 롱 폴링과 웹소켓을 사용하는 것을 추천합니다.
QR 코드를 스캔하여 로그인하면 얻을 수 있는 이점은 명백합니다. 하나는 인간화이고, 다른 하나는 비밀번호 유출을 방지하는 것입니다. 그러나 새로운 접근 방법에는 새로운 위험이 수반되는 경우가 많습니다. 따라서 전체 프로세스에 적절한 안전 메커니즘을 추가하는 것이 필요합니다. 예:
코드 구현 및 소스 코드는 나중에 제공됩니다. .
사용자가 요청한 QR 코드 리소스를 확인하고 qid
를 얻을 수 있습니다. qid
。
获取二维码时候会建立相应缓存,并设置过期时间:
之后会连接 socket 服务器,定时发送心跳。
此时 socket 服务器会有相应连接日志输出:
服务器验证并处理登录,创建 session,建立对应的缓存:
Socket 服务器读取到缓存,开始推送信息,并关闭剔除连接:
前端获取信息,处理登录:
注意:本 Demo 只是个人学习测试,所以并未做太多安全机制!
使用 Nginx 作为代理 socke 服务器。可使用域名,方便做负载均衡。本次测试域名:loc.websocket.net
그런 다음 소켓 서버에 연결되어 정기적으로 하트비트를 보냅니다.
이제 소켓 서버에는 해당 연결 로그 출력이 있습니다.서버는 로그인을 확인하고 처리합니다. 세션 생성 및 통신 설정 캐시:
소켓 서버는 캐시를 읽고 정보 푸시를 시작하며 제거 연결을 닫습니다. 프런트 엔드는 정보를 획득하고 로그인을 처리합니다.
Nginx를 프록시 소켓 서버로 사용하세요. 도메인 이름을 사용하여 로드 밸런싱을 용이하게 할 수 있습니다. 이 테스트의 도메인 이름: loc.websocket.net
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 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>
<?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);
위 내용은 PHP 코드 스캐닝 로그인 원리 및 구현 방법 공유의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!