Пользователь
0,0
рейтинг
27 мая 2013 в 13:31

Разработка → Django: Использование QR-кодов для быстрого входа на сайт с мобильных устройств tutorial

Если у вас есть сайт, которым часто пользуются с мобильных устройств (таких как телефоны и планшетные ПК), то вы, возможно, задавались вопросом, как реализовать быстрый вход — так, чтобы пользователю не требовалось вводить ни адрес сайта, ни логин и пароль (либо E-mail и пароль).

На некоторых сайтах вы, возможно, видели возможность отправить SMS-сообщение со ссылкой для быстрого входа — это, по сути, приблизительно то же самое. Основное отличие описанного в данной заметке подхода в том, что вместо отправки SMS-сообщения мы будем генерировать QR-код, который содержит ссылку, позволяющую войти на сайт без ввода авторизационных данных.

Скриншот с экрана мобильного телефона

Кстати, весь процесс написания приложения, которое приводится далее, можно посмотреть в скринкасте (есть на YouTube, либо в более хорошем качестве в виде файла MPEG2 в 1080p).

Перед тем, как начать реализовывать этот вариант авторизации, давайте рассмотрим, чем он отличается от варианта с отправкой SMS-сообщения:

  • SMS-сообщение получается немного безопаснее: с одной стороны, мы передаём ссылку через третьи стороны (в частности, SMS-гейт, оператор сотовой связи), но с другой стороны, во-первых, ссылка будет недоступна через JS (и, соответственно, даже если на сайте есть XSS, то получить ссылку из SMS-сообщения злоумышленнику не удастся — а вот из QR-кода её получить можно), а во-вторых получить физический доступ к компьютеру с браузером, в котором пользователь авторизован на сайте, в общем случае проще, чем получить доступ к компьютеру, и, одновременно с этим, к телефону пользователя
  • SMS-сообщение, в то же время, не настолько универсально: оно не подойдёт, если по какой-то причине нет сотовой связи, либо мобильное устройство, используемое пользователем, вообще не предполагает возможность работы в сотовых сетях (но зато имеет и камеру, и подключение к Интернету, и приложение для сканирования QR-кодов на нём есть или легко устанавливается)
  • Отправка SMS-сообщений стоит денег, генерирование QR-кодов абсолютно бесплатно
  • У SMS-сообщений в некоторых ситуациях могут значительно падать показатели скорости и надёжности (то есть сообщение может прийти со значительной задержкой, а может даже вообще не прийти), а сканирование QR-кодов предсказуемо и как правило работает хорошо (по крайней мере, если с камерой всё в порядке)
  • Для отправки SMS-сообщений сайту необходим номер телефона пользователя, однако далеко не всем пользователям нравится идея вводить свой номер телефона где-либо в Интернете

Получается, что вариант с QR-кодами весьма неплох. И даже если нам важна высокая безопасность, то теоретически никто не мешает отправлять QR-код по электронной почте, или, например, каждый раз запрашивать пароль (лишний раз ввести пароль на удобной большой клавиатуре компьютера, мне кажется, намного проще, чем вводить адрес сайта + логин/адрес электронной почты + пароль на виртуальной клавиатуре мобильного устройства). Тем не менее, сейчас предлагаю реализовать самый простой, базовый вариант быстрого входа по QR-кодам, на реализацию которого нам потребуется минимум времени.

Текст ниже, как и скринкаст, поясняет различные особенности реализации. Если вы хотите быстро добавить приложение на сайт, то вы можете установить его через pip:

pip install django-qrauth

В этом случае вам останется только включить схему urls приложения в ваш главный urls.py, а также добавить шаблоны. Инструкция по установке, а также исходники вы можете найти на Github.

Ниже описано, как вы можете собрать приложение самостоятельно — актуально, например, в том случае, если вы сразу хотите что-то в нём отредактировать.

Итак, прежде всего перейдём в рабочую директорию Django-проекта, в котором мы хотим добавить такую авторизацию, и создадим новое приложение. Назовём его, например, qrauth:

python manage.py startapp qrauth

В появившейся директории создадим файл qr.py:

try:
    from PIL import Image, ImageDraw
except ImportError:
    import Image, ImageDraw

import qrcode.image.base
import qrcode.image.pil

