最近在做 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
## 设计思路
扫码登录 URL
首先能想到的是, 服务器要提供给移动 APP 可以访问的 URL(展现成二维码给 APP 扫描), 这个 URL 需要包括
user 的唯一标志符
验证码
那么其 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 生成的对应数据库为:
什么时候生成验证码, 那当然是生成二维码的时候, 所以, 这个 URL 不是给移动端请求的, 而是给前端来请求的, 前端在已登录的情况下, 访问该 URL 可以直接传递 user 信息, 后端通过拿到 user 信息, 生成一条 QuickLoginRecord 记录.
前端访问并拿到验证码的 URL 的大致模样是:
frontend/account/request_quick_login
那么整个流程就是 (省略了细节处理):
后端代码
- 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)
规定 HTTPMethod 必须为 GET 访问
获取 request 中的 user_id 参数
通过 user_id 查询 QuickLoginRecord 记录
如果未查询到结果, 新建一个 QuickLoginRecord 记录, 设置 user 关联, verification_code(create_time 和 update_time 在 QuickLoginRecordDao 中自动设置)
如果查询到结果, 更新 verification_code 字段 ( update_time 在 QuickLoginRecordDao 中自动设置)
返回 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):
规定 HTTPMethod 必须为 GET 访问
拿到 reqeust 中的 user_id
拿到 reqeust 中的 verification_code
通过 verification_code 获取 QuickLoginRecord 记录
如果记录不存在则表示验证码不存在或过期
如果存在, 比较 update_time 字段, 判断是否已经超过 60 秒
超过 60 秒返回验证码过期
未超过 60 秒, 让用户登录
返回 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
前端代码
前端的效果是这样的:
在已登录的状态下, 点击右上角的客户端扫码登录按钮, 弹出二维码.
动效, 模态窗什么的就不过多展示代码了, 只关注主流程的代码:
导航栏按钮
<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 秒进行一次定时刷新.
来源: https://juejin.im/post/5b0a7b566fb9a07ac90dab32