Serve private media with angular, DRF, CORS and token authentication
You can also be interested in:
A long time has passed since my last post, I was fighting hard with a project learning django REST framework and angularjs, and now time has come to share some interesting knowledges.
This entry talks about serving private media from a django REST webservice, developed with django REST framework, to angularjs, in a scenario which uses a token authentication schema and CORS requests.
The problems to face
Let's see the problems we have to deal with.
- Media with django are normally served directly by the web server, so they are out of the authentication/permission logic of the application
- The token authentication schema needs every request to contain an header 'Authentication' part providing the token key (also when requesting files)
- Allowing CORS roughly means to add headers in the response allowing asyncronous communication between different domains.
Let's solve them all
The first thing to do is to find a way to serve media in django relying upon the authentication/permission logic. I've choosen to use a django app named private_media written by Racing Tadpole.
It does exactly what I need, except from the fact that it implements the apache way of doing such thing, while I need the nginx way, but we'll see this will not be a great problem (actually in the github project nginx is covered but installing it with pip I got an older version, so I've written my own nginx server class).
To set up the private media application we need to set some settings in our settings.py
:
PRIVATE_MEDIA_URL = '/private/' if DEBUG: # dev import os PROJECT_PATH = os.path.abspath(os.path.dirname(__file__)) PRIVATE_MEDIA_ROOT = os.path.join(PROJECT_PATH, 'private') PRIVATE_MEDIA_SERVER = 'private_media.servers.DefaultServer' else: # prod PRIVATE_MEDIA_ROOT = '/home/user/my/path/to/private/media' #PRIVATE_MEDIA_SERVER = 'private_media.servers.ApacheXSendfileServer' PRIVATE_MEDIA_SERVER = 'private_media.servers.NginxServer' PRIVATE_MEDIA_PERMISSIONS = 'myapp.permissions.MyPermissionClass'
As you can see, I've commented out the "Apache" part and introduced an "Nginx" part. We'll need to write the NginxServer class and add it to the servers.py
file of the private_media
app.
There's a difference between DEBUG modes, since when DEBUG is TRUE files are served directly from the django development server.
With the PRIVATE_MEDIA_PERMISSIONS setting we set the class containing the authentication logic. It must implement a method called:
has_read_permission(self, request, path)
Which returns True
if the user can access the file, otherwise False
or any exception. In my case such class was a bit more complicated, because I can't rely over request.user
, while I have to get the authentication token directly from the request headers:
class MediaPermissions(object): """ Access permission to all private media files Token key must be read from request object since this view is not routed by the rest framework """ def has_read_permission(self, request, path): try: token_key = request.META['HTTP_AUTHORIZATION'].rsplit(' ', 1)[1] token = Token.objects.get(key=token_key) request.user = token.user res_folder = os.path.split(os.path.split(os.path.split(path)[0])[0])[0] if(res_folder == 'cases'): #... return True # or False elif(res_folder == 'doctors'): return request.user.is_authenticated() raise PermissionDenied('Forbidden') except: raise PermissionDenied('Forbidden')
Then we can add the private_media
app in the INSTALLED_APPS
and add such line in urls.py
... url(r'^', include('private_media.urls')),
In your models.py
, to upload a specific file or image to a private area, use:
from django.db import models from private_media.storages import PrivateMediaStorage class Car(models.Model): photo = models.ImageField(storage=PrivateMediaStorage())
Now we have to set up nginx in order to serve the protected files. We'll use the internal directive, which causes a 404 when the file is accessed from the outside.
server { listen 80; server_name example.com; access_log /var/log/nginx/example.com/access.log; location /static { root /home/example/sites/example/shared; } location /media { root /home/example/sites/example; } location /protected { add_header Access-Control-Allow-Origin *; alias /home/example/sites/example/private; internal; } location / { uwsgi_pass unix:/tmp/uwsgi_example.sock; include /etc/nginx/uwsgi_params; } }
The bold rules:
- prevent the protected files to be accessed directly,
- add the Access-Control-Allow-Origin header in order to enable CORS
- set an alias pointing to the real filesystem path where files reside.
You can see the url matched here is '/protected
', while the PRIVATE_MEDIA_URL
is '/private/
'. The reason is that we have to use different urls, one used to request the file which will be caught by the django app which then runs the authentication logic, and the one used internally by the django app to serve it (and which is instead hidden from the outside thanks to the internal directive).
So the NginxServer class must take into account such urls, and here comes my implementation:
class NginxServer(object): def serve(self, request, path): response = HttpResponse() del response['Content-Type'] nginx_path = os.path.join('/protected', path) response['X-Accel-Redirect'] = nginx_path response['Content-Type'] = '' return response
Just add this class to server.py
file of the private_media
app.
As you can see, we use the X-Accel-Redirect
header in order to access internally the protected file, and we alter the path taking into account what I've just explained above.
So what will happen?
- We request a file http://example.com/private/path/to/file
- the
private_media.urls
module catches the request (which matches thePRIVATE_MEDIA_URL
setting) - The permission class is run to see if the user can access the file
- If the user can access the file, an header is added to the response so that the file is accessed internally and served to the user
What the hell has to do angular with all such things?
Well, I said that I've developed a token authentication schema, in which angular adds an Authentication header to every API request, in order to send the token key. But when you add a link in your html template, pointing directly to a private file, the browser will manage the request and the token will not be included, so the authentication logic will always fail.
The solution here is to request the file through ajax, get a blob and make the browser open it (or display it if it is an image). For this reason I've written two angular directives, one to display protected images and the other to display/download other kinds of file.
(function () { 'use strict'; angular .module('example.utils.directives', []) .directive('imgajax', imgajax) .directive('aajax', aajax); aajax.$inject = ['files']; imgajax.$inject = ['files']; /** * @namespace aajax * @description Directive used to link external files which have to be retrieved using ajax * Private media must be retrieved using ajax calls because the auth token must * be included in the request header. Then the response must be used as a Blob * in order to allow download or browser visualization * @returns {Directive} */ function aajax(files) { return { restrict: 'E', replace: false, transclude: true, scope: { href: "@href" }, template: '<a href ng-transclude></a>', link: function(scope, elem, attrs) { elem.bind('click', function() { files.getMedia(attrs.href).then(function(response) { var blob = response.data; var objectUrl = URL.createObjectURL(blob); window.open(objectUrl); }); }); } }; } /** * @namespace imgajax * @description Directive used to show external images which have to be retrieved using ajax * Private media must be retrieved using ajax calls because the auth token must * be included in the request header. Then the response must be used as a Blob * in order to allow download or browser visualization * @returns {Directive} */ function imgajax(files) { return { restrict: 'E', replace: false, transclude: true, scope: true, template: '<img class="{{ class }}" ng-src="{{ data }}" />', link: function(scope, elem, attrs) { // init value scope.data = 'assets/img/photo_placeholder.png'; scope.class = attrs.class; attrs.$observe('iaSrc', function(value) { if(value !== '') { files.getMedia(value).then(function(response) { var blob = response.data; scope.data = URL.createObjectURL(blob); }); } }); } }; } })();
Both use a simple service to request the protected files:
/** * Performs an ajax get request to get a private media * @param {String} url * @return {Promise} */ function getMedia(url) { return $http({method: 'GET', url: url, responseType: "blob"}); }
Here it is important to set the responseType as blob, in order to use the
URL.createObjectURL(blob)
javascript function which will provide you with an url that opened in a new window displays or downloads the file.
When displaying images, it is enough to set the ng-src
attribute as the received blob data.
You can simply use them this way:
<!-- link to a protected file --> <aajax href="{{ a.file }}">bla bla</aajax> <!-- protected img --> <imgajax ia-src="{{ a.photo }}" class="img-responsive img-circle"></imgajax>
Hope you enjoy this entry, see you in september!
Your Smartwatch Loves Tasker!
Your Smartwatch Loves Tasker!
Featured
Archive
- 2021
- 2020
- 2019
- 2018
- 2017
- Nov
- Oct
- Aug
- Jun
- Mar
- Feb
- 2016
- Oct
- Jun
- May
- Apr
- Mar
- Feb
- Jan
- 2015
- Nov
- Oct
- Aug
- Apr
- Mar
- Feb
- Jan
- 2014
- Sep
- Jul
- May
- Apr
- Mar
- Feb
- Jan
- 2013
- Nov
- Oct
- Sep
- Aug
- Jul
- Jun
- May
- Apr
- Mar
- Feb
- Jan
- 2012
- Dec
- Nov
- Oct
- Aug
- Jul
- Jun
- May
- Apr
- Jan
- 2011
- Dec
- Nov
- Oct
- Sep
- Aug
- Jul
- Jun
- May