пятница, 8 декабря 2006 г.

Дополнительная авторизация для Django. One-time code.

Термин one-time code (одноразовый код) известен из криптографии. Это — некий код произвольного содержания, который запускает процесс, который ожидает появления этого кода. После разглашения кода и запуска процесса, код становится бесполезен, и больше не действует. Вы наверняка слышали о таких фразах как "В Сантьяго идет дождь", "Над всей Испанией безоблачное небо", "Начинайте восхождение на гору Ниитака", которые, по сути не содержащие никаких важных сведений, оказали существенное влияние на историю XX века. Это — одноразовые коды.

Чем могут быть полезны одноразовые коды для веб-программирования? Применений может быть много. Первое, что приходит в голову — это возможность авторизовать посетителя, предоставившего одноразовый код, не требуя от него обычных логина и пароля. Например, по заходу по ссылке, которую мы посылаем ему по email, которая ведет на часть сайта, требующую авторизации, и содержащую одноразовый код. Возможны и другие сценарии, например, запуск специального механизма, устанавливающего скидку клиенту в интернет-магазине, или перенаправление клиента на некую "секретную" страницу, на которую нельзя попасть по ссылке.


Рассмотрим реализацию авторизации по одноразовому коду на примере Django. Мы будем использовать такие механизмы Django как дополнительный источник авторизации (в частности, дополнительный бэкенд авторизации - custom authentication backend) и дополнительный middleware.

Дополнительный бэкенд авторизации - это класс, интерфейс которого должен определять два метода - authenticate и get_user. Метод authenticate принимает произвольные аргументы и должен возвращать объект пользователя, совместимый по интерфейсу с объектом класса django.contrib.auth.models.User (фактически, мы будем использовать этот стандартный класс User), либо None, если по переданным аргументам авторизовать пользователя не получилось. Метод get_user должен принимать единственный аргумент - идентификатор пользователя, и возвращать объект пользователя по идентификатору, либо None, если пользователь не найден. Эти методы вызываются соответственно функциями authenticate и get_user из модуля django.contrib.auth при успешно сконфигурированном бэкенде. Наш бэкенд будет выглядеть примерно так:

# module myproj.apps.onetimecode.backend
from django.contrib.auth.models import User
from myproj.apps.onetimecode.models import OneTimeCode

class OneTimeCodeBackend(object):
def authenticate(self, code):
if not code:
return None
try:
otcode = OneTimeCode.objects.get(pk=code)
except OneTimeCode.DoesNotExist:
return None
user = otcode.user
otcode.delete()
return user

def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None


Метод authenticate получает код, находит ассоциированного с ним пользователя, удаляет код из базы (он же у нас одноразовый) и возвращает полученный объект user.

Как заметно из кода, используется класс OneTimeCode, который пока не определен. Сделаем это:
#module myproj.apps.onetimecode.models
from django.db import models
from django.contrib.auth.models import User

class OneTimeCode(models.Model):
code = models.CharField(maxlength=255, primary_key=True)
user = models.ForeignKey(User, raw_id_admin=True)

class Admin:
pass


Для того, чтобы инициализировать эту модель в приложении Django, добавьте myproj.apps.onetimecode (имя проекта здесь — myproj, вам скорее всего надо изменить это) в список INSTALLED_APPS файла settings.py вашего проекта и запустите python manage.py syncdb в каталоге проекта. Django создаст таблицу в базе данных и зарегистрирует модель в своих служебных таблицах.

У нас есть бэкенд, надо его подключить. Для этого в файле settings.py определяем список AUTHENTICATION_BACKENDS и добавляем в него наш бэкенд. Этот список определен в файле конфигурации Django по умолчанию, и содержит 'django.contrib.auth.backends.ModelBackend', стандартный бэкенд авторизации в Django. В файле settings.py вашего проекта список AUTHENTICATION_BACKENDS скорее всего отсутствует. Включите в него 'django.contrib.auth.backends.ModelBackend', иначе стандартный логин работать не будет.
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'myproj.apps.onetimecode.backend.OneTimeCodeBackend',
)


