python

Мега-Учебник Flask, Часть 5: Вход пользователей

  • пятница, 16 мая 2014 г. в 03:10:28
http://habrahabr.ru/post/222983/

Предисловие от переводчика.
Переводом предыдущих частей этого руководства занимался wiygn. С его согласия я продолжаю это дело.


Это пятая статья в серии, где я описываю свой опыт написания веб-приложения на Python с использованием микрофреймворка Flask.

Цель данного руководства — разработать довольно функциональное приложение-микроблог, которое я за полным отсутствием оригинальности решил назвать microblog.

Оглавление
Часть 1: Привет, Мир!
Часть 2: Шаблоны
Часть 3: Формы
Часть 4: База данных
Часть 5: Вход пользователей (данная статья)
Часть 6: Страница профиля и аватары
Часть 7: Unit-тестирование
Часть 8: Подписчики, контакты и друзья
Часть 9: Пагинация
Часть 10: Полнотекстовый поиск
Часть 11: Поддержка e-mail
Часть 12: Реконструкция
Часть 13: Дата и время
Часть 14: I18n and L10n
Часть 15: Ajax
Часть 16: Отладка, тестирование и профилирование
Часть 17: Развертывание на Linux (даже на Raspberry Pi!)
Часть 18: Развертывание на Heroku Cloud

Краткое повторение


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

В этой статье мы объединим наши знания о веб-формах и базах данных и напишем свою систему для входа пользователей. В конце данного руководства наше небольшое приложение будет регистрировать новых пользователей и проводить их авторизацию.

Для работы с этой главой ваше приложение должно быть таким, каким мы оставили его в конце предыдущей главы. Пожалуйста, убедитесь, что приложение установлено и работает.


Конфигурация


Как и в предыдущих главах, мы начнём с настройки расширений, которые будем использовать. Для авторизации нам понадобятся два расширения — Flask-Login и Flask-OpenID. Настроим их следующим образом (файл app/__init__.py):

import os
from flask.ext.login import LoginManager
from flask.ext.openid import OpenID
from config import basedir

lm = LoginManager()
lm.init_app(app)
oid = OpenID(app, os.path.join(basedir, 'tmp'))

Расширению Flask-OpenID нужно где-то хранить свои временные файлы, для этого при инициализации ему передаётся путь до папки tmp.

Функция представления авторизации


Давайте обновим нашу функцию представления (файл app/views.py):

from flask import render_template, flash, redirect, session, url_for, request, g
from flask.ext.login import login_user, logout_user, current_user, login_required
from app import app, db, lm, oid
from forms import LoginForm
from models import User, ROLE_USER, ROLE_ADMIN

@app.route('/login', methods = ['GET', 'POST'])
@oid.loginhandler
def login():
  if g.user is not None and g.user.is_authenticated():
      return redirect(url_for('index'))
  form = LoginForm()
  if form.validate_on_submit():
      session['remember_me'] = form.remember_me.data
      return oid.try_login(form.openid.data, ask_for = ['nickname', 'email'])
  return render_template('login.html', 
      title = 'Sign In',
      form = form,
      providers = app.config['OPENID_PROVIDERS'])


Обратите внимание, мы импортировали несколько новых модулей, некоторые из которых будут использованы позднее.

Отличий от предыдущей версии немного. Мы добавили новый декоратор для функции отображения. Благодаря oid.loginhandler Flask-OpenID теперь знает, что это — функция для авторизации.

g — это глобальный объект Flask, предназначенный для хранения и обмена данными во время жизни запроса. Именно в нём мы будем хранить данные о текущем пользователе. В верхней части тела функции мы проверяем значение g.user. Если пользователь уже авторизован, мы перенаправляем его на главную страницу. Бессмысленно пытаться еще раз проводить авторизацию в этом случае.

