#34. Cookie. Session. Cache

Cookie. Session. Cache

enter image description here

HTTP cookie (web cookie, cookie браузера) — это часть данных, которую сервер отправляет в ответе HTTP. Клиент (необязательно) сохраняет файл cookie и возвращает его в последующие запросы. Это позволяет клиенту и серверу совместно использовать состояние. Чтобы задать файл cookie, сервер включает в ответ заголовок Set-cookie. Формат файла cookie — это пара “ключ-значение”.

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

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

Куки добавляются в request/response для хранения абсолютно разных данных. Например, стандартная Django авторизация добавляет куку с данными о пользователя, чтобы можно было определить кто именно делает запрос. Поэтому там и нужны csrf токены в формах или просто токены в REST запросах, так как перехватить значение куки при запросе очень просто, а мы должны быть уверенны, что запрос пришел именно от авторизированного пользователя (Куки хранит информацию, кто это, а токены позволяют проверить, что это был именно этот пользователь).

Для неавторизированного пользователя:
enter image description here

Авторизация с импользованием cookie:
enter image description here

Посмотреть в Chrome куки можно нажав F12 и перейти на вкладку Application. Там же их можно и изменить и удалить:

enter image description here

По одной из версий, термин «куки» (печенье) происходит от «волшебного печенья» — набора данных, которые программа получает и затем отправляет обратно неизменными. Содержимое куки, как правило, не значимо для получателя и не интерпретируется до тех пор, пока получатель не вернёт куки обратно отправителю или другой программе.

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

Session

enter image description here
Cессия — период работы учётной записи пользователя между авторизацией и её завершением.

Cессия( или сеанс) представляет собой запись факта авторизации пользователя и запись времени автоматического завершения работы. Второе можно наблюдать в сеансах, где используется аутентификация по cookies.

Задумайтесь о том, каким образом браузеры следят, что пользователь залогинен, когда страница перезагружается. HTTP запросы не имеют состояний, так как же вы определите, что запрос пришел именно от залогиненого пользователя? Вот почему важны куки — они позволяют вам отслеживать пользователя от запроса к запросу, пока не истечет их срок действия.

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

Подключение сессий

Необходимые конфигурации для подключения сессий выполняются в разделах INSTALLED_APPS и MIDDLEWARE файла проекта settings.py как показано ниже:

