Ограничиваем доступ к файлам на сервере

Владимир | | CodeIgniter, PHP, Web разработка.

file download logo

В этой статье я хочу рассказать о нескольких вариантах ограничения доступа к файлам на сервере.

Т.е. мы сделаем так, чтобы посетитель перед загрузкой в обязательном порядке выполнил какие-то действия (например, вводил captcha).

Существует два основных варианта решения задачи.

1) Скачиванием файлов будет управляет PHP скрипт.

2) Скачивание происходит через web сервер, но только в том случае, если у посетителя есть соответствующее разрешение.

Прежде всего, разберем достоинства и недостатки обоих методов.

Первый вариант. Вообще-то единственное его достоинство – это возможность реализации на shared-хостинге.

Зато недостатки очень серьезные. Файл отправляется пользователю через PHP скриты, т.е. работают все ограничения, действующие на них. Например, если для скриптов установлено максимальное время работы 15 сек, то значит, файл должен быть загружен за это время. Тоже самое и с максимально допустимым объемом памяти.

Проще говоря, никаких больших файлов отдавать таким способом не получится.

К тому же скрипты будут потреблять память, что увеличит нагрузку на сервер и скажется на производительности.

Второй вариант выглядит значительно привлекательнее. Ситуация с достоинствами и недостатками полностью противоположная. Отдавать файлы будет Apache без использования PHP. Но чтобы реализовать проверку посетителей, нужно иметь доступ к конфигу сервера (httpd.conf).

Рассмотрим общий принцип загрузки файла


file_download_thumb

Как видите, после запроса файла посетителем, сервер проверяет, можно ли отдавать ему данный файл. Реализовать эту проверку можно двумя способами:

1) с помощью PHP скрипта (конечно, это может быть скрипт на любом другом языке);

2) с помощью встроенных средств web сервера Apache (речь, конечно, идет о mod_rewrite).

В зависимости от выбранного варианта проверки меняется и способ отправки файла посетителю. В первом случае отправку придется выполнять с помощью PHP скрипта, во втором – Apache сам будет отправлять файл.

Теперь посмотрим, что должно происходить, если доступ для данного посетителя к файлу закрыт.

В этом случае необходимо сформировать страницу с описанием ошибки и отправить её посетителю. Что именно будет содержать эта страница – зависит от требований к конкретному сайту и большой роли для этого примера не играет.

Гораздо важнее определиться, при выполнении каких условий посетитель получит доступ к файлу.

Для этого примера я выбрал два параметра: IP адрес и время доступа к файлу.

Принцип работы следующий.

1) Посетитель заходит на сайт и выбирает нужный файл.

2) Вводит captcha (captcha я взял исключительно ради примера, её можно заменить какой-нибудь другой формой).

3) Скрипт создает запись с IP адресом посетителя, названием файла и временем, в течении которого можно скачивать файл. Запись может находиться либо в БД, либо в обычном текстовом файле, на данном этапе это роли не играет.

4) Либо web сервер, либо PHP скрипт отправляют посетителю запрошенный файл.

5) Если посетитель повторно запрашивает файл, то сервер ищет его IP, проверяет время доступа и, в зависимости от результатов, отправляет файл или страницу с ошибкой.

Теперь рассмотрим варианты реализации.

Начнем с более простого варианта – отправки через PHP скрипт.

Прежде всего, немного уточним задачу.
Будем считать, что все файлы, которые посетитель будет скачивать находятся в папке archive.
В качестве движка сайта используем фреймворк CodeIgniter, web сервер – Apache.

На рисунке показан алгоритм работы сайта.

php_file_download_thumb

Обратите внимание, что если посетитель напрямую запрашивает файл (обращается к папке archive), то сервер все равно передает управление движку сайта, который отправляет 404-ую ошибку.

О фреймворке CodeIgniter я уже неоднократно рассказывал, поэтому останавливаться на его настройке и принципах работы не буду. В этом примере фреймворк используется только для демонстрации, чтобы сократить объем кода, и вы можете спокойно заменить его любым другим.

Контроллер

