由于扫码登录比账号密码登录更方便、快捷、灵活,在实际使用中更受到用户的欢迎。 本文主要介绍了扫码登录的原理及整体流程,包含了二维码的生成/获取、过期失效的处理、登录状态的监听。 扫码登录的原理整体流程为方便理解,我简单画了一个 UML 时序图,用以描述扫码登录的大致流程! 总结下核心流程: 请求业务服务器获取用以登录的二维码和 UUID。 通过 websocket 连接 socket 服务器,并定时(时间间隔依据服务器配置时间调整)发送心跳保持连接。 用户通过 APP 扫描二维码,发送请求到业务服务器处理登录。根据 UUID 设置登录结果。 socket 服务器通过监听获取登录结果,建立 session 数据,根据 UUID 推送登录数据到用户浏览器。 用户登录成功,服务器主动将该 socker 连接从连接池中剔除,该二维码失效。
关于客户端标识也就是 UUID,这是贯穿整个流程的纽带,一个闭环登录过程,每一步业务处理都是围绕该次的 UUD 进行处理的。UUID 的生成有根据 session_id 的也有根据客户端 ip 地址的。个人还是建议每个二维码都有单独的 UUID,适用场景更广一些! 关于前端和服务器通讯前端肯定是要和服务器保持一直通讯的,用以获取登录结果和二维码状态。看了下网上的一些实现方案,基本各个方案都有用的:轮询、长轮询、长链接、websocket。也不能肯定的说哪个方案好哪个方案不好,只能说哪个方案更适用于当前应用场景。个人比较建议使用长轮询、websocket 这种比较节省服务器性能的方案。 关于安全性扫码登录的好处显而易见,一是人性化,再就是防止密码泄漏。但是新方式的接入,往往也伴随着新的风险。所以,很有必要再整体过程中加入适当的安全机制。例如: 扫码登录的过程演示代码实现和源码后面会给出。 开启 Socket 服务器访问登录页面可以看到用户请求的二维码资源,并获取到了 qid 。 获取二维码时候会建立相应缓存,并设置过期时间: 之后会连接 socket 服务器,定时发送心跳。 此时 socket 服务器会有相应连接日志输出: 用户使用 APP 扫码并授权服务器验证并处理登录,创建 session,建立对应的缓存: Socket 服务器读取到缓存,开始推送信息,并关闭剔除连接: 前端获取信息,处理登录: 扫码登录的实现注意:本 Demo 只是个人学习测试,所以并未做太多安全机制! Socket 代理服务器使用 Nginx 作为代理 socke 服务器。可使用域名,方便做负载均衡。本次测试域名: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 服务器使用 PHP 构建的 socket 服务器。实际项目中大家可以考虑使用第三方应用,稳定性更好一些! 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(); 登录页面
扫码登录 - 测试页面
登录
扫码登录
二维码已失效 点击重新获取
原作者: 互联网
来自: 网络收集
|