INSTALLED_APPS = [
    ...
    'django.contrib.sessions',
    ....

MIDDLEWARE = [
    ...
    'django.contrib.sessions.middleware.SessionMiddleware',
    ....

Как этим пользоваться?

В Django сессия всегда храниться в реквесте, request.session в виде словаря.

Рассмотрим несколько примеров.

>>> request.session[0] = 'bar'
>>>  # subsequent requests following serialization & deserialization
>>>  # of session data
>>> request.session[0]  # KeyError
>>> request.session['0']
'bar'

Данные хранятся в формате JSON, а значит ключи будут преобразованы в строки.

Допустим, вам нужно “запомнить” комментировал ли этот пользователь только что статью, чтобы не позволить написать большое кол-во комментариев подряд. Конечно можно сохранить эти данные в базе, но зачем? Проще воспользоваться сессией:

def post_comment(request, new_comment):
    if request.session.get('has_commented', False):
        return HttpResponse("You've already commented.")
    c = Comment(comment=new_comment)
    c.save()
    request.session['has_commented'] = True
    return HttpResponse('Thanks for your comment!')

Сохраним это состояние в сессии и будем перепроверять именно его.

Допустим, вам нужно хранить сколько времени назад пользователь последний раз совершал действие после логина.

request.session['last_action'] = timezone.now()

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

Если нам нужно воспользоваться сессией вне мест, где есть доступ к реквесту:

from django.contrib.sessions.backends.db import SessionStore

s = SessionStore()

# stored as seconds since epoch because datetimes are not serializable in JSON.
s['last_login'] = 1376587691
s.create()
s.session_key
'2b1189a188b44ad18c35e113ac6ceead'
SessionStore(session_key='2b1189a188b44ad18c35e113ac6ceead')
s['last_login']
1376587691

Мы можем получить сессию по ключу (любая созданная Django сессия автоматически хранит переменную session_key) по которой можно получить нужные нам данные.

Данные в любой сессии хранятся в кодированном виде, чтобы получить все данные сессии, не зная конкретного ключа их можно получить через метод .get_decoded()

s.session_data
'KGRwMQpTJ19hdXRoX3VzZXJfaWQnCnAyCkkxCnMuMTExY2ZjODI2Yj...'
s.get_decoded()
{'user_id': 42}

Сохранение данных в сессии происходит только тогда когда меняется значение request.session :

# Session is modified.
request.session['foo'] = 'bar'

# Session is modified.
del request.session['foo']

# Session is modified.
request.session['foo'] = {}

# Gotcha: Session is NOT modified, because this alters
# request.session['foo'] instead of request.session.
request.session['foo']['bar'] = 'baz'

В последнем случае данные не будут сохранены, т.к. модифицируется не request.session, а request.session['foo']

Это поведение можно изменить, если добавить настройку в settings.py SESSION_SAVE_EVERY_REQUEST = True, тогда запись в сессию будет происходить каждый запрос, а не только в момент изменения.

Второй вариант - явно указать, что данные изменились

# Явное указание, что данные изменены.
# Сессия будет сохранена, куки обновлены (если необходимо).
request.session.modified = True

Для очистки данных сессии можно воспользоваться менедж командой
python manage.py clearsessions

Некоторые настройки можно поменять и перенастроить, как и полностью кастомизировать любые действия с сессиями. Подробнее об этом тут

Данные сессии хранятся в БД, таблица django_session.
enter image description here

Простой пример — получение числа визитов

В качестве примера из реального мира мы обновим view так, чтобы сообщать пользователю количество совершенных им визитов главной страницы из прошлого занятия.

views.py

class NoteListView(LoginRequiredMixin, ListView):
    model = Note
    template_name = 'index.html'
    login_url = 'login/'
    extra_context = {'create_form': NoteCreateForm()}
    paginate_by = 5

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['num_visits'] = self.request.session['num_visits']
        return context

    def get(self, request, *args, **kwargs):
        num_visits = request.session.get('num_visits', 0)
        request.session['num_visits'] = num_visits + 1
        return super().get(request, *args, **kwargs)

И осталось добавить в index.html строку

<p>You have visited this page {{ num_visits }}{% if num_visits == 1 %} time{% else %} times{% endif %}.</p>

enter image description here

Cache

enter image description here
Официальная документация тут

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

Если упростить, то это хранилище для часто запрашиваемых данных.

Предположим мы разрабатываем новостной сайт, и знаем, что новости у нас обновляются раз в час.

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

Для использования кеша, мы можем воспользоваться огромным кол-вом заранее заготовленных решений для кеша.
enter image description here

Memcached

enter image description here
Самый быстрый и эффективный тип кэша, доступный Django, Memcached является кэшем, который полностью располагается в оперативной памяти, он был разработан для LiveJournal и позднее переведён в опенсорс компанией Danga Interactive. Он используется такими сайтами как Facebook и Wikipedia для снижения нагрузки на базу данных и значительного увеличения производительности сайта.

Memcached работает как демон и занимает определённый объём оперативной памяти. Его задачей является представление быстрого интерфейса для добавления, получения и удаления определённых данных в кэше. Все данные хранятся прямо в оперативной памяти, таким образом нет никакой дополнительной нагрузки на базу данных или файловую систему.

Установка Memcached

Linux (детальнее)

$ apt-get install memcached

MacOS

$ brew install memcached

Windows
Скачать тут

И запустить сам процесс, если он еще не запушен
Linux\MacOS
memcached

Windows
memcached.exe

Можно передать дополнительные параметры, например:
p - порт,
m - количество выделяемой ОЗУ,
v - выводить в консоль логи

memcached.exe -m 512 -vvv

После установкам самого Memcached, следует установить его пакет для Python. Существует несколько таких пакетов; два наиболее используемых — pymemcache и pylibmc.

pip install pymemcache

Для использования Memcached с Django:

  • Установите в settings.py BACKEND в django.core.cache.backends.memcached.PyMemcacheCache или django.core.cache.backends.memcached.PyLibMCCache (зависит от выбранного пакета).
  • Определите для LOCATIONзначение ip:port (где ip — это IP адрес, на котором работает демон Memcached, port — его порт) или unix:path (где path является путём к файлу-сокету Memcached).

Примеры настройки кеша:

Хранение данных с использованием PyMemcacheCache (cтандартный кеш для Django), на отдельном для этого порту 11211:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
        'LOCATION': '127.0.0.1:11211',
    }
}

Хранение в файле сокета (временный файл хранилище в юникс системах):

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
        'LOCATION': 'unix:/tmp/memcached.sock',
    }
}

Хранение на нескольких серверах, для уменьшения нагрузки:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
        'LOCATION': [
            '172.19.26.240:11211',
            '172.19.26.242:11211',
        ]
    }

Можно хранить кеш прям в базе данных, для этого нужно указать таблицу в которую складывать кеш:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
        'LOCATION': 'my_cache_table',
    }
}

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

python manage.py createcachetable

Можно хранить кеш в обычном файле:
Linux/MacOS

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
        'LOCATION': '/var/tmp/django_cache',
    }
}

Windows:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
        'LOCATION': os.path.join(BASE_DIR, 'mysite_cache'),
    }
}

Можно хранить в оперативной памяти сервера:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'unique-snowflake',
    }
}

Есть упрощенная схема кеширования для разработки:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
    }
}

Как и всё остальное, кеш можно кастомизировать написав собственные классы для управления кешем:

CACHES = {
    'default': {
        'BACKEND': 'path.to.backend',
    }
}