<?php
/**
 * Управление скачиванием файлов
 *
 * @version 0.1
 * @link http://www.simplecoding.org
 * @author Стаценко Владимир <vova_33@gala.net>
 */
class Main extends Controller {

	function Main() {
		parent::Controller();
		$this->load->helper('file');
		$this->load->config('downloader');
	}

	/**
	 * Формирует главную страницу со списком файлов
	 */
	function index() {
		$pageData['title'] = 'Управление скачиванием файлов';
		$pageData['files'] = get_filenames($this->config->item('filesdir'));
		
		$this->load->view('header', $pageData);
		$this->load->view('filelist');
		$this->load->view('footer');
	}
	
	/**
	 * Формирует страницу с каптчей, которую должен 
	 * ввести посетитель чтобы скачать файл
	 *
	 * @param $fileName - имя файла, который пользователь хочет загрузить
	 */
	function confirm($fileName = '') {
		session_start();
		
		//если посетитель ввел каптчу
		if ($this->input->post('captcha')) {
			//проверяем ответ на вопрос
			if ((int)$this->input->post('captcha') === $_SESSION['captcha']) {
				//создаем временную ссылку
				//сохраняем её в базе
				$this->load->model('mfilelink');
				
				$fileLink = $this->mfilelink->createLink($fileName);
				//если ссылка создана и сохранена
				if ($fileLink) {
					//отправляем посетителя на страницу загрузки файла
					redirect('main/download/'.$fileLink, 'refresh');
					exit;
				}
				//если нет - создаем сообщение об ошибке и перенаправляем посетителя на страницу
				//со списком файлов
				else {
					$this->session->set_flashdata('error', 'Извините, этот файл не может быть загружен.');
					redirect('main/index', 'refresh');
					exit;
				}
			}
			//если посетитель неправильно ввел каптчу
			else {
				$this->session->set_flashdata('error', 'Вы неправильно ответили на вопрос. Попробуйте ещё раз.');
				redirect('main/confirm/'.$fileName, 'refresh');
				exit;
			}
		}
		
		//игнорируем запросы к несуществующим файлам
		if (!get_file_info($this->config->item('filesdir').'/'.$fileName)) {
			redirect('main/index', 'refresh');
			exit;
		}
		
		$this->load->helper('form');
		
		//подготавливаем данные для каптчи
		$x = mt_rand(1, 9);
		$y = mt_rand(1, 9);
		
		$_SESSION['captcha'] = $x + $y;
		
		//формируем страницу
		$pageData['title'] = 'Управление скачиванием файлов';
		$pageData['fileName'] = $fileName;
		$pageData['x'] = $x;
		$pageData['y'] = $y;
		
		$this->load->view('header', $pageData);
		$this->load->view('confirm');
		$this->load->view('footer');
	}
	
	/**
	 * Этот метод проверяет временную ссылку
	 *
	 * @param $fileLink - временная ссылка на файл
	 */
	function download($fileLink) {
		//проверка актуальности ссылки (по БД)
		$this->load->model('mfilelink');
		$fileName = $this->mfilelink->checkFileLink($fileLink);
		//если проверка прошла - отправляем файл
		if ($fileName) {
			$this->_sendFile($fileName);
		}
		else {
			//в протином случае - отправляем посетителя на страницу со списком файлов
			$this->session->set_flashdata('error', 'Извините, эта ссылка нерабочая.');
			redirect('main/index', 'refresh');
		}
	}
	
	/**
	 * Этот метод отправляет файл посетителю
	 * (вызывается из $this->download после проверки временной ссылки)
	 *
	 * @param $fileName имя файла
	 */
	function _sendFile($fileName) {
		$this->load->helper('download');
		//читаем содержимое файла
		$data = file_get_contents($this->config->item('filesdir').'/'.$fileName);
		//отправляем его посетителю
		force_download($fileName, $data);
	}
}
?>

Продолжение на следующей странице >>>

