最近在作 Sparrow(還在內測的一個敲好用 Mock 系統😁)的時候遇到了一個需求。Sparrow 服務器是使用 Django 2.0 編寫的產品,因此本文全部的代碼背景均爲 Django 2.0 環境和 Python 3.6.3 語言,總體是 Vue + Django + SQLite。html
Sparrow 的操做通常都是在網頁上操做,而手機客戶端每每是用來同步一些簡單數據的。那麼這裏遇到一個和日常 APP 不一樣的使用場景。前端
通常來講,一個產品的操做大可能是在手機上,那麼 PC 客戶端和網頁版就能夠經過已經登陸的移動端 APP 掃碼登陸。python
而如今的狀況是,Sparrow 的使用大多在網頁版,那麼,我須要的就是,讓移動 APP 用戶在網頁版已經登陸的狀況下免去輸入用戶名、密碼的登陸操做,讓移動 APP 用戶掃描網頁二維碼,完成移動 APP 的登陸。數據庫
##設計思路django
首先能想到的是,服務器要提供給移動 APP 能夠訪問的 URL(展示成二維碼給 APP 掃描),這個 URL 須要包括json
那麼其 URL 的大體模樣就是:後端
frontend/account/quick_login?user_id=<user_id>&verification_code=<verification_code>
複製代碼
驗證碼是從哪裏來的?服務器
緣由是這樣的,若是掃碼登陸的 URL 永久有效,顯然是不合理的,這意味着只要獲得了這個 URL,任何人均可以經過這個 URL 隨時登陸該用戶的帳號,因此須要有驗證碼。app
同時,驗證碼須要附帶生成時間,以此來達到驗證碼一分鐘有效的 Feature。爲此,設計一個外鍵爲 user 的 Model:frontend
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 生成的對應數據庫爲:
何時生成驗證碼,那固然是生成二維碼的時候,因此,這個 URL 不是給移動端請求的,而是給前端來請求的,前端在已登陸的狀況下,訪問該 URL 能夠直接傳遞 user 信息,後端經過拿到 user 信息,生成一條 QuickLoginRecord 記錄。
前端訪問並拿到驗證碼的 URL 的大體模樣是:
frontend/account/request_quick_login
複製代碼
那麼整個流程就是(省略了細節處理):
urlpatterns = [
// ···
path('frontend/account/quick_login', AccountAction.quick_login),
path('frontend/account/request_quick_login', AccountAction.request_quick_login),
]
複製代碼
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='')
複製代碼
這裏代碼不想看的話,大概描述一下過程:
@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')
複製代碼
@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')
複製代碼
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
複製代碼
前端的效果是這樣的:
在已登陸的狀態下,點擊右上角的『客戶端掃碼登陸』按鈕,彈出二維碼。
動效、模態窗什麼的就不過多展現代碼了,只關注主流程的代碼:
<p class="nav-item" v-if="account.status">
<button class="button is-primary" type="submit" @click="openModalImage">客戶端掃碼登陸 </button>
</p>
複製代碼
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 代碼就不展現了,就是掃碼訪問二維碼裏的 URL,再加上一些非法 URL 的判斷便可。
過完整個流程後,能夠感受到,相似於支付寶的掃碼支付。給出一個定時刷新的二維碼,供給客戶端進行掃碼登陸。
固然,還有能夠完善的地方,好比前段在打開了二維碼模態窗時,每 60 秒進行一次定時刷新。
有什麼問題均可以在博文後面留言,或者微博上私信我,或者郵件我 coderfish@163.com。
博主是 iOS 妹子一枚。
但願你們一塊兒進步。
個人微博:小魚周凌宇