Python——奇怪的掃碼登入

小魚周凌宇發表於2018-05-27

最近在做 Sparrow(還在內測的一個敲好用 Mock 系統?)的時候遇到了一個需求。Sparrow 伺服器是使用 Django 2.0 編寫的產品,所以本文所有的程式碼背景均為 Django 2.0 環境和 Python 3.6.3 語言,整體是 Vue + Django + SQLite。

Sparrow 的操作一般都是在網頁上操作,而手機客戶端往往是用來同步一些簡單資料的。那麼這裡遇到一個和平常 APP 不同的使用場景。

一般來說,一個產品的操作大多是在手機上,那麼 PC 客戶端和網頁版就可以通過已經登入的移動端 APP 掃碼登入。

而現在的情況是,Sparrow 的使用大多在網頁版,那麼,我需要的就是,讓移動 APP 使用者在網頁版已經登入的情況下免去輸入使用者名稱、密碼的登入操作,讓移動 APP 使用者掃描網頁二維碼,完成移動 APP 的登入。

大致的 Use Case

Python——奇怪的掃碼登入

##設計思路

掃碼登入 URL

首先能想到的是,伺服器要提供給移動 APP 可以訪問的 URL(展現成二維碼給 APP 掃描),這個 URL 需要包括

  1. user 的唯一標誌符
  2. 驗證碼

那麼其 URL 的大致模樣就是:

frontend/account/quick_login?user_id=<user_id>&verification_code=<verification_code>
複製程式碼

驗證碼

驗證碼是從哪裡來的?

原因是這樣的,如果掃碼登入的 URL 永久有效,顯然是不合理的,這意味著只要得到了這個 URL,任何人都可以通過這個 URL 隨時登入該使用者的賬號,所以需要有驗證碼。

同時,驗證碼需要附帶生成時間,以此來達到驗證碼一分鐘有效的 Feature。為此,設計一個外來鍵為 user 的 Model:

class QuickLoginRecord(models.Model, Dictable):
    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    create_time = models.DateTimeField(auto_now_add=True)
    update_time = models.DateTimeField(auto_now=True)
    verification_code = models.CharField(max_length=32, null=True, default='')
複製程式碼

通過 Django 生成的對應資料庫為:

Python——奇怪的掃碼登入

什麼時候生成驗證碼,那當然是生成二維碼的時候,所以,這個 URL 不是給移動端請求的,而是給前端來請求的,前端在已登入的情況下,訪問該 URL 可以直接傳遞 user 資訊,後端通過拿到 user 資訊,生成一條 QuickLoginRecord 記錄。

前端訪問並拿到驗證碼的 URL 的大致模樣是:

frontend/account/request_quick_login
複製程式碼

那麼整個流程就是(省略了細節處理):

Python——奇怪的掃碼登入

後端程式碼

url.py

urlpatterns = [
	// ···
	path('frontend/account/quick_login', AccountAction.quick_login),
    path('frontend/account/request_quick_login', AccountAction.request_quick_login),
]
複製程式碼

models.py

class QuickLoginRecord(models.Model, Dictable):
    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    create_time = models.DateTimeField(auto_now_add=True)
    update_time = models.DateTimeField(auto_now=True)
    verification_code = models.CharField(max_length=32, null=True, default='')
複製程式碼

account_action.py

這裡程式碼不想看的話,大概描述一下過程:

def request_quick_login(request: HttpRequest)

  1. 規定 HTTPMethod 必須為 GET 訪問
  2. 獲取 request 中的 user_id 引數
  3. 通過 user_id 查詢 QuickLoginRecord 記錄
  4. 如果未查詢到結果,新建一個 QuickLoginRecord 記錄,設定 user 關聯、verification_code(create_time 和 update_time 在 QuickLoginRecordDao 中自動設定)
  5. 如果查詢到結果,更新 verification_code 欄位( update_time 在 QuickLoginRecordDao 中自動設定)
  6. 返回 Success
    @track(AccountRequestQuickLogin)
    def request_quick_login(request: HttpRequest):
        if request.method != CommonData.Method.GET.value:
            return HttpResponse(Response.methodInvalidResponse().toJson(), content_type='application/json')
        user = request.user
        r = QuickLoginRecordDao.get_record_with_user_id(user.id)
        if r is not None:
            r.verification_code = str(uuid.uuid1())
            QuickLoginRecordDao.update_record(r)
            response = Response(Success, 'Success', {'verification_code': r.verification_code})
            return HttpResponse(response.toJson(), content_type='application/json')
        else:
            record = QuickLoginRecord()
            record.user = user
            record.verification_code = str(uuid.uuid1())
            QuickLoginRecordDao.add_record(record)
            response = Response(Success, 'Success', {'verification_code': record.verification_code})
            return HttpResponse(response.toJson(), content_type='application/json')
