PHP w trybie CGI – autentyfikacja HTTP – system logowania

Autentyfikacja HTTP

Ostatnio, podczas przenosin dość starej aplikacji na nowy serwer natknąłem się na problem z powodu wykorzystania autentyfikacji HTTP. Problem okazał się błachy, jednak w momencie przenosin i pierwszych testów miałem problem z diagnozą przyczyn takiego stanu rzeczy.

Opis sytuacji

Na wstępie krótko opiszę sytuację, objawy jakie się pojawiły. W dalszej części pokażę sposób poradzenia sobie z problemem.

Na starym serwerze PHP działał sobie jako zwykły moduł apache. Jako, że aplikacja była pisana dawno temu, wówczas system logowania oparty na autentyfikacji HTTP wydawał się wystarczający. Tym bardziej, iż była to aplikacja intranetowa. Po przenosinach na nowy serwer okazało się, że okienko autentyfikacji nie przestawało wyskakiwać nawet po podaniu poprawnego loginu i hasła. Na pierwszy ogień poszło sprawdzenie poprawności przeniesionej bazy danych, użytkownika bazy, zawartości tabel. Okazało się, że wszystko jest ok, zatem na warsztat poszedł test systemu logowania.

System logowania oparty o autentyfikację HTTP

System logowania był bardzo prosty. Pobrane dane z $_SERVER['PHP_AUTH_USER'] oraz $_SERVER['PHP_AUTH_PW'] były porównywane z tymi w bazie danych. Jeśli autoryzacja nie przebiegała pomyślnie wyświetlany był odpowiedni komunikat i po trzech sekundach strona była odświeżana z ponownym wyświetleniem okienka autentyfikacji.

Skrócony na potrzeby przykładu plik index.php.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
if (empty($_SERVER['PHP_AUTH_USER'])) {
	header('WWW-Authenticate: Basic realm="Logowanie"');
	header('HTTP/1.0 401 Unauthorized');
	sleep(3);
	exit;
}else {
	DEFINE ('LOGINSALT','TESTOWYSALTLOGOWANIA');
 
	require_once 'class/db/db.class.php';
	require_once 'class/login.class.php';
 
	$login_string = md5($_SERVER['PHP_AUTH_USER'].LOGINSALT.md5($_SERVER['PHP_AUTH_PW']));
	$logowanie = new login($login_string);
	$logowanie->login();
	if ($logowanie->isLogged()){
		// ok
	} else {
		include 'notLogged.php';
		sleep(3);
		header('WWW-Authenticate: Basic realm="Logowanie"');
		header('HTTP/1.0 401 Unauthorized');
		exit;
	}
}
?>

Jak widać jest to bardzo proste, wręcz banalne rozwiązanie. Z tą różnicą względem podstawowych systemów opartych na .htpasswds, że dane użytkowników są przechowywane w bazie danych.

Sama klasa logowania też jest bardzo prosta. Jednak w tym wypadku, doskonale spełniała swoje założenia. Wersja odchudzona:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<?php 
/**
 * Klasa logowania
 * 
 * @param string $login_string - Ciąg danych logowania md5(login+salt+md5(hasło))
 */
class login{
	/**
	 * Ciąg logowania
	 *
	 * @var string
	 */
	private $_login_string;
	/**
	 * Stan zalogowania
	 *
	 * @var bool $_logged 
	 */
	private $_logged = false;
	/**
	 * Id zalogowanego użytkownika
	 *
	 * @var int
	 */
	private $_id;
	/**
	 * Konstruktor klasy logowania
	 *
	 * @param string $login_string
	 */
	public function __construct($login_string){
		$this->_login_string = $login_string;
	}
	/**
	 * Logowanie użytkownika
	 *
	 * @return bool
	 */
	public function login(){
		$db = new db_class(DB_HOST,DB_LOGIN,DB_PASS,DB_NAME);
		$query = "SELECT `ID` FROM `LISTA_USR` WHERE md5(CONCAT(`LOGIN`,'".LOGINSALT."',`PASS`))='".$this->_login_string."' ;";
		$db->sqlQuery($query);
		if ($db->sqlNumRows()>0){
			$row = $db->getRow();
			$this->_id = $row['ID'];
			$this->_logged = true;
			return true;
		}
		return false;
	}
	/**
	 * Sprawdzenie czy zalogowany
	 *
	 * @return bool $this->_logged
	 */
	public function isLogged(){
		return $this->_logged;
	}
	/**
	 * Pobranie ID zalogowanego użytkownika
	 *
	 * @return int $this->_id;
	 */
	public function getUsrId(){
		return $this->_id;
	}
}
?>

