Наткнувшись недавно на Надёжная авторизация для веб-сервиса за один вечер решил тоже, что нужно рассказать о своем велосипеде.
Аутентификация будет с использованием 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)
NorthDakota
18.10.2017 19:19+1Мне кажется вы не знаете разницу между авторизацией и аутентификацией
Не увидел последнее предложение в статье
У меня кровь из глаз пошла от вида вашего кодаpan-alexey Автор
18.10.2017 20:07-2Ну я думаю что не только у вас. Значит статья не для вас, т.к. целью написать красивый код я не ставил. По поводу разницы между авторизацией и аутентификация, я думаю вы не внимательно прочли. В статье нет ни слова что это авторизация, лишь в заголовке.
Hazrat
18.10.2017 20:33Ну можно же было для людей, которым будет интересно сделать форматирование, это не серьезный поход
AlexLeonov
18.10.2017 20:37+1У меня одного желание сказать «горшочек не вари»?
Что происходит с хабом PHP последнее время? Откуда берутся эти адепты Codeigniter?pan-alexey Автор
18.10.2017 20:47Это не реклама, но вот отсюда
AlexLeonov
18.10.2017 21:51+1Имхо вы сгущаете краски.
Я сам с ними долго сотрудничал, преподавал. Правда вот уже третий год как ушел в свой камерный проектик.
Разумеется, все студенты разные. И преподаватели тоже. Но никто же в здравом уме не будет учить злостно игнорировать все стандарты?
Dreyk
18.10.2017 22:36астрологии объявили неделю велосипедов на пхп?
sayber
19.10.2017 01:38Ладно велосипед, есть более серьезные проблемы.
Большая куча текста в перемешку с кодом, который читать то невозможно.
И ладно бы что то дельное было в статье, можно было простить и попросить переписать.
Но тут ведь нет чего то нового или хотя бы современного.
Видимо автор прямиком из начала нулевых к нам прилетел.
ErickSkrauch
А может не стоит?
pan-alexey Автор
От чего же? Я думаю конструктивная критика будет полезна.
HunterNNm
Для начала — PSR. Рекомендую.
pan-alexey Автор
кроме camelCase что не так?
HunterNNm
Отступы. Код плохо читается. Из-за этого его хочется просто… не читать.
pan-alexey Автор
К сожалению у меня не работает tab. Учту, попозже сделаю рефакторинг. Спасибо за конструктивную критику.
r-moiseev
Он вам и не нужен, в PSR 4 пробела если что.
pan-alexey Автор
Я выбираю tab.
sayber
Вы можете выбрать что угодно, но есть PSR.
За табы, я бы руки отрубал, особенно если такой код в кодревью попадает.
Форматирование — делается просто, смотрим изображение.