Context

前天睡醒之前,隐隐约约的梦到了扫码登录的实现方案,虽然这个扫码登录已经有很多很多很多人实现过了,而且说不定还有了框架(我没有搜索过),但是既然我在梦里想过了一遍,那还是不要辜负自己,把它做出来看看吧~

Requirements

扫码登录:网页上显示个二维码,用登录过的app扫描一下,然后网页上就显示登陆成功

所以需要: 网页一个、app一个、服务器一个

网页用了jqueryjquery-qrcode两个框架.以前做艾米奇定位鞋的时候,用到了很多的二维码,当时把二维码图片文件存在了服务器的本地,我觉得比较难过,因为这次我没有服务器,所有的一切都在我的小电脑中,所以这次就打算让客户端自己去根据字符串生成QRCode了

服务器依然是Django,用了一下Celery(我假装做了一个发送短信验证码的异步操作),Redis(Celery需要的).只配了nginx,没有配uwsgi,因为我想动态的看看服务器运行起来的一些log,用了uwsgi我就不方便调试了~

App嘛,必然又是iOS,我做得快一些~,用了几个很基础的框架,AFNetworking、IQKeyBoardManager

Solution in Dream

在梦里,我是打算在服务器中建立QRCode和一个session的关系,然后App登录过的Session可以将QRCode对应的Session也变为已登陆状态,然后网页端的Session就变成了已登录状态,然后就扫码登录成功了.

For Security

在整套工作过程中,哪些部分可能会出现安全风险呢?

要分析这个问题,可能需要先整理一下,什么情况下,网页端可以登陆成功.下面是根据功能得到的最显而易见的一组条件

1. 网页端有QRCode
2. App扫描了QRCode,并且同意登陆

这意思是,网页问服务器要一个QRCode,然后在那儿一个劲的问服务器,我这个QRCode通过登录了嘛?

App扫描了QRCode,告诉服务器,我是XXX,我扫描了这个QRCode,我要登录

然后服务器就告诉了网页端,你登录了

这样的做法,我觉得大概也是可行的,只不过QRCode这东西可能攻击者伪造一下,就撞到别的已经通过的QRCode,然后他就幸运的登录成功了呢!

所以我想,应该还是要多搞一些规则才行~

然后就建立了这样一个模型

class LoginQRCode(models.Model):
    # 显示的二维码
    code = models.CharField(max_length=255)
    # 传递参数时必备参数
    token = models.UUIDField()
    # session中对应的uuid
    session_token = models.UUIDField()
    # 创建时间(更新时间)
    timestamp = models.DateTimeField()
    # 登陆后记录一下这个二维码对应的用户
    user_id = models.UUIDField(blank=True, null=True)
    # 是否通过
    status = models.BooleanField(default=False)

    def get_fetch_qrcode_response(self):
        return {"code": self.code, "token": str(self.token), "timestamp":get_update_time(self.timestamp)}

在获取QRCode的时候

def ask_for_login_qrcode(request):
    if "login" in request.session:
        if request.session["login"] is True:
            return JsonResponse({"msg": "already login", "status": 1}, status=200)
    if "session_uuid" in request.session:
        old_uuid = request.session["session_uuid"]
        old_qrcode_record = fetch_qrcode_record_with_session(old_uuid)
        if qrcode_record_is_expired(old_qrcode_record) is True:
            record = generate_new_qrcode_record_for_request(request)
            return JsonResponse({"msg": "succeed", "status": 0, "data": record.get_fetch_qrcode_response()}, status=200)
        else:
            return JsonResponse({"msg": "succeed", "status": 0, "data": old_qrcode_record.get_fetch_qrcode_response()},
                                status=200)
    else:
        record = generate_new_qrcode_record_for_request(request)
        return JsonResponse({"msg": "succeed", "status": 0, "data": record.get_fetch_qrcode_response()}, status=200)

意思差不多就是,一个session会产生一个token,一个code,一个session_token,其中session_token会对应记录在session中,以便服务器根据session来判断某个token是否是该会话产生的,(也算是防了一下跨站攻击?),app根据扫描二维码,得到code,将code作为参数告诉服务器,我要这个code登录,然后服务器对这个qrcode纪录进行授权,之后检查到这个session的时候,将这个session标记为已登录,整个流程就走完了~

所以处理App扫码之后做的请求是

@csrf_exempt
@require_POST
@pass_auth
@require_parameter(["code"])
def allow_the_qrcode_login(request):
    code = request.POST["code"]
    user = get_user_from_response_session(request)
    qr_record = fetch_qrcode_record_with_code(code)
    if user is not None and qr_record is not None:
        if qr_record.user_id is not None:
            return JsonResponse({"msg": "expired", "status": -1}, status=400)
        qr_record.user_id = user.user_uuid
        qr_record.status = True
        qr_record.save()
        return JsonResponse({"msg": "succeed", "status": 0}, status=200)
    return JsonResponse({"msg": "code not existed", "status": -400}, status=400)

这里用了两个自己写的修饰器用了确定session是登录过的,并且包含了参数”code”~

当然还有很多个工具方法,看名字大概也知道他是什么意思吧~

最后是刷新登录状态的接口

@csrf_exempt
@require_POST
@require_parameter(["token"])
def checking_login_status(request):
    token_uuid = request.POST["token"]
    qrcode_entity = fetch_qrcode_record_with_token(token_uuid)
    if qrcode_entity is None:
        return JsonResponse({"msg": "bad request", "status": -403}, status=400)
    if "login" in request.session:
        if request.session["login"] is True:
            if request.session["session_uuid"] == qrcode_entity.session_token:
                return JsonResponse({"msg": "pass", "status": 0}, status=200)
            else:
                return JsonResponse({"msg": "token error", "status": -1}, status=403)
        elif qrcode_entity.status is True and request.session["session_uuid"] == str(qrcode_entity.session_token):
            request.session["login"] = True
            request.session["user_id"] = str(qrcode_entity.user_id)
            return JsonResponse({"msg": "pass", "status": 0}, status=200)
        elif qrcode_record_is_expired(qrcode_entity):
            return JsonResponse({"msg": "code is expired,please refresh it", "status": -1}, status=200)
    return JsonResponse({"msg": "waiting", "status": 1}, status=200)

这段我也懒得解释了,反正要改了……

Have a break

这一篇先写这么多,下一篇会讲扫码App的故事(网页端会在很后面讲,因为这个版本网页端和服务端存在一个轮训操作,这个操作效率很低下,我打算在后面加入了websocket之后,再来一起讲网页端~)