Rozwiązanie proste jak budowa cepa. Nie ma się tu nad czym rozwodzić. Niestety w trybie CGI się nie sprawdza, gdyż w zmiennej $_SERVER nie ma zmiennych $_SERVER['PHP_AUTH_USER'] oraz $_SERVER['PHP_AUTH_PW'].

Rozwiązanie

Rozwiązaniem na brak wymaganych zmiennych okazał się odpowiedni wpis w .htaccess, który tworzył nową zmienną w tablicy $_SERVER, do której przypisywał ciąg logowania 1 .

RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]

Teraz wystarczyło tylko obsłużyć ten ciąg po stronie PHP. Dane zapisane do zmiennej $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] są kodowane przy wykorzystaniu base64. W celu wydobycia loginu i hasła, trzeba pobrać fragment tego zmiennej, zdekodować i rozbić po znaku dwukropka.

1
2
3
<?php
list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) = explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
?>

To by było na tyle. Strona się uruchomiła, login i hasło zostało przyjęte, użytkownik zalogowany. Ale, ale… Byłoby zbyt prosto. Po kliknięciu na dowolny odnośnik (było wykorzystane przepisywanie adresów) w oczy rzucał się błąd 404.

Okazało się, że nasza regułka włączająca autentyfikację HTTP była zbyt zachłanna i przyjmowała na siebie całą pracę, nie dopuszczając innych regułek do pracy.

W związku z tym trzeba było opracować inne rozwiązanie, takie, które dawało obsługę przyjaznych adresów oraz $_SERVER['PHP_AUTH_USER'] i $_SERVER['PHP_AUTH_PW']

Finalne rozwiązanie

Jak człowiek siądzie i pomyśli, to wymyśli. Pierwszy pomysł był dobry, ale zabrakło w nim podstawowej rzeczy, warunku, dla którego miał być aktywowany. Poza tym umieszczenie go na początku pliku .htaccess też nie było mądre. Po przeniesieniu reguły na koniec pliku, oraz dodaniu odpowiedniego warunku wszystko zaczęło działać jak należy.

RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]

Dodatkowo należało przerobić skrypt wyciągający nazwę użytkownika i hasło ze zmiennej $_SERVER['REDIRECT_HTTP_AUTHORIZATION'], gdyż poprzedni skrypt rozsypywał się, gdy użytkownik miał hasło zawierające znak dwukropka.

1
2
3
4
5
6
7
8
<?php
if(isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && empty($_SERVER['PHP_AUTH_USER'])) { 
    $auth_params = explode(":" , base64_decode(substr($_SERVER['REDIRECT_HTTP_AUTHORIZATION'], 6))); 
    $_SERVER['PHP_AUTH_USER'] = $auth_params[0]; 
    unset($auth_params[0]); 
    $_SERVER['PHP_AUTH_PW'] = implode('',$auth_params); 
}
?>

Podsumowanie

Jak się okazało, obeszło się bez przepisywania systemu logowania na zwykły formularz. Poza tym nie zawsze należy wierzyć temu co jest napisane w manualu:

Uwierzytelnianie HTTP jest obsługiwana przez PHP tylko wtedy, gdy PHP pracuje jako moduł Apache’a, nie jest dostępna w trybie CGI. 2

Czyba czas najwyższy przepisać część działających aplikacji na „dobry”, działający w różnych konfiguracjach system logowania. Systemy oparte na autentyfikację HTTP są dobre dla wbudowanych rozwiązań (routery, zarządzalne switch’e, etc), zwykłe aplikacje powinny mieć normalny system uwierzytelniania i autoryzacji.

Źródła

  1. HTTP Authentication with PHP running as CGI/SuExec []
  2. HTTP authentication with PHP []
 

Przeczytaj także

Komentarze: 2

Dodaj komentarz »

 
 
 

Miło wiedzieć :). Dodam, że takich różnic mod_php/CGI/FastCGI jest więcej i dotyczą one szeregu innych zmiennych, dlatego warto się tym tematem głębiej zainteresować, gdyż szczególnie FastCGI zdobywa ostatnio coraz większą popularność.

 

Odpowiedz

 

Wszystko spoko i tak dalej tylko szkoda, że niedoświadczeni webmasterzy nie wiedzą, że jest to najgorsze rozwiązanie 😉 Dlaczego? Wystarczy sobie poczytać jakie zagrożenia to niesie.

 

Odpowiedz

 

Dodaj komentarz

 
(nie będzie publikowany)
 
 
Komentarz
 
 

Dozwolone tagi XHTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
© 2009 - 2016 Vokiel.com
WordPress Theme by Arcsin modified by Vokiel