Home > other >  Flask login vs. Apache caching
Flask login vs. Apache caching

Time:01-01

I'm a novice web developer, but experienced python programmer, and Apache dolt. Recently, I've been tinkering with hosting a small website and learning my way through some hosting issues, Flask, html templates, etc.

I've followed several Flask tutorials about controlling access to pages with @login_required decorators on access-controlled endpoints and using session to store a logged in k-v pair. This all works perfectly when running locally on Flask's development server on my local machine. However, when I push this onto my hosting service, I'm getting what I believe is cached behavior to many of the access-controlled endpoints and I'm able to see them after logging out (and checking the session data to ensure the key is removed).

Some specifics...

  • Using flask with session for the login info, not flask-login.

  • Hosting on a managed VPS that is using Phusion Passenger as a WSGI interface to Apache

  • I have no config files in use for Apache...just defaults right now.

  • Website is very low traffic... Prolly just me & the bots right now. :)

My passenger_wsgi file:

import sys, os
from datetime import timedelta
INTERP = "/home/<website>/venv1/bin/python3"
#INTERP is present twice so that the new Python interpreter knows the actual executable path
if sys.executable != INTERP: os.execl(INTERP, INTERP, *sys.argv)

# def application(environ, start_response):
#     start_response('200 OK', [('Content-type', 'text/plain')])
#     return ["Hello, world!"]

sys.path.append(os.getcwd())
from app import app as application

After I login, things work as expected. After logout, I can still hit the endpoints that are supposed to be access-controlled and I repeatedly see this "evidence" when I inspect the network traffic in my browser:

Summary
URL: https://<website>/<endpoint>  <---- an endpoint covered by @login_required
Status: 200
Source: Memory Cache

Request
No request, served from the memory cache.

Response
Content-Type: text/html; charset=utf-8
Expires: Wed, 22 Dec 2021 17:14:00 GMT
Date: Wed, 22 Dec 2021 17:04:00 GMT
Content-Length: 23
Cache-Control: max-age=600
Vary: User-Agent
Status: 200 OK
x-powered-by: Phusion Passenger 5.0.30
Server: Apache

So my questions are these...

  1. Is my diagnosis correct that this is Apache caching at work? (Evidence looks compelling... :) )
  2. I'd rather not (at this time) invest effort into moving to flask-login unless that is curative.
  3. Is there a simple way to control this behavior without becoming an expert in apache or Passenger config files or such? I don't mind just turning caching off if that is doable for this low-traffic site, but I'd be interested in what a good solution looks like to in order to educate myself on either controlling the caching or telling apache somehow (?) which endpoints are access controlled, etc.

I humbly submit my question to the folks who deal with these stacks!

CodePudding user response:

Since 5.0, passenger will "helpfully" add cache-control headers to responses it deems 'cachable'.

In order to stop this, your application should add the header Cache-Control: no-store.

To do this globally in Flask as described here:

@app.after_request
def add_header(response):
    # response.cache_control.no_store = True
    if 'Cache-Control' not in response.headers:
        response.headers['Cache-Control'] = 'no-store'
    return response

If you want to be more discriminating and only want to do this for routes which require a login, you could make your own extension to the login_required decorator to do this for your routes which require logins (or a separate decorator entirely)

from flask import make_response
from flask_login import login_required as _login_required

def login_required(f):
    # apply the usual login_required decorator
    decorated = _login_required(f)
    def cache_no_store_login_required_view(*args, **kwargs):
        resp = make_response(decorated(*args, **kwargs))
        # add the cache-control header to the response
        resp.headers['Cache-Control'] = 'no-store'
        return resp
    return cache_no_store_login_required_view

Or as a separate decorator to set no-store...

from functools import wraps
def cache_no_store(f):
    @wraps(f)
    def decorated_view(*args, **kwargs):
        resp = make_response(f(*args, **kwargs))
        resp.headers['Cache-Control'] = 'no-store'

@app.route('/protected')
@cache_no_store
@login_required
def my_route():
    # ...
  • Related