#34. Cookie. Session. Cache

Cookie. Session. Cache

HTTP cookie (web cookie, cookie браузера) - це частина даних, яку сервер надсилає у відповіді HTTP. Клієнт (необов’язково) зберігає файл cookie і повертає його в наступні запити. Це дає змогу клієнту і серверу спільно використовувати стан. Щоб задати файл cookie, сервер включає у відповідь заголовок Set-cookie. Формат файлу cookie - це пара “ключ-значення”.

Куки зберігаються в браузері користувача до закінчення якогось певного терміну. Вони можуть бути застосовані практично для будь-якого завдання, але найчастіше їх використовують, щоб зберегти користувача в тому самому місці веб-сторінки, якщо він втратить інтернет-з’єднання, або, щоб зберігати прості налаштування відображення сайту.

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

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

Для неавторизованого користувача:

Авторизація з використанням cookie:

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

За однією з версій, термін “кукі” (печиво) походить від “чарівного печива” - набору даних, які програма отримує і потім відправляє назад незмінними. Вміст кукі, як правило, не значущий для одержувача і не інтерпретується доти, доки одержувач не поверне кукі назад відправнику або іншій програмі.

У реальному житті кукі можна порівняти з номерком у гардеробі: номерок не має власної цінності, але він дає змогу отримати натомість правильне пальто.

Session


Сесія - період роботи облікового запису користувача між авторизацією та її завершенням.

Сесія (або сеанс) являє собою запис факту авторизації користувача і запис часу автоматичного завершення роботи. Друге можна спостерігати в сеансах, де використовується аутентифікація за 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.

Простий приклад - отримання числа візитів

Як приклад із реального світу ми оновимо 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>

введіть тут опис зображення

Cache


Офіційна документація тут

Кеш - проміжний буфер зі швидким доступом до нього, що містить інформацію, яка може бути запитана з найбільшою ймовірністю. Доступ до даних у кеші здійснюється швидше, ніж вибірка вихідних даних із повільнішої пам’яті або віддаленого джерела, проте її обсяг істотно обмежений порівняно зі сховищем вихідних даних.

Якщо спростити, то це сховище для часто запитуваних даних.

Припустимо, ми розробляємо новинний сайт, і знаємо, що новини у нас оновлюються раз на годину.

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

Для використання кеша, ми можемо скористатися величезною кількістю заздалегідь заготовлених рішень для кеша.

Memcached


Memcached - швидший і ефективний тип кешу, доступний Djangо. Є кешем, що повністю розташовується в оперативній пам’яті, його розробили для 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 - кількість RAM, що виділяється
  • v - виводити в консоль логи
memcached.exe -m 512 -vvvv

Після встановлення самого 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 (стандартний кеш для 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.locmem.LocMemCache',
        'LOCATION': 'unique-snowflake',
    }
}

Як і все інше, кеш можна кастомізувати, написавши власні класи для управління кешем:

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. Django’s cache framework