class PilImage(qrcode.image.pil.PilImage):
    def __init__(self, border, width, box_size):
        if Image is None and ImageDraw is None:
            raise NotImplementedError("PIL not available")
        qrcode.image.base.BaseImage.__init__(self, border, width, box_size)
        self.kind = "PNG"

        pixelsize = (self.width + self.border * 2) * self.box_size
        self._img = Image.new("RGBA", (pixelsize, pixelsize))
        self._idr = ImageDraw.Draw(self._img)

def make_qr_code(string):
    return qrcode.make(string, box_size=10, border=1, image_factory=PilImage)

Здесь используется модуль python-qrcode. Установить его можно с помощью pip:

pip install qrcode

Для того, чтобы получались картинки с прозрачным (а не белым) фоном, мы специально используем свой класс для создания картинок, наследуя его от qrcode.image.pil.PilImage. Если вас устраивают картинки с белым фоном, то достаточно будет написать так:

import qrcode

def make_qr_code(string):
    return qrcode.make(string, box_size=10, border=1)

Стоит отметить, что в данном случае картинки, которые возвращает qrcode.make (и, соответственно, функция make_qr_code) неоптимальны с точки зрения размера. Например, с помощью optipng их размер удаётся уменьшить примерно на 70% (разумеется, без потери качества). Тем не менее, в большинстве случаев это непринципиально — их размер в любом случае получается небольшим (в пределах нескольких кибибайтов).

Далее создадим файл utils.py и добавим функции, которые затем будем использовать в представлениях (views):

import os
import string
import hashlib

from django.conf import settings

def generate_random_string(length,
                           stringset="".join(
                               [string.ascii_letters+string.digits]
                           )):
    """
    Returns a string with `length` characters chosen from `stringset`
    >>> len(generate_random_string(20) == 20 
    """
    return "".join([stringset[i%len(stringset)] \
        for i in [ord(x) for x in os.urandom(length)]])

def salted_hash(string):
    return hashlib.sha1(":)".join([
        string,
        settings.SECRET_KEY,
    ])).hexdigest()

Функция generate_random_string генерирует строку случайных символов заданной длины. По умолчанию строка составляется из букв латинского алфавита (как нижнего, так и верхнего регистра) и цифр.

Функция salted_hash солит и хэширует строку.

Теперь откроем views.py и напишем представления:

import redis

from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, get_backends
from django.contrib.sites.models import get_current_site
from django.template import RequestContext
from django.shortcuts import render_to_response
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.core.urlresolvers import reverse

from django.contrib.auth.models import User

from privatemessages.context_processors import \
number_of_new_messages_processor

from utils import generate_random_string, salted_hash
from qr import make_qr_code

@login_required
def qr_code_page(request):
    r = redis.StrictRedis()

    auth_code = generate_random_string(50)
    auth_code_hash = salted_hash(auth_code)

    r.setex(auth_code_hash, 300, request.user.id)

    return render_to_response("qrauth/page.html",
                              {"auth_code": auth_code},
                              context_instance=RequestContext(request))

@login_required
def qr_code_picture(request, auth_code):
    r = redis.StrictRedis()

    auth_code_hash = salted_hash(auth_code)

    user_id = r.get(auth_code_hash)

    if (user_id == None) or (int(user_id) != request.user.id):
        raise Http404("No such auth code")

    current_site = get_current_site(request)
    scheme = request.is_secure() and "https" or "http"

    login_link = "".join([
        scheme,
        "://",
        current_site.domain,
        reverse("qr_code_login", args=(auth_code_hash,)),
    ])

    img = make_qr_code(login_link)
    response = HttpResponse(mimetype="image/png")
    img.save(response, "PNG")
    return response

def login_view(request, auth_code_hash):
    r = redis.StrictRedis()
    user_id = r.get(auth_code_hash)

    if user_id == None:
        return HttpResponseRedirect(reverse("invalid_auth_code"))

    r.delete(auth_code_hash)

    try:
        user = User.objects.get(id=user_id)
    except User.DoesNotExist:
        return HttpResponseRedirect(reverse("invalid_auth_code"))

    # In lieu of a call to authenticate()
    backend = get_backends()[0]
    user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
    login(request, user)

    return HttpResponseRedirect(reverse("dating.views.index"))

При обращении к странице с QR-кодом (qr_code_page) генерируется случайная строка из 50 символов. Далее в Redis (установить клиент можно с помощью pip install redis) добавляется новая пара ключ-значение, где в качестве ключа задаётся солёный хэш сгенерированной случайной строки, а в качестве значения — идентификатор пользователя (это нужно для того, чтобы картинка с QR-кодом, которая добавляется на страницу, была доступна только этому пользователю). Это этого ключа устанавливается время истечения, в примере указано 300 секунд (5 минут).