Функция url_for, которую мы использовали при вызове redirect, предоставляет возможность получения URL для переданного ей имени функции представления. Вы, конечно же, можете использовать redirect('/index'), однако есть весьма веские причины поручить построение URL специально предназначенной для этого функции.

Мы также обновили код, обрабатывающий данные полученные из формы авторизации. Здесь мы делаем две вещи. Во-первых, мы сохраняем значение поля remember_me в сессии Flask (не путайте с db.session — сессией, предоставленной расширением Flask-SQLAlchemy). Как было сказано выше, объект flask.g может хранить данные только во время жизни запроса. В то время как flask.session является более сложным хранилищем. Данные, сохраненные в сессии, будут также доступны во время всех последующих запросов от одного клиента. Информация хранится до тех пор, пока не будет явно удалена. Такое поведение возможно благодаря тому, что Flask хранит отдельные сессии для каждого клиента.

Вызов oid.try_login запускает процесс авторизации с помощью Flask-OpenID. Эта функция принимает два аргумента: openid, полученный из веб-формы и список полей, которые мы хотели бы получить от провайдера OpenID. Так как наша модель User имеет атрибуты nickname и email, именно эти данные мы и будем запрашивать.

Аутентификация через OpenID проводится асинхронно. Если получен положительный ответ от провайдера, Flask-OpenID вызовет функцию, объявленную с помощью декоратора oid.after_login. В противном случае пользователь снова вернётся на страницу авторизации.

Обработка ответа от провайдера OpenID


Так выглядит реализация функции after_login (файл app/views.py):

@oid.after_login
def after_login(resp):
    if resp.email is None or resp.email == "":
        flash('Invalid login. Please try again.')
        return redirect(url_for('login'))
    user = User.query.filter_by(email = resp.email).first()
    if user is None:
        nickname = resp.nickname
        if nickname is None or nickname == "":
            nickname = resp.email.split('@')[0]
        user = User(nickname = nickname, email = resp.email, role = ROLE_USER)
        db.session.add(user)
        db.session.commit()
    remember_me = False
    if 'remember_me' in session:
        remember_me = session['remember_me']
        session.pop('remember_me', None)
    login_user(user, remember = remember_me)
    return redirect(request.args.get('next') or url_for('index'))


Аргумент resp, переданный функции after_login содержит в себе данные, полученные от провайдера OpenID.

В первую очередь нам необходимо проверить, что в ответе от сервера содержится email пользователя, в противном случае мы не можем его авторизовать. Проверяем, содержится ли полученный email в нашей базе данных. Если ничего не найдено, добавляем нового пользователя в базу. Стоит отметить, что некоторые провайдеры OpenID не предоставляют nickname, но для нас это не является проблемой, мы можем использовать имя из почты.

После этого мы пытаемся получить значение remember_me из сессии Flask, это то самое значение, которое мы сохранили в функции представления login.

Затем мы вызываем функцию login_user из модуля Flask-Login, чтобы наконец авторизовать пользователя в нашем приложении.

В конце концов мы перенаправляем пользователя по адресу, переданному в атрибуте next, или же на главную страницу, если такой параметр в запросе отсутствует. Идея параметра next весьма проста. Допустим, вы хотите сделать некоторые страницы доступными для просмотра только авторизованным пользователям. С помощью Flask-Login такие страницы могут быть обозначены с помощью декоратора login_required. Если анонимный пользователь попытается открыть такую страницу, он будет автоматически перенаправлен на страницу авторизации, при этом Flask-Login сохранит URL исходной страницы в параметре next. Нам останется только отправить пользователя по этому адресу после того, как авторизация будет пройдена.

Для того, чтобы Flask-Login знал куда отправлять пользователей для авторизации, мы должны сообщить ему об этом при инициализации (файл app/__init__.py):

lm = LoginManager()
lm.init_app(app)
lm.login_view = 'login'


Глобальный объект g.user