Любой тип кеширования поддерживает большое кол-во доп настроек, подробно о которых в документации.

Как же этим пользоваться?

Существует два основных способа использовать кеш.

Кешировать весь сайт, или кешировать конкретную вью.

Для того, чтобы кешировать весь сайт, нужно добавить два мидлвара (Как это работает, на следующем занятии), до и после CommonMiddleware (это важно, иначе работать не будет):

В settings.py

MIDDLEWARE = [
    ...
    'django.middleware.cache.UpdateCacheMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.cache.FetchFromCacheMiddleware',
    ...
]

Время кеширование или ограничения на кеш выставляются через переменные settings.py, подробно в документации.

Для того, чтобы кешировать отдельный метод или класс используется декоратор cache_page

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def my_view(request):
    ...

В скобках указывается время, которое кеш должен хранится, обычно записывается в виде умножения на секунды\минуты, для простоты чтения (15*60 это 15 минут, никакой разницы от того чтобы записать 900, но так проще воспринимать на вид).

Чаще всего декоратор используется в урлах:

from django.views.decorators.cache import cache_page

urlpatterns = [
    path('foo/<int:code>/', cache_page(60 * 15)(my_view)),
]

Для кеширования class base view кешируется весь класс:

from django.views.decorators.cache import cache_page

url(r'^my_url/?$', cache_page(60 * 60)(MyView.as_view())),

Можно указать конкретную функцию в классе, но, так как мы не можем задекорировать класс, необходимо использовать method_decorator (документация), который принимает две функции:

import datetime

import requests
from django.utils.decorators import method_decorator # NEW
from django.views.decorators.cache import cache_page # NEW
from django.views.generic import TemplateView


@method_decorator(cache_page(60), name='dispatch') # NEW
class ApiCalls(TemplateView):
    template_name = 'apicalls/home.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['content'] = 'Results received!'
        context['current_time'] = datetime.datetime.now()
        return context

Так же можно закешировать часть темплейта при помощи темплейт тега cache:

{ % load cache %}
{ % cache 500 sidebar %}
..sidebar..
{ % endcache %}

Функции низкого уровня для кэширования

Для более тонкой настройки и использования механизма кэширования в Django имеются весьма полезные функции, которые составляют уровень API для кэширования. Основные из них, следующие:

  • cache.set() – сохранение произвольных данных в кэш по ключу;
  • cache.get() – выбор произвольных данных из кэша по ключу;
  • cache.add() – заносит новое значение в кэш, если его там еще нет (иначе данная операция игнорируется);
  • cache.get_or_set() – извлекает данные из кэша, если их нет, то автоматически заносится значение по умолчанию;
  • cache.delete() – удаление данных из кэша по ключу;
  • cache.clear() – полная очистка кэша.
from django.core.cache import cache

cache.set('my_key', 'hello, world!', 30)
cache.get('my_key')
'hello, world!'
# Wait 30 seconds for 'my_key' to expire...
cache.get('my_key')
None

cache.set('add_key', 'Initial value')
cache.add('add_key', 'New value')
# .add() сработает только если в указанном ключе ничего не было
cache.get('add_key')
'Initial value'

cache.get_or_set('my_new_key', 'my new value', 100)
'my new value'

import datetime

cache.get_or_set('some-timestamp-key', datetime.datetime.now)
datetime.datetime(2014, 12, 11, 0, 15, 49, 457920)

cache.set('a', 1)
cache.set('b', 2)
cache.set('c', 3)
cache.get_many(['a', 'b', 'c'])
{'a': 1, 'b': 2, 'c': 3}

cache.set_many({'a': 1, 'b': 2, 'c': 3})
cache.get_many(['a', 'b', 'c'])
{'a': 1, 'b': 2, 'c': 3}

cache.delete('a')
cache.delete_many(['a', 'b', 'c'])

cache.clear()

cache.touch('a', 10)  # Обновить время хранения

Например, необходимо закешировать запрос в бд заметок из прошлого занятия.
Для этого переопределим метод get_queryset:

class NoteListView(LoginRequiredMixin, ListView):
    model = Note
    template_name = 'index.html'
    login_url = 'login/'
    extra_context = {'create_form': NoteCreateForm()}
    paginate_by = 5

    def get_queryset(self):
        notes = cache.get('notes')
        if not notes:
            notes = self.model.objects.all()
            cache.set('notes', notes, 60)
        return notes

Практика:

  1. Пользователь открывает одну и туже страницу. Каждый четвертый раз когда он открывает страницу добавте вверху надпись, “Это был 4-ый раз”, если обновить страницу еще раз, то счёт 4-ех открытий начинаем сначала.

  2. Множество пользователей открывает одну и туже страницу, каждый 10-ый кто открывает страницу должен видеть надпись, " Вы наш 10-ый покупатель" (Если один пользователь открыл 10 раз, это тоже подходит, один пользователь 6 раз и еще один 4 раза, тоже ок)

Литература (что почитать)

  1. Включаем кэширование данных + видео