Django Caching for Authenticated Users Only

Question:

Question

In Django, how can create a single cached version of a page (same for all users) that’s only visible to authenticated users?

Setup

The pages I wish to cache are only available to authenticated users (they use @login_required on the view). These pages are the same for all authenticated users (e.g. no need to setup vary_on_headers based on unique users).

However, I don’t want these cached pages to be visible to non-authenticated users.

What I’ve tried so far

  • Page level cache (shows pages intended for logged in users to non-logged in users)
  • Looked into using vary_on_headers, but I don’t need individually cached pages for each user
  • I checked out template fragment caching, but unless I’m confused, this won’t meet my needs
  • Substantial searching (seems that everyone wants to do the reverse)

Thanks!

Example View

@login_required
@cache_page(60 * 60)
def index(request):
    '''Display the home page'''
    return render(request, 'index.html')

settings.py (relevant portion)

# Add the below for memcache
MIDDLEWARE_CLASSES += (
    'django.middleware.cache.UpdateCacheMiddleware',
    'django.middleware.cache.FetchFromCacheMiddleware',
)

# Enable memcache
# https://devcenter.heroku.com/articles/memcache#using_memcache_from_python
CACHES = {
    'default': {
        'BACKEND': 'django_pylibmc.memcached.PyLibMCCache'
    }
}

Solution

Based on the answer by @Tisho I solved this problem by

  1. Creating a decorators.py file in my app
  2. Adding the below code to it
  3. Importing the function in views.py
  4. Applying it as a decorator to the views I wanted to cache for logged in users only

decorators.py

from functools import wraps
from django.views.decorators.cache import cache_page
from django.utils.decorators import available_attrs


def cache_on_auth(timeout):
    def decorator(view_func):
        @wraps(view_func, assigned=available_attrs(view_func))
        def _wrapped_view(request, *args, **kwargs):
            if request.user.is_authenticated():
                return cache_page(timeout)(view_func)(request, *args, **kwargs)
            else:
                return view_func(request, *args, **kwargs)
        return _wrapped_view
    return decorator

For logged in users, it would cache the page (or serve them the cached page) for non-logged in users, it would just give them the regular view, which was decorated with @login_required and would require them to login.

Asked By: Jeff

||

Answers:

The default cache_page decorator accepts a variable called key_prefix. However, it can be passed as a string parameter only. So you can write your own decorator, that will dynamically modify this prefix_key based on the is_authenticated value. Here is an example:

from django.views.decorators.cache import cache_page

def cache_on_auth(timeout):
    def decorator(view_func):
        @wraps(view_func, assigned=available_attrs(view_func))
        def _wrapped_view(request, *args, **kwargs):
            return cache_page(timeout, key_prefix="_auth_%s_" % request.user.is_authenticated())(view_func)(request, *args, **kwargs)
        return _wrapped_view
    return decorator

and then use it on the view:

@cache_on_auth(60*60)
def myview(request)

Then, the generated cache_key will look like:

cache key:   
views.decorators.cache.cache_page._auth_False_.GET.123456.123456

if the user is authenticated, and

cache key:   
views.decorators.cache.cache_page._auth_True_.GET.789012.789012

if the user is not authenticated.

Answered By: Tisho

I’d advice against using the cache middleware if you want fine tuning of your caching abilities.

However, if you do want to persist keeping it, you could try something like (not saying it would work as is, but something similar to it):

@never_cache
def dynamic_index(request):
    # do dynamic stuff

def cached_index(request):
    return dynamic_index(request)

@never_cache
def index(request):
    
    if request.user.is_authenticaded():
        return cached_index(request)

    return dynamic_index(request)
    

Worst case scenario, you can use cache.set('view_name', template_rendering_result), and cache.get, to just cache the HTML manually.

Answered By: e-satis

If the @wrap decorator in the @Tisho answer makes your brain hurt, or if an explicit solution is better than an implicit one, here’s a simple procedural way to serve different cache results:

from django.views.decorators.cache import cache_page

def index(request):
    """
    :type request: HttpRequest
    """
    is_authenticated = request.user.is_authenticated()
    if is_authenticated:
        return render_user(request)
    else:
        return render_visitor(request)

@cache_page(5, key_prefix='user_cache')
def render_user(request):
    print 'refreshing user_cache'
    return render(request, 'home-user.html', {})

@cache_page(10, key_prefix='visitor_cache')
def render_visitor(request):
    print 'refreshing visitor_cache'
    return render(request, 'home-visitor.html', {})
Answered By: Bryce

I know this is a very old question, but we have a new alternative to solve this.

You can use the decorator cache_control passing private as True to protect your data.

Answered By: Rodrigo Braga

Here’s an updated version of @Tisho’s answer that works for Django 3.0 (the old version no longer works since Python 2 support was dropped in Django 3.0):

from django.views.decorators.cache import cache_page
from functools import wraps, WRAPPER_ASSIGNMENTS


def cache_on_auth(timeout):
    """
    Caches views up to two times: Once for authenticated users, and
    once for unauthenticated users.
    """
    def decorator(view_func):
        @wraps(view_func, assigned=WRAPPER_ASSIGNMENTS)
        def _wrapped_view(request, *args, **kwargs):
            result = cache_page(
                timeout,
                key_prefix=(f"_auth_{request.user.is_authenticated}_"))
            return result(view_func)(request, *args, **kwargs)
        return _wrapped_view
    return decorator
Answered By: n_moen