image

Наткнувшись недавно на Надёжная авторизация для веб-сервиса за один вечер решил тоже, что нужно рассказать о своем велосипеде.
Аутентификация будет с использованием cookie. Сразу скажу что нам нужно использовать https — это тема даже не подлежит обсуждению. Статья будет из 2х частей. В первой части мы будем идентифицировать устройство. Во второй части мы научимся авторизовать и аутентифицировать пользователя.
В качестве фреймворка я буду использовать codeigniter. Во первых в нем намного проще показать принцип, он имеет самый низкий порог вхождения. Как правило все статьи для начинающих php программистах используют codeigniter. И кто бы не говорил о том что codeigniter мертв, его еще очень часто используют в азиатских странах.

1. Для идентификации клиента, как только пользователь зашел на страницу, ему срау же отдается cookie с идентификатором, юзер агентом и другими данными, о них чуть позже. Пользователя мы будем по cid — client id. Для этого создадим в бд таблицу с клиентами.
 CREATE TABLE `cid` (
  `cid` int(11) NOT NULL AUTO_INCREMENT COMMENT 'CLIENT ID'
  ,`user_agent` varchar(255) NOT NULL COMMENT 'USER AGENT'
  ,`status` varchar(255) NOT NULL COMMENT 'STATUS'
  ,`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'TIME OF CREATOR'
  ,PRIMARY KEY (`cid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='CLIENT ID TABLE';

cid- client id. Идентификатор устройства в системе.
user_agent- user agent браузера.
status- статус устройства. Активно ли устройство, не активно, либо устройство скомпрометировано.
timestamp- время создания CID в системе.

В целях защиты CID, будем хранить в базе хиты устройства.
CREATE TABLE `cid_hits` ( 
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT 'PRIMARY KEY' 
,`cid` INT(11) NOT NULL COMMENT 'CLIENT_ID' 
,`uri` VARCHAR(255) NOT NULL COMMENT 'IPv6 ADRESS' 
,`ipv4` INT(11) NOT NULL COMMENT 'IPv4 ADRESS' 
,`ipv6` VARCHAR(46) NOT NULL COMMENT 'IPv6 ADRESS' 
,`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'TIME OF CREATE' 
,PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET=utf8 COMMENT = 'CLIENT HITS';

Теперь напишем логику формирования cookie. Весь код представленный ниже будет только для наглядности, но с пояснениями.
1. Поверяем cookie на наличие ключа cid_token. Если его нет то создаем его.
if( !get_cookie('cidToken') ){
//Начинаем транзакцию для бд
$this->db->trans_start();
$cidData["user_agent"] = $this->agent->agent_string();
$cidData["status"] = "active";

 //Создаем новый client id
 $this->db->insert('cid',$cidData);
 $cid = $this->db->insert_id();

//Записываем новый хит в таблице 
$cidHitsData["cid"] = $cid
//Запишем также ip клиента (IPv4/IPv6)
$ip = $this->input->ip_address();
if( $this->input->valid_ip($ip,"ipv4") ){
	$cidHitsData["IPv4"] =ip2long($ip);
}
if( $this->input->valid_ip($ip,"ipv6") ){
	$cidHitsData["IPv6"] = $ip;
}
$cidHitsData['URI'] = $_SERVER['REQUEST_URI'];
$this->db->insert('cid_hits',$cidHitsData);
$cidHit= $this->db->insert_id();

 $this->db->trans_complete();
 //Завершаем транзакцию для бд
 
 //На данном этапе мы имеем $cid и $cidHit;
 //Запишем данные в cookie

$cidCoockie["cid"]=$cid;
$cidCoockie["cidHit"]=$cidHit;
$$cidToken= $this->encryption->encrypt( json_encode($cidCoockie) );
set_cookie('cidToken', $cidToken, 60*60*24*365*5 );//Записываем на 5 лет куки
}

В коде выше, в случае если у нас нет в cookie cidToken мы создаем запись и получаем cid, каждому cid мы записываем его user agent. Также мы создаем запись хита с данным cid, в хите мы записываем ip адрес, REQUEST_URI. Эти данные также полезны для отслеживания статистики посещения сайта. В сами куки шифруем данные о user agent, последнем хите, и CID. В качестве алгоритма используется aes-128 cbc, но можно использовать любой другой.
2. В случае посещения пользователя, с cidToken, нам необходимо его проверить.
2.1 Проверка на валидность.
// 1. Расшифровываем куки
	$cidToken= get_cookie('cidToken');
	$cidToken = $this->encryption->decrypt($cidToken);
	$cidData = json_decode($cidToken,true);
	if (!$cidData ){
		//Если куки не валидны (не правильно расшифрованы)
		delete_cookie('cidToken');
	}

Если в процессе расшифровки cookie на стороне сервера, они оказались не валидными, то необходимо их удалить.
2.2 Проверка cookie на соответствие user agent. К примеру, злоумышленник каким либо образом завладел вашими cookie, то мы проверим с того ли устройства он заходит. Также во многих статьях проверяется еще и ip адрес, но это имеет место только в случае IPv6. как правило у пользователей динамический ip, поэтому я считаю на мой взгляд эта проверка не имеет смысла.
$this->db->trans_start();
$query = $this->db->get_where('cid', array('cid' => $cidData ['cid']));
$row_array = $query->row_array();
//Если куки принадлежат другому юзер агенту
if($row_array["user_agent"]!=$this->agent->agent_string()){
 //На этом этапе cookie скомпрометировано
 delete_cookie('cidToken');
 $this->db->update('cid', array("sttus"=>"compromused"), array('cid' => 
 $cidData['cid']));
}
$this->db->trans_complete();

Если user агенты не совпадают, то удаляем cookie, и обновляем статут CID на скомпрометировано. Стоит отметить что тут можно более оптимально использовать ресурсы (сравнивать hash(user agent), сам hash хранить в cookie, тем самым не делать запрос к БД, но я посчитал это более наглядным способом).
2.3 Проверка активности CID.
$this->db->trans_start();
$query = $this->db->get_where('cid', array('cid' => $cidData['cid']));
$row_array = $query->row_array();
//Если куки принадлежат другому юзер агенту
if( $row_array["status"]!="active"){
	//На этом этапе cookie скомпрометировано
	delete_cookie('cidToken');
}
$this->db->trans_complete();

В случае если CID не активен (скомпрометирован, деактивирован), то удаляем cookie
2.3 Проверка CID по созданным хитам. А если cookie вдруг получил злоумышленнику, а также выставил себе user agent, то он сможет выдать себя за вас, чтобы этого не было, представим алгоритм проверки CID по хитам.
$this->db->trans_start();
 $this->db->select('count(id)');
 $this->db->from('cid_hits');
 $this->db->where('cid', $cidData['cid']);
 $this->db->where('id>', $cidData['cidHit']);
 $query = $this->db->get();
 $row_array = $query->row_array();
 $countId= $row_array['count(id)'];
 $maxOver= $this->config->item('cookie_max_over');
 if ($countId> $maxOver) {
	//На этом этапе cookie скомпрометировано
	$this->db->update('cid', array("status"=>"COMPROMUSED"), array('cid' => $cidData['cid']));
	delete_cookie('cidToken');
 }
$this->db->trans_complete();

Проверка по хитам производится следующим образом. К примеру злоумышленник получил cookie клиента. И выдает себя за него. Т.к. в самих cookie о последнем хите, то мы смотрим, сколько хитов было совершенно после последнего записанного хита в cookie. Если это значение больше чем заданное (берется из настроек, и задается пользователем), то cookie были скомпрометированы. Поэтому мы удаляем cookie и обновляем статус активности CID. Тем самым как пользователь, так и злоумышленник более не смогут ею пользоваться, т.к. в дальнейшем будет отрабатывать 2.3. Значение лучше устанавливать больше либо равным 2 и за того что иногда у пользователя зашедшим сайт, может оборваться связь т.е. запрос дошел до сервера, сервер ответил на запрос, но устройство пользователя потеряло соединение с интернетом (пример с мобильным, с телефона попытались открыть сайт, сделали запрос и въехали в тоннель, или телефон выключился, или связь оборвалась и т.п.). И если даже связь оборвалась, то у устройства будет как минимум несколько попыток (На самом деле метод не совсем идеален, необходимо также учесть сколько раз были такие попытки, если попыток было много в какой то ограниченный интервал времени то cookie тоже были скомпрометированы. Но для новичков этого пока будет достаточно).
Если все проверки пройдены, то обновляем данные в cookie, и создаем хит
$this->db->trans_start();
 	
$cidHitsData["CID"] = $cidData['cid']
$ip = $this->input->ip_address();
if( $this->input->valid_ip($ip,"ipv4") ){
	$cidHitsData["IPv4"] =ip2long($ip);
}
if( $this->input->valid_ip($ip,"ipv6") ){
	$cidHitsData["IPv6"] = $ip;
}
$cidHitsData['URI'] = $_SERVER['REQUEST_URI'];
$this->db->insert('cid_hits',$cidHitsData);
$cidHit= $this->db->insert_id();
$cidHit['cidHit'] = $cidHit;
$this->db->trans_complete();


После всех этих проверок мы можем уникально идентифицировать устройство. В дальнейшем мы будем выдавать разрешение определенному CID. Метод позволит авторизовываться от одного пользователя на нескольких устройствах. В нем есть какая-никакая, но все таки защита от кражи cookie. В следующей статье мы займемся авторизацией.

Комментарии (19)


  1. ErickSkrauch
    18.10.2017 19:19
    +4

    В следующей статье мы займемся авторизацией.

    А может не стоит?


    1. pan-alexey Автор
      18.10.2017 20:05

      От чего же? Я думаю конструктивная критика будет полезна.


      1. HunterNNm
        18.10.2017 20:09

        Для начала — PSR. Рекомендую.


        1. pan-alexey Автор
          18.10.2017 20:13

          кроме camelCase что не так?


          1. HunterNNm
            18.10.2017 20:15

            Отступы. Код плохо читается. Из-за этого его хочется просто… не читать.


            1. pan-alexey Автор
              18.10.2017 20:17

              К сожалению у меня не работает tab. Учту, попозже сделаю рефакторинг. Спасибо за конструктивную критику.


              1. r-moiseev
                18.10.2017 20:30

                Он вам и не нужен, в PSR 4 пробела если что.


                1. pan-alexey Автор
                  18.10.2017 20:43
                  -2

                  Я выбираю tab.


                  1. sayber
                    19.10.2017 01:32

                    Вы можете выбрать что угодно, но есть PSR.
                    За табы, я бы руки отрубал, особенно если такой код в кодревью попадает.
                    Форматирование — делается просто, смотрим изображение.
                    image


  1. NorthDakota
    18.10.2017 19:19
    +1

    Мне кажется вы не знаете разницу между авторизацией и аутентификацией
    Не увидел последнее предложение в статье

    У меня кровь из глаз пошла от вида вашего кода


    1. pan-alexey Автор
      18.10.2017 20:07
      -2

      Ну я думаю что не только у вас. Значит статья не для вас, т.к. целью написать красивый код я не ставил. По поводу разницы между авторизацией и аутентификация, я думаю вы не внимательно прочли. В статье нет ни слова что это авторизация, лишь в заголовке.


      1. Hazrat
        18.10.2017 20:33

        Ну можно же было для людей, которым будет интересно сделать форматирование, это не серьезный поход


  1. Hazrat
    18.10.2017 20:24
    +1

    У меня одного ностальгия?


  1. AlexLeonov
    18.10.2017 20:37
    +1

    У меня одного желание сказать «горшочек не вари»?
    Что происходит с хабом PHP последнее время? Откуда берутся эти адепты Codeigniter?


    1. pan-alexey Автор
      18.10.2017 20:47

      Это не реклама, но вот отсюда


      1. quantum
        18.10.2017 21:01

        Я был о них лучшего мнения


      1. AlexLeonov
        18.10.2017 21:51
        +1

        Имхо вы сгущаете краски.
        Я сам с ними долго сотрудничал, преподавал. Правда вот уже третий год как ушел в свой камерный проектик.

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


  1. Dreyk
    18.10.2017 22:36

    астрологии объявили неделю велосипедов на пхп?


    1. sayber
      19.10.2017 01:38

      Ладно велосипед, есть более серьезные проблемы.
      Большая куча текста в перемешку с кодом, который читать то невозможно.
      И ладно бы что то дельное было в статье, можно было простить и попросить переписать.
      Но тут ведь нет чего то нового или хотя бы современного.

      Видимо автор прямиком из начала нулевых к нам прилетел.