Если у вас есть сайт, которым часто пользуются с мобильных устройств (таких как телефоны и планшетные ПК), то вы, возможно, задавались вопросом, как реализовать быстрый вход — так, чтобы пользователю не требовалось вводить ни адрес сайта, ни логин и пароль (либо E-mail и пароль).
На некоторых сайтах вы, возможно, видели возможность отправить SMS-сообщение со ссылкой для быстрого входа — это, по сути, приблизительно то же самое. Основное отличие описанного в данной заметке подхода в том, что вместо отправки SMS-сообщения мы будем генерировать QR-код, который содержит ссылку, позволяющую войти на сайт без ввода авторизационных данных.
Кстати, весь процесс написания приложения, которое приводится далее, можно посмотреть в скринкасте на YouTube.
Перед тем, как начать реализовывать этот вариант авторизации, давайте рассмотрим, чем он отличается от варианта с отправкой SMS-сообщения:
- SMS-сообщение получается немного безопаснее: с одной стороны, мы передаём ссылку через третьи стороны (в частности, SMS-гейт, оператор сотовой связи), но с другой стороны, во-первых, ссылка будет недоступна через JS (и, соответственно, даже если на сайте есть XSS, то получить ссылку из SMS-сообщения злоумышленнику не удастся — а вот из QR-кода её получить можно), а во-вторых получить физический доступ к компьютеру с браузером, в котором пользователь авторизован на сайте, в общем случае проще, чем получить доступ к компьютеру, и, одновременно с этим, к телефону пользователя
- SMS-сообщение, в то же время, не настолько универсально: оно не подойдёт, если по какой-то причине нет сотовой связи, либо мобильное устройство, используемое пользователем, вообще не предполагает возможность работы в сотовых сетях (но зато имеет и камеру, и подключение к Интернету, и приложение для сканирования QR-кодов на нём есть или легко устанавливается)
- Отправка SMS-сообщений стоит денег, генерирование QR-кодов абсолютно бесплатно
- У SMS-сообщений в некоторых ситуациях могут значительно падать показатели скорости и надёжности (то есть сообщение может прийти со значительной задержкой, а может даже вообще не прийти), а сканирование QR-кодов предсказуемо и как правило работает хорошо (по крайней мере, если с камерой всё в порядке)
- Для отправки SMS-сообщений сайту необходим номер телефона пользователя, однако далеко не всем пользователям нравится идея вводить свой номер телефона где-либо в Интернете
Получается, что вариант с QR-кодами весьма неплох. И даже если нам важна высокая безопасность, то теоретически никто не мешает отправлять QR-код по электронной почте, или, например, каждый раз запрашивать пароль (лишний раз ввести пароль на удобной большой клавиатуре компьютера, мне кажется, намного проще, чем вводить адрес сайта + логин/адрес электронной почты + пароль на виртуальной клавиатуре мобильного устройства). Тем не менее, сейчас предлагаю реализовать самый простой, базовый вариант быстрого входа по QR-кодам, на реализацию которого нам потребуется минимум времени.
Итак, давайте начнём.
Прежде всего перейдём в рабочую директорию 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.
Также можно использовать не просто хэш, а, например, хэш с префиксом — особенно актуально в том случае, если где-то в другом месте тоже создаются аналогичные ключи.
Получение домена здесь происходит с помощью django.contrib.sites. Указать домен можно через административный интерфейс (/admin/sites/site/).
Если ваш сервер находится за reverse proxy (например, nginx), и вы используете SSL, то убедитесь, что информация об этом включается в запросы к upstream-серверу — это нужно для того, чтобы request.is_secure() выдавал правильное значение (для этого определите в настройках https://docs.djangoproject.com/en/dev/ref/settings/#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-Zd]{50})/$',
'qr_code_picture',
name='auth_qr_code'
),
url(
r'^(?P<auth_code_hash>[a-fd]{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