В контексте шаблона при этом задаётся сгенерированная случайная строка: эта строка затем используется в адресе, по которому возвращается картинка с QR-кодом (а вот для авторизации как раз используется хэш, и в QR-код включается именно адрес с хэшем: таким образом, даже если кому-то ещё становится известен адрес картинки, то для авторизации этого будет недостаточно — для авторизации нужно знать солёный хэш для случайной строки, указанной в адресе картинки).

Далее, при загрузке картинки (qr_code_picture) случайная строка, содержащаяся в адресе картинки, опять же, хэшируется, и затем проверяется, есть ли в Redis соответствующий ключ. Если такой ключ есть, и содержит идентификатор текущего пользователя, то создаётся и возвращается QR-код, содержащий абсолютную ссылку для мгновенной авторизации на сайте. В ином случае возвращается ошибка 404.

Кстати, понимаете, что здесь можно легко улучшить?
При выполнении int(user_id) != request.user.id может возникнуть ValueError — в том случае, если в Redis есть такой ключ, но его значение не подходит для преобразования к десятичному целому числу. Решается с помощью try…except, либо с помощью строкового метода isdigit. Либо можно наоборот получать строку от request.user.id и сравнивать её с полученным значением ключа.
Также можно использовать не просто хэш, а, например, хэш с префиксом — особенно актуально в том случае, если где-то в другом месте тоже создаются аналогичные ключи.

Получение домена здесь происходит с помощью django.contrib.sites. Указать домен можно через административный интерфейс (/admin/sites/site/).

Если ваш сервер находится за reverse proxy (например, nginx), и вы используете SSL, то убедитесь, что информация об этом включается в запросы к upstream-серверу — это нужно для того, чтобы request.is_secure() выдавал правильное значение (для этого определите в настройках SECURE_PROXY_SSL_HEADER, но учитывайте, что вам нужно будет обязательно устанавливать/удалять этот заголовок на стороне прокси-сервера — иначе, если, например, ваш сайт доступен и по HTTP, и по HTTPS, то пользователь, который заходит по HTTP, сможет установить этот заголовок таким образом, что request.is_secure() будет выдавать значение True, а это плохо с точки зрения безопасности).

И да, начиная с Python 2.6 вместо request.is_secure() and "https" or "http" можно писать "https" if request.is_secure() else "http".

При переходе по ссылке для мгновенной авторизации проверяется, есть ли в Redis ключ, соответствующий указанному в ссылке хэшу. Если нет — пользователь перенаправляется на страницу с сообщением о том, что QR-код недействителен (в данном примере эта страница не требует написания отдельного представления). Если есть — то ключ в Redis удаляется, после чего проверяется, есть ли в базе данных пользователь с таким идентификатором. Если нет — опять же, происходит перенаправление на страницу с сообщением о том, что QR-код недействителен. Если есть — то происходит авторизация и пользователь перенаправляется на главную страницу сайта.

Теперь добавим файл urls.py и определим в нём схему URL приложения:

from django.conf.urls import patterns, url
from django.views.generic.simple import direct_to_template

urlpatterns = patterns('qrauth.views',
    url(
        r'^pic/(?P<auth_code>[a-zA-Z\d]{50})/$',
        'qr_code_picture',
        name='auth_qr_code'
    ),
    url(
        r'^(?P<auth_code_hash>[a-f\d]{40})/$',
        'login_view',
        name='qr_code_login'
    ),
    url(
        r'invalid_code/$',
        direct_to_template,
        {'template': 'qrauth/invalid_code.html'},
        name='invalid_auth_code'
    ),
    url(
        r'^$',
        'qr_code_page',
        name='qr_code_page'
    ),
)

Также не забудьте открыть ваш главный urls.py (который указывается в ROOT_URLCONF), и включить туда urlpatterns из urls.py созданного приложения:

urlpatterns = patterns('',
    # …
    url(r'^qr/', include('qrauth.urls')),
    # …
)

Теперь откройте директорию с шаблонами и добавьте туда каталог qrauth.

Пример для invalid_code.html:

{% extends "base.html" %}

{% block title %}QR-код недействителен{% endblock %}

{% block content %}
<div class="error">
    <h1>QR-код недействителен</h1>
    <p>QR-код, который вы используете для авторизации, недействителен. Пожалуйста, попробуйте ещё раз открыть страницу с QR-кодом для входа и отсканировать код повторно.</p>
