20 września 2009
Udostępnianie plików zalogowanym uzytkownikom
Witajcie,
Wpis ten poświęcony jest problemowi, który nieustannie pojawia się na forach (ostatnio znalazłem go na GoldenLine - jak udostępniać pliki tylko wyznaczonym użytkownikom (dopiero po zalogowaniu, użytkownicy należący do wybranej grupy, itd.). Chciałem wam przedstawić trzy rozwiązania - mam nadzieję, ze któreś z nich będzie dla was optymalne (niestety, rozwiązania idealnego chyba nie ma).
Rozwiązanie pierwsze: Dostęp kontrolowany przez aplikację
Zapytanie od użytkownika trafia do serwera aplikacji (serwer www uruchamia aplikację generującą dynamiczne strony WWW). Aplikacja sprawdza poziom dostępu użytkownika (sprawdza istnienie sesji, łączy się z bazą danych aby pobrać uprawnienia użytkownika, łączy się z bazą danych aby pobrać informacje o pliku) i przesyła plik do użytkownika lub zwraca komunikat o błędzie.
Jeśli zdecydujemy się na udostępnianie plików za pomocą tego rozwiązania, to musimy pamiętać o kilku ważnych sprawach:
- Należy uważać, aby nie zapisywać pliku w pamięci (pobieranie zawartości do pliku, a następnie wysyłanie zawartości tej zmiennej do przeglądarki), a wysyłać dane bezpośrednio do przeglądarki. Bardzo negatywnie wpływa to na wydajność, a dodatkowo przy większych plikach zwyczajnie zacznie nam brakować miejsca w pamięci (chyba, że wysyłamy do przeglądarki małe kawałki plików).
- Wyłączyć buforowanie danych wychodzących do przeglądarki. Powody jak powyżej.
- Należy pamiętać o obsłudze protokołu HTTP/1.1, a w szczególności nagłówków transmisji (typ pliku, rozmiar, wznawianie transmisji).
Polecam lekturę funkcji PHP readfile i komentarze do niej dołączone - niezależnie od ulubionego języka programowania.
Rozwiązanie to będzie działać zawsze, gwarantuje 100% kontrolę dostępu, ale jest najmniej wydajne.
Uzupełnienie:Okazuje się, że pierwszy sposób też może być wydajny, zapraszam do cz. 2 tego wpisu.
Rozwiązanie drugie: Losowa lokalizacja plików
Pliki do pobrania możemy umieścić w przypadkowej lokalizacji, np.:
http://example.org/1/23/45/67/890/ab/cd/e/f/ghijk/kon_rafal.avi
Przypadkowe odgadnięcie lokalizacji jest bardzo mało prawdopodobne, a uruchamianie aplikacji i kontakt z bazą danych nie jest wymagany. Wadą tego rozwiązania jest możliwość przekazania odnośnika innemu użytkownikowi.
Rozwiązanie trzecie: Wykorzystanie Mod Rewrite - RewriteMap
Rozwiązanie to pozwala nam swobodnie określić wymagany poziom wydajności i bezpieczeństwa, dla tego chciałbym je opisać bardziej szczegółowo.
Moduł mod_rewrite posiada dyrektywę (też macie skojarzenia z Unią Europejską [http://pl.wikipedia.org/wiki/Unia_Europejska]? :-) ) RewriteMap, która pozwala zdefiniować mapę przekształceń, czyli a->b, c->d itd. Do dyspozycji mamy następujące typy map:
- Standard Plain Text - naprostsza mapa 1:1 zapisana w pliku tekstowym. np.:
Robert.Boguszewski talen Jan.Kowalski jkowalski
- Randomized Plain Text - podobnie jak w w przypadku Standard Plain Text, ale możemy podać kilka alternatyw, np.:
www www1|www2|www3
- Hash File - podobnie jak powyżej, ale plik nie jest zapisany w postaci czystego tekstu, ale w formacie NDBM - przetwarzanie większych map przekształceń będzie szybsze, ponieważ klucze są indeksowane
- Internal Function - wybieramy funkcję przekształcająca string spośród: toupper, tolower, escape, unescape
- External Rewriting Program - możemy zdefiniować własną funkcję która będzie przekształcała nam adres w jednej postaci w inny adres.
Do rozwiązania problemu ograniczania dostępu dla wybranych użytkowników wykorzystamy właśnie External Rewriting Program, a osoby bardziej zainteresowane tematem zapraszam do oficjalnej dokumentacji.
Przykładowe rozwiązanie
1. Konfiguracja wirtualnego hosta:
Niestety, ale mapy musimy definiować na poziomie konfiguracji serwera - nie ma możliwości dodania mapy na poziomie .htaccess. Do konfiguracji wirualnego hosta dodajemy:
2. Konfiguracja .htaccess:
Założyłem, że wirtualny host odpowiada wyłącznie za udostępnianie plików. Ponieważ w skrypcie mamy bardzo ograniczony dostęp do zmiennych (praktycznie nie ma w nich nic użytecznego), przekazujemy w formacie adresIp_UriPliku wszystkie potrzebne informacje których zamierzam użyć. Przykładowe dane otrzymane przez skrypt na STDIN będą wyglądały następująco:
127.0.0.1_/1/2/3/4/kon_rafal.avi
3. Skrypt dokonujący odkodowania adresu:
Skrypt poniżej działa w oparciu o bardzo prosty algorytm "kodujący": każdy plik znajduje się w 4 poziomach katalogów, a kodowanie polega na dodaniu do numerycznej nazwy katalogu adresu IP użytkownika, np.:
IP użytkownika: 127.0.0.1
Lokalizacja pliku: to /1/2/3/4/kon_rafal.avi
Url dla użytkownika to: /128/2/3/5/kon_rafal.avi
Skrypt jest uruchamiany podczas uruchamiania Apache i komunikuje się z nim za pomocą STDIN i STDOUT. Przez STDIN przesyłany jest ciąg znaków zakończony znakiem nowej linii "\n", a jako odpowiedź oczekiwany jest również taki ciąg znaków.
Nie wiem czy poniższy skrypt wymaga komentarza, ale jeśli będzie potrzebny to chętnie dopiszę.
A teraz wyjaśnienia:
Uwagi do przygotowywania skryptów
- Program uruchamiany jest tylko raz i działa cały czas w tle.
- Należy pamiętać o wyłączeniu buforowania wyjścia (u mnie: ob_implicit_flush(true) i flush());
- KISS (Keep It Simple Stupid) - Skrypt powinien być maksymalnie prosty i przygotowany możliwie w języku niższego rzędu. Przerost formy nad treścią najprawdopodobniej skończy się przeciążeniem serwera. Prosty skrypt, to szybsze działanie i mniejsza szansa popełnienia błędu. Już samo wykorzystanie w powyższym przykładzie PHP wywołuje pewien niesmak, więc bardzo proszę, aby powstrzymać się przed zaprzęganiem Zend Framework[http://framework.zend.com] i bazy danych do obsługi takich skryptów.
- [warn] mod_rewrite: Running external rewrite maps without defining a RewriteLock is DANGEROUS! - taki komunikat zobaczycie w logach Apache jeśli uruchomićie mapowanie przez zewnętrzny skrypt bez definiowania RewriteLock, który służy do synchronizacji pracy skryptu.
Podsumowanie
RewriteMap jest bardzo fajnym rozwiązaniem, które mądrze zastosowane może okazać się naprawdę przydatne. Nie nie przeprowadziłem żadnych testów wydajnościowych, ale mam podstawy przypuszczać, że taki sposób udostępniania zasobów użytkownikom zalogowanym będzie lepszy niż uruchamianie aplikacji. RewriteMap wymaga dostępu do konfiguracji wirtualnego hosta, co niestety ogranicza zakres wykorzystania praktycznie do serwerów dedykowanych.
7 komentarzy
Wszystko co 'o mnie' znajdziesz na tym blogu :-) Więcej informacji w
Yyyyy? A po co jak jest .htaccess?
Myślę, że nie będzie obciążać serwera w żaden zauważalny sposób. Można by go nawet zrobić na Visual Basic :) W końcu, jak sam napisałeś, odpala się tylko raz i działa sobie w tle.
Rozwiązanie pierwsze wcale nie musi być mało wydajne, wystarczy skorzystać z nagłówka HTTP X-ACCEL-REDIRECT (Nginx) lub X-SENDFILE (Apache). Działa to tak, że w aplikacji można sprawdzić uprawnienia do pliku, a następnie zrobić przekierowanie np. X-SENDFILE do konkretnego pliku. W ten sposób omija się problemy z pamięcią przy wczytywanie całego pliku przez skrypt, albo babranie się z przesyłaniem porcjami. I jest wydajne.
Bardzo dobry post, ale też mi się wydaje, że niektóre rozwiązania wcale nie są tak wydajne
wydajne bądź nie, grunt że działają.
@Jaro: Dzięki ! Nie znałem tego nagłówka o jest on naprawdę fajnym rozwiązaniem. Napisałem już cz2 tego wpisu: http://talen.jogger.pl/2009/10/01/udostepnianie-plikow-zalogowanym-cz-2-x-sendfile-prosciej-ju/
@hth: No za pomocą zwykłego .htaccess nie zrobisz tego o czym napisałem...
Uwaga do tekstu. Jest zdanie: "Nie wiem czy poniższy skrypt...", powinno być chyba powyższy skrypt :)
Pozdrawiam
P.S. Nie ma informacji czy wysłało komentarz czy nie :/ Nie wiem czy ze 4 razy przez to nie dodałem tego komentarza :(