php/socket.io实现扫码登录

来源:互联网 发布:windows视觉样式文件 编辑:程序博客网 时间:2024/04/30 06:59

首先先给大家道个歉,由于上次写东西不认真,自己也没有测试,不仅没能帮到大家,还害的不少人走了弯路,所以原文章代码我删掉了

扫码登陆的原理  我上面的图上已经说的很清楚  实际上就是手机端的token传递到web端的过程  关于token我不多说,这个大家应该都懂

修改过后的代码可以在下面的链接中下载到 代码注释非常清楚  可以直接运行 请先看reedme!


首先是编写服务端

需要的node moudle如下

winston      日志模块   可以用log4替代    做日志记录用 需安装    npm installwinston  下同

express      web服务器兼框架  主要利用里面一些现成的东西

socket.io    长链接的服务模块

request       网络请求  如果你的node server需要与后端web server进行通信 需要这个  选装

 

定义日志记录

var winston = require('winston');var logger = new (winston.Logger)({    transports: [        new (winston.transports.Console)({level: "info", timestamp: true}),//设置日志级别        new (winston.transports.File)({ filename: 'access.log',json: false})//设置日志记录的文件及各式    ]});

编写http服务 

由于手机端没有必要做长链接 一般是以短链接的形式 当然你使用长链接也可以  可以省掉这一段

var http = require('http');//加载url模块 解析get参数 var url = require('url');//加载query模块 解析post参数var query = require("querystring"); //开启http服务var server = http.createServer(function (request,response) {    //获取url访问参数    var pathUrl = url.parse(request.url).pathname;    //我这里不实用rount模块来实现路由 直接使用switch来做一个简单的路由    switch (pathUrl){        //手机端链接成功        case '/connection':            var postdata = '';            //当监测到post数据后 将参数追加到postdata中 如果数据量大 这里可以使用buffer            request.on("data",function(postchunk){                postdata += postchunk;            })            //接受完post数据后            request.on("end",function(){                var data = query.parse(postdata.toString('utf-8'));                if(!isNull(data.uuid)){                    errorHead(response,'没传递uuid参数');                    response.end();                }                replayToDisplayer(data.uuid,{"status":0,"message":"手机端已链接成功"},'/appconnect')                successHead(response,'链接成功');            })            break;        //手机端确认登陆        case '/confrim':            var postdata = '';            request.on("data",function(postchunk){                postdata += postchunk;            })            request.on("end",function(){                var data = query.parse(postdata.toString('utf-8'));                if(!isNull(data.token)){                    errorHead(response,'没传递token参数');                    response.end();                }                if(!isNull(data.uuid)){                    errorHead(response,'没传递uuid参数');                    response.end();                }                replayToDisplayer(data.uuid,{"status":0,"data":{"token":data.token},"message":"手机端已确认登陆"},'/appconfirm')                successHead(response,"手机端已确定登陆",data.token);            })            break;        default:            errorHead(response);            break;    }}).listen(8889, "127.0.0.1");//http服务状态报告server.once('listening', function() {      logger.info('tcp服务开启 监听端口 8889');  });

然后是socket的服务

//定义一个list存放uuid与socket.id的对应关系//同一时间会有多个客户端链接在服务器上  所以要知道手机端到底要将token给哪一个客户端 //当socket链接建立成功后 每一个链接都有一个独一无二的socket.id  服务端会根据socket.id来决定要给哪个客户端发送消息//而客户端链接的时候会提供一个唯一参数uuid //而手机端扫码完成后 就可以解析道这个参数 从而得知到底要响应那一个客户端//我们要做的就是将uuid与socket.id进行绑定 这里比较难懂一点 多看几遍  //可以理解成坐飞机 首先你得有张飞机票(socket.id) 而能上飞机的人都有飞机票  但你总得知道你坐在哪里//这时候你的座位号就有用了(uuid) 通过你的座位号你才知道 你究竟做哪 而根据座位号 可以反着计算出你的票是那张  这里要实现的就是票和座位号的绑定关系<span style="color:#FF0000;">var UUIDMap = {}</span>;//后续请主要关注这个集合的变化/** * 开启socket.io服务  */var request = require('request');var io = require('socket.io').listen(8888);logger.info('socket服务开启,监听端口:', 8888);/** * socket.io事件  连接成功 * @param {string} event名称 * @param {function} 连接成功的回调函数  */io.sockets.on('connection', function (socket) {logger.info('web端链接成功,socket_id为:', socket.id);    var UUID;    //客户端进行uuid与socket.id绑定      socket.on('/register', function(data){          UUID = data['uuid'];         UUIDMap[UUID] = socket.id;          logger.info('web端注册,uuid为', UUID);      });     //客户端断开链接      socket.on('/disconnect', function () {          if (UUID != null) {              logger.info('客户端断开链接,从连接池中删除', "uuid 为"+UUID+",socket.id为"+socket.id);              delete UUIDMap[UUID];          }      }); });

公共函数

/** * http请求成功应答  */function successHead(response,notice,token){    response.writeHead(200,{"Content-Type":"text/plain","Content-Type":"text/html; charset=utf-8"});    var message = {"status":"0","data": isNull(token) ? token : "","message" :isNull(notice) ? notice : "请求成功"};    response.write(JSON.stringify(message));    response.end();}/** * http请求失败应答  */function errorHead(response,notice){    response.writeHead(200,{"Content-Type":"text/plain","Content-Type":"text/html; charset=utf-8"});    var message = {"status":"1","data":"","message":  isNull(notice) ? notice : "请求地址不存在"};    response.write(JSON.stringify(message));    response.end();}/** * 向指定web端发送信息 *  * @param {json} data 要返回的数据 * @param {string} event 要回调客户端的监听事件 * @returns {undefined} */function replayToDisplayer(uuid, data, event) {    var submitUUID = uuid;    var displayerSocket = findSocketByUUID(submitUUID);    if (displayerSocket != null) {        logger.info('根据uuid:'+uuid+"找到socket.id:");        displayerSocket.emit(event, data);    } }/** * 通过uuid查找socket connection id * @param {uuid} data 要返回的数据 */function findSocketByUUID(UUID) {    var targetSocketID = UUIDMap[UUID];    if (targetSocketID != null) {        var targetSocket = io.sockets.connected[targetSocketID];        if (targetSocket != null) {            return targetSocket;        }else{            logger.info('不能根据uuid找到socketid,uuid为', UUID);        }    }    return null;}/**  * 判断是否null  * @param {string} data * @return bool  */function isNull(data){     return (data == "" || data == undefined || data == null) ? false : true; }


php后端

php后端主要用来生成二维码和校验token  校验部分请根据自身业务编写  在node server中使用request模块做一个网络请求发送到php端来验证token

include 'qrcode/phpqrcode.php';//uuid 唯一的标示符 用于指定客户端收发信息$uuid = 'abc123';//生成二维码文件$filename = 'qrcode'.time().mt_rand(1000,9999).'.png';//二维码中包含的数据$data = [    "ip"=>'127.0.0.1',    "port"=>'8888',    'exprise'=>time()+60,    'uuid'=>$uuid];try{   QRcode::png($data,'temp/'.$filename,'L',15);   echo json_encode(['code'=>1,'message'=>'temp/'.$filename,'uuid'=>$uuid]);}catch(\Exception $e) {   echo json_encode(['code'=>0,'message'=>$e->getMessage()]);}


客户端(web)

web端主要加载二维码  然后即等待服务器响应 代码如下

<!Doctype html><html> <head>   <title>扫码登录demo</title>  <meta charset="utf-8"></meta></head>  <body>    <div style='margin:100px auto;width:80%;text-align:center'>        <img src="" class="qrcode" style="display:none;margin:0 auto;"/><br />        <p></p><br />        <button class='button' onclick="getQrcode()" style='font-size:16px'>获取二维码登陆</button>    </div><script type="text/javascript" src="js/socket.io.js"></script><script type="text/javascript" src="js/jquery.min.js"></script><script type="text/javascript">    var timeLimit = 60;    var uuid;    /**     *  获取二维码      */    function getQrcode(){        $.ajax({            type: 'POST',            url: '../php/index.php',            data: {},            dataType: 'json',            success: function(data){                if(data.code === 1){                    $('.qrcode').attr('src','../php/'+data.message);                    $('.qrcode').show();                    $('.button').attr('disabled',true);                    uuid = data.uuid;                    countDown();                    init(data.uuid);                    console.log('生成二维码成功,正在建立链接...');                }else{                    console.log(data.message);                }            }         });    }        /**     *  刷新计时器      */    function countDown(){        var id = setInterval(function (){            var str = '二维码有效期剩余:'+timeLimit+'秒';            $('.button').html(str);            if(timeLimit >0){                timeLimit--;            }else{                timeLimit = 60;                clearInterval(id);                $('.button').html('获取二维码登陆');                $('.button').attr('disabled',false);            }        },1000);    }        /*     *  初始化链接     */    function init(uuid) {        var socket = io.connect('http://127.0.0.1:8888');        //向服务器发送uuid绑定socket.id          socket.emit('/register',{uuid:uuid});         console.log("链接成功");        //手机端扫码成功        socket.on('/appconnect',function(data){              console.log(data);              //后续操作   页面显示扫码成功啦等等        });        //手机端确认登陆        socket.on('/appconfirm',function(data){              //实际上就是要手机的token 扫码登陆实际上就是把手机的token传递到web端上            console.log(data);              //后续操作  比如跳转页面        });    } </script> </body>  </html>


手机端(ios swift)

扫码解析

import UIKitimport AVFoundationclass WKQrCodeViewController: UIViewController,AVCaptureMetadataOutputObjectsDelegate {        fileprivate let sWidth = UIScreen.main.bounds.size.width    fileprivate let sHeight = UIScreen.main.bounds.size.height    fileprivate let maskViewColor = UIColor.black    fileprivate let maskViewAlpha : CGFloat = 0.3        deinit {        print("二维码界面被销毁了")    }                //比例    let scaleWidth : CGFloat = 0.6    var session:AVCaptureSession?    var lineView:UIImageView? = UIImageView.init(imageName: "qrscan_line")    var timer = Timer()        fileprivate var isSent: Bool = false        override func viewWillAppear(_ animated: Bool) {                //即将进入时对状态条进行隐藏        UIApplication.shared.setStatusBarHidden(true, with: UIStatusBarAnimation.none)    }        override func viewWillDisappear(_ animated: Bool) {        self.timer.invalidate()    }      override func viewDidLoad() {                super.viewDidLoad()         //二维码框上的动画计时器        self.timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(configLine), userInfo: nil, repeats: true)        //获取摄像设备,注意是Video而不是Audio        let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)        //初始化AV Session来协调和处理AV的输入和输出流        let session = AVCaptureSession()                //创建输入流        let input:AVCaptureDeviceInput? = try! AVCaptureDeviceInput(device: device)                if session.canAddInput(input){            session.addInput(input)        }                //创建输出流        let output:AVCaptureMetadataOutput = AVCaptureMetadataOutput()        if session.canAddOutput(output){            session.addOutput(output)            //设置输出流代理,从接收端收到的所有元数据都会被传送到delegate方法,所有delegate方法均在queue中执行            output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)            //设置元数据的类型,这里是二维码QRCode            output.metadataObjectTypes = [AVMetadataObjectTypeQRCode]            //            //固定宽度//            let gdWidth : CGFloat = 200//            let scaleWidth : CGFloat = 200 / sWidth                        //比例//            let scaleWidth : CGFloat = 0.6            /*!             这个是手机横着的时候的 x,y,w,h             */                        output.rectOfInterest = CGRect(x : (1 - scaleWidth * sHeight / sWidth) / 2, y : (1 - scaleWidth) / 2,width : scaleWidth * sHeight / sWidth, height :  scaleWidth)            print(output.rectOfInterest)        }                //创建视频设备拍摄视频区域        let layer:AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer.init(session: session)                layer.videoGravity = AVLayerVideoGravityResizeAspectFill                layer.frame = CGRect(x : 0, y : 0, width : UIScreen.main.bounds.size.height,height : UIScreen.main.bounds.size.width);                self.view.layer.addSublayer(layer)            //上        let topView = UIView()        print((sHeight - sWidth * scaleWidth) / 2)        print(sHeight)//320        print(sWidth)//640        topView.frame = CGRect(x:0, y:0, width:sHeight, height:(sWidth -  sHeight * scaleWidth) / 2)        topView.backgroundColor = maskViewColor        topView.alpha = maskViewAlpha                //下        let downView = UIView()        downView.frame = CGRect(x:0, y:sWidth - topView.frame.size.height, width:topView.frame.size.width, height:topView.frame.size.height)        downView.backgroundColor = maskViewColor        downView.alpha = maskViewAlpha                //左        let leftView = UIView()        leftView.frame = CGRect(x:0,y:topView.frame.size.height,width:(sHeight - (sWidth - 2*topView.frame.size.height)) / 2, height:sWidth - 2*topView.frame.size.height)        leftView.backgroundColor = maskViewColor        leftView.alpha = maskViewAlpha        //右        let rightView = UIView()        rightView.frame = CGRect(x:sWidth - 2*topView.frame.size.height + leftView.frame.size.width, y:topView.frame.size.height, width:(sHeight - (sWidth - 2*topView.frame.size.height)) / 2, height:sWidth - 2*topView.frame.size.height)        rightView.backgroundColor = maskViewColor        rightView.alpha = maskViewAlpha                //温馨提示(上)        var tmpview = UIView()        tmpview = tmpview.configOnPrompt(center: rightView.center)        view.addSubview(tmpview)        //温馨提示(下)        var lab = UILabel()        lab = lab.configDownPrompt(frame: leftView.frame)        view.addSubview(lab)                self.view.layer.addSublayer(topView.layer)        self.view.layer.addSublayer(downView.layer)        self.view.layer.addSublayer(leftView.layer)        self.view.layer.addSublayer(rightView.layer)          //线        configLine()                //框        configborder()                //取消        configBack()                //开始采集视频数据        session.startRunning()     }        func configBack() -> Void {                let backButton = UIButton()        backButton.setTitle("取消", for: .normal)        backButton.sizeToFit()        backButton.frame = CGRect(x:sHeight - 37.5, y:15, width:40, height:20)        backButton.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2));                backButton.backgroundColor = UIColor.clear        backButton.addTarget(self, action: #selector(backEvent), for: UIControlEvents.touchUpInside)        view.addSubview(backButton)     }        func backEvent() -> Void {           print("二维码界面的返回被点击")        guard (self.presentingViewController? .isKind(of: WKQrConfirmViewController.classForCoder()))! else {                        self.presentingViewController?.dismiss(animated: true, completion: nil)            return        }                self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)         }         //线(存在问题是图片不能够放在UIImageView上)    func configLine() -> Void {                /*         imageView.contentScaleFactor = [[UIScreen mainScreen] scale];         5         imageView.contentMode = UIViewContentModeScaleAspectFill;         6         imageView.autoresizingMask = UIViewAutoresizingFlexibleHeight;         7         imageView.clipsToBounds = YES;         *///        lineView?.contentScaleFactor = UIScreen.main.scale//        lineView?.autoresizingMask = .flexibleHeight//        lineView?.contentMode = .scaleAspectFill                lineView!.frame = CGRect(x: (sHeight - (sWidth - 2*(sWidth -  sHeight * scaleWidth) / 2)) / 2 + self.sWidth - 2*(self.sWidth - self.sHeight * self.scaleWidth) / 2,y:  (sWidth -  sHeight * scaleWidth) / 2, width: 2, height: (sWidth -  sHeight * scaleWidth) / 2 + 2)        UIView.animate(withDuration: 2) {                        self.lineView!.frame = CGRect(x: (self.sHeight - (self.sWidth - 2*(self.sWidth -  self.sHeight * self.scaleWidth) / 2)) / 2,y: (self.sWidth -  self.sHeight * self.scaleWidth) / 2,width: 2, height: (self.sWidth -  self.sHeight * self.scaleWidth) / 2 + 2)            self.view.addSubview(self.lineView!)                                }            }    func configborder() -> Void {                let qrCodeFrameView = UIImageView(image: UIImage(named: "qrscan_frame"))        qrCodeFrameView.frame = CGRect(x:(sHeight - (sWidth - (sWidth -  sHeight * scaleWidth))) / 2, y:(sWidth -  sHeight * scaleWidth) / 2, width:sWidth - (sWidth -  sHeight * scaleWidth), height:sWidth - (sWidth -  sHeight * scaleWidth))        view.addSubview(qrCodeFrameView)        self.view.layer.addSublayer(qrCodeFrameView.layer);    }        //实现AVCaptureMetadataOutputObjectsDelegate的成员方法来处理二维码信息    @objc(captureOutput:didOutputMetadataObjects:fromConnection:) func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from: AVCaptureConnection!) {        session?.stopRunning()            //获取二维码信息元数据        guard let metadataObject = metadataObjects.first else {            return        }                //让扫描只执行一次        if isSent == true {            return        }        isSent = true                let readableObject = metadataObject as! AVMetadataMachineReadableCodeObject                //添加震动        AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))    // MARK: - 拿到加密串base64 解码 && 反序列化        let decodedData = NSData(base64Encoded            : readableObject.stringValue!, options:.ignoreUnknownCharacters )                let decodedString = String(data: decodedData! as Data, encoding: String.Encoding.utf8)                let UTF8Data = decodedString?.data(using: String.Encoding.utf8)        let oResult = try! JSONSerialization.jsonObject(with: UTF8Data!, options: [JSONSerialization.ReadingOptions.mutableContainers, JSONSerialization.ReadingOptions.mutableLeaves])        print(oResult)        guard let result = oResult as? [String: AnyObject] else {            print("result没解析出来!!")            return        }        let host = result["host"] as! String        let port = String(describing: result["port"])        let uuid = result["uuid"] as! String                let viewModel = WKQrCodeViewModel()                viewModel.qrCodeUpData(host: host, port : port, UUID: uuid, success: {                            let userToken = UserAccountViewModel.sharedUserAccount                let para = ["uuid" : uuid, "token" : userToken.accessToken!] as [String : Any]                print(para)                            viewModel.qrCodeUpDataAgain(para: para as! Dictionary<String, String>, success: {                                  //第二次网络请求成功,触发去除二维码界面,返回跳进刷新界面                    //完成跳转后对二维码界面进行销毁                    self.dismiss(animated: false, completion: nil)                    let confirmVC = WKQrConfirmViewController()                                        self.presentingViewController?.present(confirmVC, animated: true, completion: {                                          })                                    }, failure: { (errMsg) in                    print("打印第二次失败信息:\(errMsg)")                                        let alert = UIAlertController(title: "温馨提示", message: "服务器故障,请取消扫码", preferredStyle: .alert)                    alert.addAction(UIAlertAction(title: "知道了", style: .default){(action)->() in                                                alert.view.isHidden = true                    })                                        alert.view.isHidden = true                    self.present(alert, animated: true, completion: {() -> Void in                                                alert.view.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))                        alert.view.isHidden = false                    })                })                        }) { (errMsg) in                print("打印失败信息\(errMsg)");                let alert = UIAlertController(title: "温馨提示", message: "服务器故障,请取消扫码", preferredStyle: .alert)                alert.addAction(UIAlertAction(title: "知道了", style: .default){(action)->() in                                        alert.view.isHidden = true                })                                alert.view.isHidden = true                self.present(alert, animated: true, completion: {() -> Void in                                        alert.view.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))                    alert.view.isHidden = false                })        }    }        override var shouldAutorotate : Bool {        return false    }    override var supportedInterfaceOrientations : UIInterfaceOrientationMask {        return UIInterfaceOrientationMask.portrait    }        override func didReceiveMemoryWarning() {        super.didReceiveMemoryWarning()        // Dispose of any resources that can be recreated.    }  }//MARK: 二维码界面提示extension UIView {        func configOnPrompt(center:CGPoint) -> UIView {                let promptLab1 = UILabel()        let promptLab2 = UILabel()        let backboard = UIView()                backboard.backgroundColor = UIColor.clear        backboard.bounds = CGRect(x: 0,y: 0,width: 300,height: 40)        backboard.center = center                promptLab1.text = "请使用电脑登陆"        promptLab1.textColor = UIColor.white        promptLab1.font = UIFont.boldSystemFont(ofSize: 15)        promptLab1.textAlignment = .center        promptLab1.backgroundColor = UIColor.clear        promptLab1.numberOfLines = 1        promptLab1.frame = CGRect(x: 0,y: 0,width: 300,height: 15)        promptLab2.text = "www.yiqiweikeshangchuan.com"        promptLab2.textColor = UIColor.white        promptLab2.font = UIFont.boldSystemFont(ofSize: 14)        promptLab2.textAlignment = .center        promptLab2.backgroundColor = UIColor.clear        promptLab2.numberOfLines = 1        promptLab2.frame = CGRect(x: 0,y: 20,width: 300,height: 15)                backboard.addSubview(promptLab1)        backboard.addSubview(promptLab2)                backboard.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))        return backboard    }}extension UILabel {    //温馨提示lable(下)    func configDownPrompt(frame:CGRect) -> UILabel {                let promptLab = UILabel()        promptLab.text = "扫码登陆后进行上传"        promptLab.textColor = UIColor.white        promptLab.font = UIFont.boldSystemFont(ofSize: 15)        promptLab.textAlignment = .center        promptLab.backgroundColor = UIColor.clear        promptLab.numberOfLines = 1        promptLab.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))        promptLab.frame = frame        return promptLab    }}


网络请求

import Foundationclass WKQrCodeViewModel {        var url = "" //不含路径    private let netTool = NetworkTools.sharedTools;        func qrCodeUpData(host : String, port : String, UUID : String, success :  @escaping ()->(), failure : @escaping (_ errMsg : String) -> ()) {                //协议        let url_protocol = "http://"        //路径        let url_host = host;        //端口号(暂用8889)        let url_port = ":8889"        //前缀        let path = "/connection"                url = url_protocol + url_host + url_port                print("第一次当前的url是\(url)")        //url        let urlStr: String = url + path                //参数        let param = ["uuid":UUID]        print("拼接后的网址是\(urlStr),parameterDic是\(param)")                //请求        netTool.request(.POST, URLString: urlStr, parameters: param as [String : AnyObject]?) { (result, error) in                        if error == nil {                guard let result = result as? [String: AnyObject] else {                    return                }                                guard let status = result["status"] as? String else {                    return                }                print(status,result)                                if Int(status) == 0 {                    success()                }else {                                        guard let message = result["message"] as? String else {                        return                    }                    failure(message)                }            } else {                                     failure("网络异常")            }        }    }        //第二次请求    func qrCodeUpDataAgain(para : Dictionary<String, String>, success:@escaping ()->(), failure:@escaping (_ errMsg : String)->()) -> Void {                print(url)        netTool.request(.POST, URLString: url+"/confrim", parameters: para as [String : AnyObject]?) { (result, error) in                            if error == nil {                guard let result = result as? [String: AnyObject] else {                    return                }                            guard let status = result["status"] as? String else {                    return                }                print(status,result)                                if Int(status) == 0 {                                        success()                }else {                    guard let message = result["message"] as? String else {                                                return                    }                    //服务器返回失败消息                    failure(message)                }            }else {                failure("网络异常")            }        }    }}



流程图


运行结果

客户端




模拟手机端的请求



手机端一般是2次请求 

第一次需要告诉web端自己已经成功扫描二维码并解析 

第二次是登陆确认

解析的步骤由手机端实现   这里我只是模拟 所以可以看到我的uuid和token是随便写的


node服务器端收到的信息


node服务器的搭建非常简单  http://blog.csdn.net/zhangsheng_1992/article/details/51322707

所有代码可以在这里找到 https://code.csdn.net/zhangsheng_1992/socket-io/tree/master

0 0
原创粉丝点击