</div>
{% endblock %}

Пример для page.html:

{% extends "base.html" %}

{% block title %}QR-код для входа{% endblock %}

{% block content %}
<div class="qr_code">
    <h1>QR-код для входа</h1>
    <p>Для быстрого входа на сайт с мобильного устройства (например, телефона или планшета) отсканируйте этот QR-код:</p>
    <div><img src="{% url auth_qr_code auth_code %}" alt="QR"></div>
    <p>Каждый сгенерированный QR-код работает только один раз и только 5 минут. Если вам требуется другой QR-код, то просто откройте <a href="{% url qr_code_page %}">эту страницу</a> снова.</p>
</div>
{% endblock %}

Собственно, теперь остаётся открыть сайт в браузере и проверить. Если QR-код успешно генерируется и отображается, попробуйте сосканировать его с помощью телефона или ещё чего-нибудь с камерой и Интернетом.

Если будут какие-то вопросы или мысли о том, какие ещё могут быть варианты для быстрой и удобной авторизации с мобильных устройств — буду рад комментариям.

Всем удачи и приятного программирования!

С наступающим вас летом! :)
Арсений @MaGIc2laNTern
карма
231,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (11)

  • +4
    На хабре статьям оч не хватает QR кодов, приходится через избранное перекидывать
  • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    Интереснее когда сканируешь код, и в браузере обновляется страница, пуская внутрь. Видел в AirDroid (но там и сервер и считывалка в самой программе на устройстве). Вероятно, если использовать websockets или что-то такое, можно реализовать и у себя на сервере, а считывалка просто будет проходить по ссылке, куда нужно подсунуть что-то вроде «Поздравляем, вы вошли, сейчас страница на вашем компьютере перезагрузится.»
    • 0
      Уточните, пожалуйста, в чём здесь заключается смысл, если на стороне мобильного телефона ничего не проверяется (ведь можно и на компьютере QR-код распознать и точно так же пройти по этой ссылке)? Авторизация будет доступна любому, кто может сосканировать QR-код, с любого устройства? Или всё-таки сканировать нужно будет специальным приложением, которое будет что-то дополнительно проверять и, например, отправлять POST-запрос, содержащий какие-то дополнительные параметры (например, идентификатор сессии, который программа ранее получила, запросив у пользователя логин и пароль и отправив на сервер запрос авторизации)?
      • 0
        Правильные вопросы. Для AirDroid функционал очевиден: следует подтвердить, что именно ты владелец устройства, в которое хочешь войти. Для десктопа ясно вариант входа как замены логина/пароля не прокатит, но возможно есть какие-то другие варианты: подтверждения, какой-то другой функционал, который требует «сложного» перехода по ссылке.
        Но это я так, из упрямства, подумав, признаю, что интересно в техническом плане, но, вероятно, бесполезно.
  • 0
    Чего-то не понимаю задачу «как реализовать быстрый вход — так, чтобы пользователю не требовалось вводить ни адрес сайта, ни логин и пароль». Если пользователь не идентифицировал себя (не вводил хотя бы логин), то откуда сайту известно, чей входной QR-код показывать? Если пользователь не вводил адрес сайта, то откуда (и где) QR-код вообще возьмётся?

    Или задача стоит так: пользователь зашёл на сайт (залогинился) с обычного компьютера — а тут ему показывают такой QR-код, при помощи которого он может заодно и мобильник (или планшет) залогинить?

    Или задача стоит так: один раз зайти на сайт, затем получить QR-код, распечатать — а все последующие заходы совершать через сканирование этого QR-кода?
    • +1
      Второй описанный вариант наиболее точно соответствует подходу, описанному в этой блогозаписи. За тем исключением, что в данном примере за QR-кодом можно после входа на сайт обращаться неограниченное количество раз (до тех пор, пока пользователь авторизован на сайте).

      То есть смысл здесь в том, что на компьютере входить на сайт удобно, а на мобильных устройствах — не очень удобно (у многих, например, пароль содержит слова на русском языке, набранные в английской раскладке). Значит, имеет смысл дать пользователям возможность входить на сайт без ручного ввода адреса сайта и каких-либо авторизационных данных.
  • –1
    Тут уже похожее реализовано QR код
    • +1
      Какое у них суровое SEO на главной.

      P.S. А где там QR-коды для мгновенной авторизации?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.