複製程式碼

def quick_login(request: HttpRequest):

  1. 規定 HTTPMethod 必須為 GET 訪問
  2. 拿到 reqeust 中的 user_id
  3. 拿到 reqeust 中的 verification_code
  4. 通過 verification_code 獲取 QuickLoginRecord 記錄
  5. 如果記錄不存在則表示驗證碼不存在或過期
  6. 如果存在,比較 update_time 欄位,判斷是否已經超過 60 秒
  7. 超過 60 秒返回『驗證碼過期』
  8. 未超過 60 秒,讓使用者登入
  9. 返回 Success
	@track(AccountQuickLogin)
    def quick_login(request: HttpRequest):
        if request.method != CommonData.Method.GET.value:
            return HttpResponse(Response.methodInvalidResponse().toJson(), content_type='application/json')
        user_id = request.GET.get('user_id')
        verification_code = request.GET.get('verification_code')

        record = QuickLoginRecordDao.get_record_with_verification_code(verification_code)
        if record is None:
            response = Response(QuickLoginFailed, '驗證碼不存在或已過期', {})
            return HttpResponse(response.toJson(), content_type='application/json')

        now = datetime.now(timezone.utc)
        offset = (now - record.update_time).seconds

        if offset > 60:
            response = Response(QuickLoginFailed, '驗證碼已過期', {})
            return HttpResponse(response.toJson(), content_type='application/json')

        user = AccountDao.get_user_with_id(user_id)
        if user is None:
            response = Response(QuickLoginFailed, '使用者不存在', {})
            return HttpResponse(response.toJson(), content_type='application/json')
        user.backend = 'django.contrib.auth.backends.ModelBackend'
        print('使用者 ' + user.username + ' 嘗試登入')
        auth.login(request, user)
        accountInfo = User.objects.get(id=user.id)
        response = Response(Success, 'Success', {'id': accountInfo.id,
                                                 'username': accountInfo.username,
                                                 'email': accountInfo.email})
        return HttpResponse(response.toJson(), content_type='application/json')
複製程式碼

quick_login_record_dao.py

class QuickLoginRecordDao:
    @staticmethod
    def add_record(record):
        record.save()

    @staticmethod
    def get_record_with_user_id(user_id):
        try:
            record = QuickLoginRecord.objects.get(user_id=user_id)
            return record
        except:
            return None

    @staticmethod
    def update_record(record):
        result = QuickLoginRecord.objects.filter(id=record.id).update(
            verification_code=record.verification_code,
            update_time=datetime.datetime.now())
        if result > 0:
            return True
        else:
            return False

    @staticmethod
    def get_record_with_verification_code(code):
        try:
            record = QuickLoginRecord.objects.get(verification_code=code)
            return record
        except:
            return None
複製程式碼

前端程式碼

前端的效果是這樣的:

Python——奇怪的掃碼登入

在已登入的狀態下,點選右上角的『客戶端掃碼登入』按鈕,彈出二維碼。

動效、模態窗什麼的就不過多展示程式碼了,只關注主流程的程式碼:

導航欄按鈕

<p class="nav-item" v-if="account.status">
	<button class="button is-primary" type="submit" @click="openModalImage">客戶端掃碼登入		</button>
</p>
複製程式碼

openModalImage 函式

  openModalImage () {
    const imageModal = openImageModal()
    imageModal.loading = true
    var baseUrl = window.location.protocol + '//' + window.location.host
    request('/frontend/account/request_quick_login', {
      method: 'get'
    }).then((response) => {
      var verificationCode = response.data.verification_code
      var url = baseUrl + '/frontend/account/quick_login' + '?' +
        'verification_code=' + verificationCode + '&' +
      'user_id=' + this.accountInfo.id

      QRCode.toDataURL(url)
        .then(url => {
          imageModal.imgUrl = url
          imageModal.loading = false
          imageModal.$children[0].active()
        })
        .catch(err => {
          console.error(err)
        })
    }).catch((response) => {
      notification.toast({
        message: response.message,
        type: 'danger',
        duration: 2000
      })
    })
  }
複製程式碼

iOS程式碼

iOS 程式碼就不展示了,就是掃碼訪問二維碼裡的 URL,再加上一些非法 URL 的判斷即可。

總結

過完整個流程後,可以感覺到,類似於支付寶的掃碼支付。給出一個定時重新整理的二維碼,供給客戶端進行掃碼登入。

當然,還有可以完善的地方,比如前段在開啟了二維碼模態窗時,每 60 秒進行一次定時重新整理。


有什麼問題都可以在博文後面留言,或者微博上私信我,或者郵件我 coderfish@163.com

博主是 iOS 妹子一枚。

希望大家一起進步。

我的微博:小魚周凌宇

相關文章