В функции представления login мы проверяли состояние g.user, для того, чтобы определить, не является ли текущий пользователь уже авторизованным. Чтобы это работало, мы используем событие Flask before_request. Все функции, объявленные с помощью декоратора before_request будут запущены непосредственно перед вызовом функции отображения каждый раз, когда получен запрос. Таким образом, вполне логичным будет устанавливать значение g.user именно здесь (файл app/views.py):

@app.before_request
def before_request():
    g.user = current_user


Это всё, что нам нужно. Flask-Login предоставляет нам доступ к переменной current_user, мы просто копируем в g ссылку на это значение, для удобства дальнейшего использования. Теперь текущий пользователь будет доступен везде, даже внутри шаблонов.

Отображение главной страницы


В предыдущей главе мы использовали в функции index объекты-заглушки, так как у нас еще не было настоящих пользователей и постов. Теперь у нас есть пользователи, самое время это использовать:

@app.route('/')
@app.route('/index')
@login_required
def index():
    user = g.user
    posts = [
        { 
            'author': { 'nickname': 'John' }, 
            'body': 'Beautiful day in Portland!' 
        },
        { 
            'author': { 'nickname': 'Susan' }, 
            'body': 'The Avengers movie was so cool!' 
        }
    ]
    return render_template('index.html',
        title = 'Home',
        user = user,
        posts = posts)


Мы сделали всего два изменения в этой функции. Во-первых, был добавлен декоратор login_required. Отныне мы можем быть уверены, что эту страницу увидят только зарегистрированные пользователи.

Во-вторых, мы передаём в шаблон непосредственно объект g.user вместо заглушки, используемой ранее.

Самое время запустить приложение.

Когда вы перейдете по адресу http://localhost:5000, вместо главной страницы вы увидите страницу для входа. Авторизация с помощью OpenID проходит с помощью URL, предоставляемого провайдером. Чтобы не вводить адрес вручную, можно использовать одну из ссылок под текстовым полем.

В процессе авторизации вы будете перенаправлены на сайт провайдера, на котором будет необходимо войти в систему и дать своё разрешение на передачу некоторой информации вашему приложению. При этом будут переданы только те параметры, которые мы запросили, т.е. адрес почты и ник. Никакую приватную информацию, включая пароли, провайдеры OpenID не сообщают.

После этого вы окажетесь на главной странице, теперь уже в качестве авторизованного пользователя.

Можете также поэкспериментировать с флагом remember_me. Если его включить, вы будете оставаться в системе даже после того, как закроете браузер и откроете его снова.

Выход из системы


Мы реализовали вход, самое время добавить возможность выхода из системы. Это делается очень просто (файл app/views.py):

@app.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('index'))


Помимо этого нам необходимо добавить соответствующую ссылку в шаблон. Расположим её вверху страницы, рядом с другими навигационными ссылками (файл app/templates/base.html):
<html>
  <head>
    {% if title %}
    <title>{{title}} - microblog</title>
    {% else %}
    <title>microblog</title>
    {% endif %}
  </head>
  <body>
    <div>Microblog:
        <a href="{{ url_for('index') }}">Home</a>
        {% if g.user.is_authenticated() %}
        | <a href="{{ url_for('logout') }}">Logout</a>
        {% endif %}
    </div>
    <hr>
    {% with messages = get_flashed_messages() %}
    {% if messages %}
    <ul>
    {% for message in messages %}
        <li>{{ message }} </li>
    {% endfor %}
    </ul>
    {% endif %}
    {% endwith %}
    {% block content %}{% endblock %}
  </body>
</html>


Видите, насколько это просто! Нам всего лишь надо проверить содержимое g.user, и если текущий пользователь авторизован, добавить ссылку для выхода. Не забывайте, что вместо прямых адресов лучше использовать url_for в таких случаях..

Заключительные слова


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

Для экономии времени вы можете воспользоваться ссылкой и скачать код приложения, включающий в себя все изменения из данной статьи:
Скачать microblog-0.5.zip


До новых встреч!

Мигель