Осталось только задействовать наш бэкенд, чтобы он заработал. Делаем это через middleware.

# module myproj.apps.onetimecode.middleware
from django.contrib.auth import authenticate, login

class OneTimeCodeAuthMiddleware(object):
def process_request(self, request):
code = request.GET.get('otcode', None)
if not code:
return None

user = authenticate(code=code)
if user:
login(request, user)

return None


Наш middleware ожидает переменную otcode, переданную в строке запроса. Если переменная есть, вызывается функция django.contrib.auth.authenticate с параметром code=code. Обратите внимание, что вызывается не метод нашего бэкенда, а функция из пакета авторизации Django. Она сама загружает нужный бэкенд и проверяет, принимает ли его метод authenticate предоставленные аргументы. Если метод не принимает такие аргументы или возвращает None, то функция берет следующий бэкенд из списка и пробует с ним. Если метод вернул объект пользователя, то authenticate его возвращает, и другие бэкенды не трогает. Затем нужно вызвать функцию login, которая ассоциирует полученного пользователя с текущим сеансом.

Подключаем наш middleware через список MIDDLEWARE_CLASSES в settings.py так, чтобы этот класс был указан после "django.contrib.sessions.middleware.SessionMiddleware" (для авторизации в любом случае требуется объект request.session) и до "django.contrib.auth.middleware.AuthenticationMiddleware" (который фактически присваивает request.user). Список будет выглядеть примерно так:

MIDDLEWARE_CLASSES = (
"django.middleware.common.CommonMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"myproj.apps.onetimecode.middleware.OneTimeCodeAuthMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.middleware.doc.XViewMiddleware",
)


Ну вот, собственно, и всё. Осталось только использовать это. Например, так:
from myproj.apps.onetimecode.models import OneTimeCode
from django.contrib.auth.models import User

user = User.object.get(pk=uid)
code = 'ToraToraTora'
otc = OneTimeCode.objects.create(code=code, user=user)

text = '''You can enter to our secret page
http://example.com/secret/page?otcode=%s
without your login and password''' % code

user.email_user('login with one-time code', text)


Получив это письмо, пользователь сможет зайти по ссылке, автоматически авторизовавшись.

Чтобы не выдумывать одноразовый код каждый раз, сделаем случайный генератор кодов в классе OneTimeCode. Для этого добавим в класс такой метод, например:

import os, md5, time
@classmethod
def generate(cls, user):
rnd = '%s-%s' % (os.urandom(20), str(time.time()))
code = md5.md5(rnd).hexdigest()
return cls.objects.create(code=code, user=user)


Метод generate вернет нам объект OneTimeCode со случайно сгенерированным кодом.

otc = OneTimeCode.generate(user)



Механизм одноразовых кодов, как уже говорилось, можно использовать не только для авторизации. Расширьте модель OneTimeCode, добавив туда, например, поле action и интерпретируйте его, чтобы понять, для чего выдан этот код и какие действия следует с ним запускать.

1 комментарий:

  1. [...] 23.10.2008 17:05 Как пример по автологину, можно посмотреть это:http://www.ragbag.ru/2006/12/08/django_one_time_code/А насчет правильности - это смотря как все будет реализовываться.Если каждому пользователю будешь выдавать например сертификатыи авторизовывать только по ним - то это очень правильное решениев рамках предложенной Вами задачи.Ну и требуемая безопасность определяется не уровнем "продвинутости"пользователей а ценностью информации, так что пишите сразу требуемыйуровень, а то сегодня у вас все пользователи "чайники", а завтра кодному из чайников зайдет друг-"хакер" и наступит в Вашей жизниперсональный "финансовый кризис". [...]

    ОтветитьУдалить

Постоянные читатели