Дано:
на сервере установлен pfSense 2.3
с модулем Captive Portal (кэптив
портал для контроля входа пользователей в Интернет через WiFi - wifi-hotspot).
Задача:
настроить собственную страницу ввода логина-пароля (или номера ваучера) взамен
встроенной (login page form),
а также отображать собственную страницу выхода (logout
page, отсоединения сесии), а также обойти проблему всплывающего окна формы выхода (popup window, которая зачастую блокируется в
браузере).
Т.к.
мы хотим позволить пользователю не только вводить логин-пароль, но и иметь
возможность войти в сеть по выданному ваучеру, нам необходимо было совместить
обе формы входа, т.к. по умолчанию pfSense отображает только форму ввода логина.
Как загрузить свои шаблоны форм?
Делается
это на странице настроек Captive портала
в блоке "HTML Page Contents" (Services - Captive Portal - Имя зоны
- Configuration):
Можно загрузить три
шаблона форм: страница входа, страница вывода ошибки и страница выхода.
Выводить отдельную страницу с ошибкой нецелесообразно, поэтому в качестве нее
будет использовать ту же страницу входа.
Как видно уже
предлагается базовый шаблон:
<form
method="post" action="$PORTAL_ACTION$">
<input name="auth_user"
type="text">
<input name="auth_pass"
type="password">
<input name="auth_voucher"
type="text">
<input name="redirurl"
type="hidden" value="$PORTAL_REDIRURL$">
<input name="zone"
type="hidden" value="$PORTAL_ZONE$">
<input name="accept"
type="submit" value="Continue">
</form>
Здесь
важны имена полей для ввода, а также скрытые поля и заменяемые значения ($). На базе этого шаблона, не меняя имен,
можно сделать любой свой шаблон.
Если
необходимо использовать внешние файлы-ресурсы (рисунки, стили, скрипты), то их можно
загрузить в File Manager портала
(Services - Captive Portal - Имя зоны - File
Manager). Нужно учесть, что у ресурсов в имени необходимо
добавить префикс "captiveportal-"
Например,
captiveportal-logo.png
captiveportal-mystyle.css
Если
этого не сделать, то им этот префикс будет присвоен при загрузке и в шаблоне
ссылки на них перестанут работать.
Я
приведу примеры двух шаблонов:
1)
форма входа login.html
2)
форма выхода logout.html
Форма входа login.html
В
нашем случае она должна была содержать как поля ввода логина пароля, так и поле
для ввода номера ваучера. Также нужно было добавить предварительную валидацию
полей ввода от пустых значений и некорректного ввода (через javascript). Стили и скрипты можно описывать
сразу в коде страницы.
Примечание: шаблон формы также допускает
наличие php-скриптов (чем мы воспользуемся при создании
формы выхода).
Итак,
шаблон должен содержать следующие скрытые поля:
<input name="redirurl"
type="hidden" value="$PORTAL_REDIRURL$">
<input name="zone"
type="hidden" value="$PORTAL_ZONE$">
Оставляем
их без изменений.
Поле
для ввода логина должно называться auth_user, а поле пароля auth_pass. Поле для ввода
номера ваучера - auth_voucher.
Форма
должна содержать кнопку submit
с именем accept
(попытка отправить форму с
другим именем такой кнопки не удалась, хотя не понятно почему, т.к. имя именно
кнопки отправки не должно ни на что влиять).
Также,
т.к. мы объединяем эту страницу со страницей вывода ошибок, то в том месте
страницы, где хотелось бы показать текст ошибки, нужно вставить шаблон
переменной: $PORTAL_MESSAGE$.
Т.к. заранее
известно, что если логин-пароль не верны и ошибка возвращает текст "Invalid credentials specified.", то можно
это "отловить" в скрипте и показать текст по-русски (см. мой шаблон
ниже).
Тексты
ошибок для ваучеров можно настроить непосредственно на странице параметров
генерации ваучеров (Services - Captive Portal - Имя зоны
- Vouchers):
Примечание: будьте осторожнее со сменой настроек
ваучеров, любое изменение помимо текста ошибок (например, битность кода или
ключа) сделает все сгенерированные ранее ваучеры недействительными.
Также при вводе
ваучера пользователь должен согласиться с условиями использования нашей сети
(поставить "галочку" "Согласен").
Код страницы входа
получился следующим (для публикации убрала лишние стили, которые можно добавить
самостоятельно, обязательные поля выделила желтым):
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;
charset=UTF-8" http-equiv="Content-Type">
<meta name="viewport" content="width=device-width, initial-scale=1.0,
minimum-scale=1.0">
<title>Подключение к WiFi</title>
<script type="text/javascript">
function
getCookie(name) {
var
matches = document.cookie.match(new RegExp(
));
return
matches ? decodeURIComponent(matches[1])
: undefined;
}
function
setCookie(name, value) {
value =
encodeURIComponent(value);
var
updatedCookie = name + "=" + value;
var
date = new Date;
date.setDate(date.getDate() + 30);
updatedCookie += ";
expires=" + date.toUTCString() + "; path=/";
document.cookie = updatedCookie;
}
function
CheckMail(txtLogin){
var
email = txtLogin.value;
if(email != 0)
{
if(isValidEmailAddress(email))
{
txtLogin.style.backgroundColor = "#ffffff";
} else {
txtLogin.style.backgroundColor = "#fff0f0";
}
} else {
txtLogin.style.backgroundColor = "#ffffff";
}
}
function
CheckRequired(txtField){
if(txtField.value
!= 0)
{
txtField.style.backgroundColor = "#ffffff";
return true;
} else {
txtField.style.backgroundColor = "#fff0f0";
return false;
}
}
function
CheckBoxRequired(cbxFieldName){
var
cbxField = document.getElementById(cbxFieldName);
if(cbxField.checked)
{
return true;
} else {
return false;
}
}
function
isValidEmailAddress(emailAddress) {
var
pattern = new RegExp(/^(("[\w-\s]+")|([\w-]+(?:\.[\w-]+)*)|("[\w-\s]+")([\w-]+(?:\.[\w-]+)*))(@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(@\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,2}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\]?$)/i);
return
pattern.test(emailAddress);
}
function
isValidVaucher (VaucherNumber) {
var
pattern = new RegExp(/^([23456789abcdefhikmnprstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ]){11,12}$/i);
return
pattern.test(VaucherNumber);
}
function
CheckForm ()
{
var
blnIsValid =
true;
var
CookieValue =
'';
var
CookieVaucherNumber = '';
var
CookieLogin =
'';
if (document.getElementById('VaucherForm').style.display!='none') {
if (CheckBoxRequired('cbxRulesVaucher')) {
document.getElementById('RequiredFieldVaucherRules').style.display='none';
} else {
document.getElementById('RequiredFieldVaucherRules').style.display='';
blnIsValid = false;
};
var inputVaucher = document.getElementById("auth_voucher");
if (!CheckRequired(inputVaucher)) {
document.getElementById('RequiredFieldVaucher').style.display='';
blnIsValid = false;
} else {
document.getElementById('RequiredFieldVaucher').style.display='none';
if (!isValidVaucher(inputVaucher.value)) {
document.getElementById('ErrorFieldVaucher').style.display='';
blnIsValid = false;
inputVaucher.style.backgroundColor = "#fff0f0";
} else {
document.getElementById('ErrorFieldVaucher').style.display='none';
inputVaucher.style.backgroundColor = "#ffffff";
};
};
CookieValue = 'VaucherForm';
CookieVaucherNumber = inputVaucher.value;
}
if (document.getElementById('LoginForm').style.display!='none') {
var inputLogin = document.getElementById("auth_user");
if (!CheckRequired(inputLogin)) {
document.getElementById('RequiredFieldMail').style.display='';
blnIsValid = false;
} else {
document.getElementById('RequiredFieldMail').style.display='none';
if (isValidEmailAddress(inputLogin.value)) {
document.getElementById('FieldMailError').style.display='none';
inputLogin.style.backgroundColor = "#ffffff";
} else {
document.getElementById('FieldMailError').style.display='';
blnIsValid = false;
inputLogin.style.backgroundColor = "#fff0f0";
}
}
if (!CheckRequired(document.getElementById("auth_pass"))) {
document.getElementById('RequiredFieldPass').style.display='';
blnIsValid = false;
} else {
document.getElementById('RequiredFieldPass').style.display='none';
}
CookieValue = 'LoginForm';
CookieLogin = inputLogin.value;
}
if (blnIsValid) {
setCookie ('CaptiveForm',CookieValue);
if(CookieLogin != 0) {
setCookie ('CaptiveLogin',CookieLogin);
}
if(CookieVaucherNumber != 0) {
setCookie ('CaptiveVaucher',CookieVaucherNumber);
}
}
return
blnIsValid;
}
</script>
</head>
<body>
<form id="formcaptiveportal"
action="$PORTAL_ACTION$" method="post">
<input name="redirurl"
type="hidden"
value="$PORTAL_REDIRURL$">
<input name="zone"
type="hidden"
value="$PORTAL_ZONE$">
<img class="logo" src="captiveportal-logo.jpg" border="0">
<div id="LoginForm">
<h1>WiFi для сотрудников</h1>
<p><label>Логин</label>
<input name="auth_user" id="auth_user" type="email" onBlur="CheckMail(this);" ></p>
<p><label>Пароль</label>
<input name="auth_pass" id="auth_pass" type="password"></p>
<p><a href="#" onclick="document.getElementById('VaucherForm').style.display='';
document.getElementById('LoginForm').style.display='none';return
false;">Подключение по ваучеру</a></p>
<p>
<span id="RequiredFieldMail" style="color:
red; display: none;">Логин не может быть пустым! </span>
<span id="FieldMailError" style="color:
red; display: none;">Не верный формат логина (name@mycorp.ru)! </span>
<span id="RequiredFieldPass" style="color:
red; display: none;">Пароль не может быть пустым.</span>
<span id="ErrorLogin" style="color: red;"></span>
</p>
<p><input name="accept" id="btnLogin" onclick='return
CheckForm()' type="submit"
value="Подключиться"></p>
</footer>
</div>
<div id="VaucherForm"
style="display:none;">
<h1>Подключение к WiFi по ваучеру</h1>
<p><label>Ваучер</label>
<input name="auth_voucher" id="auth_voucher"></p>
<p><input name="cbxRulesVaucher" id="cbxRulesVaucher" type="checkbox">Я ознакомился и согласен с
<a href="#" onclick="var block =
document.getElementById('RulesBlock'); if (block.style.display == 'none')
{ block.style.display = '';} else
{block.style.display = 'none';}; return false;">
условиями</a> использования сети WiFi</label></p>
<p><a href="#" onclick="document.getElementById('LoginForm').style.display='';
document.getElementById('VaucherForm').style.display='none';return
false;">Войти при помощи логина</a></p>
<p><span id="RequiredFieldVaucher" style="color:
red; display: none;"> Номер ваучера не может быть пустым. </span>
<span id="ErrorFieldVaucher" style="color:
red; display: none;"> Неверный номер ваучера. </span>
<span id="RequiredFieldVaucherRules" style="color:
red; display: none;"> Вам необходимо ознакомиться и согласиться с условиями. </span>
<div id="RulesBlock" style="display:none;">Наши условия...</div>
<span id="ErrorVaucher" style="color: red;"></span></p>
<p><input name="accept" class="button"
id="btnLoginVaucher"
onclick='return
CheckForm()' type="submit"
value="Подключиться"></p>
</div>
<script type="text/javascript">
if (getCookie
('CaptiveForm')=='VaucherForm') {
document.getElementById('LoginForm').style.display='none';
document.getElementById('VaucherForm').style.display = '';
}
var
loadCookieCaptiveLogin =
getCookie ('CaptiveLogin');
if (!((loadCookieCaptiveLogin==0) | (typeof loadCookieCaptiveLogin=='undefined'))) {
document.getElementById('auth_user').value = getCookie ('CaptiveLogin');
}
var
loadCookieCaptiveVaucher =
getCookie ('CaptiveVaucher');
if (!((loadCookieCaptiveVaucher==0) | (typeof loadCookieCaptiveVaucher=='undefined'))) {
document.getElementById('auth_voucher').value = loadCookieCaptiveVaucher;
}
var
txtError =
'$PORTAL_MESSAGE$';
if (txtError
== 'Invalid credentials specified.') {
document.getElementById('ErrorLogin').innerHTML = 'Указаны неверные учетные данные.';
document.getElementById('ErrorVaucher').innerHTML = 'Указаны неверные учетные данные.';
} else
{
document.getElementById('ErrorLogin').innerHTML = txtError;
document.getElementById('ErrorVaucher').innerHTML = txtError;
}
</script>
</form></body>
</html>
В итоге
будет страница со ссылкой, которая переключает формы ввода логина и ваучера для
удобства пользователя. Если наложить стили, форма может выглядеть примерно так:
Учтите, что также нужно предусмотреть ситуацию, когда пользователь заполнил и поле логина и поле ваучера - в этом случае перед отправкой одно из полей нужно очистить, иначе авторизация не пройдет (например, стирать скриптом не нужное поле).
Полученный html-файл можно загружать в поле Portal page contents и Auth error page contents выше указанной формы настроек.
Полученный html-файл можно загружать в поле Portal page contents и Auth error page contents выше указанной формы настроек.
Форма выхода logout.html
Образец
формы выхода (disconnecting form)
пришлось искать отдельно. Форма по умолчанию представлена в файле
captiveportal.inc и представляет собой страницу, которая скриптом отображает
всплывающее popup окно, а сама
переходит по адресу, который первоначально запросил пользователь (redirect url).
Использовав
эту страницу как образец, можно получить свою форму с нужными стилями и русским
текстом.
Чтобы
форма выхода (отсоединения от
сессии) появлялась, нужно в настройках портала в блоке "Captive Portal Configuration" задать
параметр "Logout popup window - Enable logout popup
window":
Пример
кода страницы с формой выхода:
<!DOCTYPE html>
<html><head>
<meta content="text/html;
charset=UTF-8"
http-equiv="Content-Type">
<title>Отсоединение от WiFi</title>
</head>
<body>
<p>Переход на
страницу:
<a href="<?php echo $_POST['redirurl'] ?>">
<?php echo $_POST['redirurl'] ?></a></p>
<script type="text/javascript">
//<![CDATA[
LogoutWin = window.open('',
'Logout', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width=256,height=64');
if (LogoutWin) {
LogoutWin.document.write('<!DOCTYPE
html>');
LogoutWin.document.write('<html>');
LogoutWin.document.write('<head><meta
content="text/html; charset=UTF-8"
http-equiv="Content-Type">');
LogoutWin.document.write('<title>Отсоединение от
WiFi</title></head>');
LogoutWin.document.write('<body>');
LogoutWin.document.write('<div>') ;
LogoutWin.document.write('<p>Щелкните кнокпу
"Отсоединиться", чтобы отключиться от сети WiFi</p>');
LogoutWin.document.write('<form method="POST"
action="<?php
echo
$logouturl ?>">');
LogoutWin.document.write('<input
name="logout_id" type="hidden" value="<?php echo $sessionid ?>" />');
LogoutWin.document.write('<input
name="zone" type="hidden" value="<?php echo $cpzone ?>" />');
LogoutWin.document.write('<input
name="logout" type="submit" value="Отсоединиться" />');
LogoutWin.document.write('</form>');
LogoutWin.document.write('</div></body>');
LogoutWin.document.write('</html>');
LogoutWin.document.close();
}
document.location.href="<?php echo $_POST['redirurl'] ?>";
//]]>
</script>
</body></html>
На этой странице присутствует PHP-код для получения идентификатора сессии и других параметров.
Полученный html файл (logout.html) также загружаем в форму настроек в поле Logout page contents.
В итоге будет появляться такое окошко при входе:
Недостаток данной страницы в том, что браузеры блокируют всплывающие окна и форма выхода может вообще не
отобразиться, поэтому можно изменить эту форму по своему усмотрению, убрав
скрипт вывода всплывающего окна, например:
<!DOCTYPE html>
<html><head>
<meta content="text/html;
charset=UTF-8" http-equiv="Content-Type">
<title>Отсоединение от WiFi</title>
</head>
<body>
<p>Перейти на запрашиваемую страницу:
<a target="_blank" href="<?php
echo $_POST['redirurl'] ?>">
<?php echo $_POST['redirurl'] ?></a></p>
<div><p>Щелкните кнокпу
"Отсоединиться",
чтобы отключиться от сети
WiFi</p>
<form method="POST" action="<?php
echo $logouturl ?>">
<input name="logout_id" type="hidden"
value="<?php
echo $sessionid ?>" />
<input name="zone" type="hidden" value="<?php echo $cpzone ?>" />
<input name="logout" type="submit" value="Отсоединиться" />
</form></div>
</body></html>
Но в этом примере пользователю придется самостоятельно нажать
ссылку для перехода на первоначально запрашиваемую страницу, если это
требуется.
(с) Ella S.
Если Вам понравилась статья, пожалуйста, поставьте лайк, сделайте репост или оставьте комментарий. Если у Вас есть какие-либо замечания, также пишите комментарии.
Сколько ищю не могу найти четкого мануала по настройке Captive Portal на PfSense, если может кто может дайте ссылку РАБОЧЕГО манула для весии 2.3
ОтветитьУдалитьТоже не встречала, приходилось все делать "методом тыка". Пока только есть статья по настройке радиус-сервера для авторизации - http://www.e-du.ru/2016/08/freeraius-ad-freebsd-samba.html
УдалитьУ меня пишет все кракозябами и фото заднего плана не ставится. Почему?
ОтветитьУдалитьВозможно страница сделана не в той кодировке? Файл фона загружен в ресурсы с префиксом "captiveportal-"?
Удалитья только загрузил logo.png, хотя как я понял по коду надо jpg, но он тоже не прогружается стоит крестик. А откуда брать еще 3 файла я не знаю
Удалитьне могли бы скинуть недостающие файла на shon_kostja@mail.ru или выложить их тут
УдалитьДругие файлы - это значки (спец.веб-шрифт значков fontawesome в трех вариациях). Если Вы их не используете намеренно в своей html-странице входа, то они Вам не нужны. Если Вы не знаете, используете Вы их или нет, значит они Вам также не нужны (иначе, Вы бы об этом знали).
УдалитьПо поводу jpg ли png - зависит от того, что у Вас написано в HTML коде страницы входа. Покажите строку, как Вы ссылаетесь на данный файл в html. Должно быть что-то вроде <img src="captiveportal-logo.jpg"> или <img src="captiveportal-logo.png">. Данная статья писалась для версии 2.3, может у Вас другая версия и там что-то по другому...
УдалитьЭлла, подскажите вот с такой проблемой. Развернул как-то captive portal на pfsense v2.1, заменил страницу логина на свою на русском языке и всё было хорошо, но тут черт дернул обновиться до версии 2.4.5 и весь русский шрифт заменился на черные ромбики со знаками вопроса внутри. Сделал чистую установку на другм компе - тоже самое. Вставил выложенные выше примеры - ни одной кириллической буквы - сплошные ромбы с вопросами... Пробовал в UTF-8 и в CP-1251 - одинаково. В чем может быть проблема? Буду очень признателен за какой-нибудь рабочий пример!
ОтветитьУдалитьК сожалению, не подскажу, т.к. остановились на версии 2.3 и более не обновляли, т.е. по принципу "Работает, не трожь!" :) что там сейчас с последними версиями не в курсе.
УдалитьА не остался ли случаем дистрибутив версии 2.3? Может это как раз и решит всю проблему?
УдалитьСейчас пока нет времени искать по своим архивам. Посмотрите, вроде на гите лежат все релизы: https://github.com/pfsense/pfsense/releases?after=v2.4.2_1
Удалить