很多网站都多出来了微信二维码扫描登录,微信官方也有提供网站应用微信登录,然后在DAOClOUD看到了他们的微信扫描登录跟其他地方不一样,于是研究了一下,发现是利用生成带参数的二维码扫描带参数二维码事件来实现的。

下面是我的实现过程,用的是Tornado实现的。

用户登录实现

首先是实现用户登录功能,这里我为了偷懒,就没有用实际的用户登录认证,而是只匹配用户名

主要代码如下

首先创造1000个假用户(名)

for i in xrange(1000):
    q.put(i)
    users.append(dict(username=str(i),
                      openid=None))

然后是登录

class IndexHandler(tornado.web.RequestHandler):

    def get(self):
        self.render('index.html')

    def post(self):
        username = self.get_argument('username')

        for user in users:
            if user['username'] == username:
                self.set_cookie('user', username)
                return self.redirect('/user', 302)

        self.redirect('/', 302)

接下来是一个虚假的用户中心,主要是为了证明登录成功了

class UserCenterHandler(tornado.web.RequestHandler):

    def get_current_user(self):

        user = self.get_cookie('user')
        for _user in users:
            if _user['username'] == user:
                return _user
        return user

    @tornado.web.authenticated
    def get(self):
        self.render('usercenter.html', user=self.current_user)

二维码扫描登录

能够进行正常的登录之后,接下来就是二维码的扫描登录了

当用户扫描二维码后,微信会推送扫描带参数二维码事件,而这个事件有两种,一种是已关注扫描,另一种是未关注扫描,但是这两种都会带上 scene_id 和 ticket 两个参数,为了识别到是哪个用户扫描的,所以需要将 scene_id 或者 ticket 与当前打开二维码扫描界面的用户绑定起来,所以我用了一种我觉得很蠢的方法来生成 scene_id

q = Queue.LifoQueue(1000)

我生成了1000个 scene_id 放到一个队列里,当用户打开登录页面时,从队列里获得一个 scene_id,当用户关闭登录页面时,释放 scene_id,并且入队,这样就保证了 scene_id 的复用和不重复,用户登录界面与服务器保持连接我用的是 WebSocket,实现如下

class WebSocketHandler(tornado.websocket.WebSocketHandler):

    @tornado.gen.coroutine
    def open(self):
        print 'open'
        wechat = WeChat(access_token)
        qid = q.get()
        qr = yield wechat.get_ticket(180, qid)
        c = {
            'client': self,
            'qid': qid,
            'ticket': qr['ticket'],
            'uuid': str(uuid.uuid4()),
            'expires': 180,
        }

        clients.append(c)
        d = copy.copy(c)
        del d['qid'], d['client']

        self.write_message(json.dumps(dict(type='qr', data=d)))

    def on_close(self):
        for c in clients:
            if c['client'] == self:
                q.put(c['qid'])
                clients.remove(c)

前端实现如下

var ws = new WebSocket('ws://' + window.location.host + '/ws');
    var ws = new WebSocket('ws://' + window.location.host + '/ws');
$(function() {
    ws.onopen = function() {
        console.info('open');
        var get_qr = {
            type: 'get_qr'
        }

        ws.send(JSON.stringify(get_qr));
    };
    var t;
    ws.onmessage = function (evt) {
        var msg = JSON.parse(evt.data);
        console.log(msg);
        if (msg.type == 'qr') {
            $('#wechat-qr-img').attr('src', 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=' + msg.data.ticket)
            var expires = msg.data.expires;
            function run() {
                console.info(expires);
                expires--;
                if (expires == 0) {
                    clearTimeout(t);
                    $('#wechat-qr-img').attr('src', '');
                    alert('二维码过期');
                    window.location.reload();
                }
                t = setTimeout(function(){run()}, 1000);
            }
            run();
        } else if (msg.type == 'success_and_not_bind') {
            clearTimeout(t);
            $('#wechat-bind').slideDown();
            $('#bind-openid').val(msg.data);
        } else if (msg.type == 'success') {
            $.cookie('user', msg.data);
            window.location.href = '/user';
        }
    };

    $('#wechat-bind-btn').click(function() {
        var username = $('#bind-username').val();
        var openid = $('#bind-openid').val();
        $.post('/user/bind', {openid: openid, username: username})
        .success(function(data) {
            console.info(data);
            if (data.status) {
                window.location.href = '/user';
            }
        })
        .fail(function(data) {
            console.error(data.responseJSON);
        });
    });
});

因为 WebSocket 没法设置 cookies,所以我就把需要设置的 cookies 发到了浏览器,在前端设置 cookies

服务器端接收到微信扫描事件时的处理

if msg.event == 'scan':
    reply = create_reply('扫描', msg)
    for c in clients:
        if c['qid'] == int(msg.scene_id):
            for user in users:
                if user['openid'] == msg.source:
                    c['client'].write_message(dict(type='success', data=user['username']))
                    break
            else:                         c['client'].write_message(dict(type='success_and_not_bind', data=msg.source))

当用户第一次扫描,并没有绑定帐号时,要先绑定帐号

class UserBindHandler(tornado.web.RequestHandler):

    def post(self):
        username = self.get_argument('username')
        openid = self.get_argument('openid')

        for user in users:
            if user['username'] == username:
                user['openid'] = openid
                data = {
                    'status': True
                }
                self.set_cookie('user', username)
                break
        else:
            data = {
                'status': False,
                'err': '用户不存在'
            }
            self.set_status(403)
        self.set_header('content-type', 'application/json')
        for user in users:
            if user['openid']:
                print user
        self.write(json.dumps(data))

结尾

以上只是为了实验利用微信扫描二维码登录的可行性
上述完整代码放在了gist里面,点这里可以看到完整的