Страница: 1 2 3

  • qnikst

    ну не знаю, наверное я не модный но существует такой вариант:
    1). все файлы лежат в папке филес вне корневого раздела, доступ к которому открыт для пхп через open_basedir. В базе данных хранятся их атрибуты, чтобы не тратить время на мученье hdd.
    2). создаётся файл php файл вида data в корне и прописывается в htaccess то что это скрипт.
    3). доступ к файлам осуществяется через /data/path/filename:
    в файле data есть следующие строки:

    $url_array = explode(«/», $_SERVER[«REQUEST_URI»]);//получили путь
    if (/* условие того, что юзер подтвердил файл капчу и т.д. */){
    $dev = mysql_query(«select type,name,leght …»);
    //получение данных
    header(«Content-Type: application/x-force-download;name='name'»);
    header(«Content-Length: $length»);
    // и т.д.
    @readfile(dirname($_SERVER['DOCUMENT_ROOT']).'/files/'.$filename);
    }else{
    вывод страницы где пользователю надо что-то ввести
    }

    В общем чтобы не было лишний слов, скажу, что все проверки на валидность и прочие естественные вещи я не писал, только общую суть

    Может метод и является странным но имеет право на жизнь

    • Согласен, список файлов лучше хранить в базе, а не читать каждый раз с винчестера. Хотя если страница кэшируется, то разница небольшая.

      Вынос файлов за пределы DOCUMENT_ROOT тоже вариант. Можно не думать о том как запретить доступ к файлам.
      Но остается проблема — высокое потребление ресурсов при отдаче через PHP скрипт.

    • Gad666999

      для кого вообще в самом начале писали о потреблении ресурсов и их ограничение через php. readfile читает файл и в буфер кидает, весело будет 2гб читать.
      А такой способ в статье тоже не особо, представьте у вас куча поситителей и куча файлов, мод реврайт будет свой массив всё время юзать в том время когда текстовой файл разростёться до огромной базы ip адресов

      • Высоконагруженная система — это в любом случае проблема. Но если нужно контролировать раздачу (запретить кому-то доступ), то вести учет пользователей (их ip или каких-то других данных) все-равно придется.
        А устаревшие ip можно удалять отдельным скриптом (его можно запускать по расписанию).

    • Alex

      Ивините, но после того как я скачал архив с первым вариантом и выполнил инструкции написанные в файле install. Я что-то не могу понять, что написать в файле index.php, что бы как то проверить действие вашего скрипта…

      • Вы ничего не должны писать в index.php. После установки фреймворка CI и скрипта с примером, просто заходите на ваш локальный сайт через браузер.

  • qnikst

    ну не знаю, наверное я не модный но существует такой вариант:
    1). все файлы лежат в папке филес вне корневого раздела, доступ к которому открыт для пхп через open_basedir. В базе данных хранятся их атрибуты, чтобы не тратить время на мученье hdd.
    2). создаётся файл php файл вида data в корне и прописывается в htaccess то что это скрипт.
    3). доступ к файлам осуществяется через /data/path/filename:
    в файле data есть следующие строки:

    $url_array = explode(«/», $_SERVER[«REQUEST_URI»]);//получили путь
    if (/* условие того, что юзер подтвердил файл капчу и т.д. */){
    $dev = mysql_query(«select type,name,leght …»);
    //получение данных
    header(«Content-Type: application/x-force-download;name='name'»);
    header(«Content-Length: $length»);
    // и т.д.
    @readfile(dirname($_SERVER['DOCUMENT_ROOT']).'/files/'.$filename);
    }else{
    вывод страницы где пользователю надо что-то ввести
    }

    В общем чтобы не было лишний слов, скажу, что все проверки на валидность и прочие естественные вещи я не писал, только общую суть

    Может метод и является странным но имеет право на жизнь

    • Согласен, список файлов лучше хранить в базе, а не читать каждый раз с винчестера. Хотя если страница кэшируется, то разница небольшая.

      Вынос файлов за пределы DOCUMENT_ROOT тоже вариант. Можно не думать о том как запретить доступ к файлам.
      Но остается проблема — высокое потребление ресурсов при отдаче через PHP скрипт.

  • Спасибо помогло!

  • Спасибо помогло!