В рамках работы по оценке различных способов реализации Web UI для существующего C++ приложения, на основе хорошо известного на Хабре фреймворка Fastcgi Daemon был создан фреймворк Fastcgi Container.
При сохранении всех возможностей прототипа, основные отличия нового фреймворка от него заключаются в следующем:
- фреймворк переписан на C++11
- добавлена поддержка фильтров
- добавлена поддержка аутентификации и авторизации клиента
- добавлена поддержка сессий
- добавлена поддержка сервлетов (расширение обработчиков запросов из оригинального фреймворка)
- добавлен Page Compiler для генерирования C++ сервлетов из JSP-подобных страниц
Особенности и детали реализации прототипа обсуждались на Хабре несколько раз (например, здесь). В данной статье приведены особенности нового фреймворка Fastcgi Container.
Использование C++11
Фреймворк-прототип Fastcgi Daemon широко использует библиотеки Boost. Производный фреймворк было решено переписать на C++11, заменив использование Boost на новые стандартные конструкции. Исключение составила библиотека Boost.Any, эквивалент которой отсутствует в C++11. Необходимый функционал был добавлен через использование библиотеки MNMLSTC Core.
Фильтры
Протокол FastCGI предусматривает роли Filter и Authorizer для организации соответствующего функционала, однако распространённые реализации (например, модули для Apache HTTPD и NGINX) поддерживают только роль Responder.
В результате поддержка фильтров была добавлена непосредственно в Fastcgi Container.
В приложении фильтры создаются как расширение класса fastcgi::Filter
:
class Filter { public: Filter(); virtual ~Filter(); Filter(const Filter&) = delete; Filter& operator=(const Filter&) = delete; virtual void onThreadStart(); virtual void doFilter(Request *req, HandlerContext *context, std::function<void(Request *req, HandlerContext *context)> next) = 0; };
Их загрузка в контейнер осуществляется динамически, аналогично другим компонентам приложения:
<modules> <module name="example" path="./example.so"/> ... </modules> <components> <component name="example_filter_1" type="example:filter1"> <logger>daemon-logger</logger> </component> <component name="example_filter_2" type="example:filter2"> <logger>daemon-logger</logger> </component> ... </components>
Фильтры могут быть либо глобальными для данного приложения:
<handlers urlPrefix="/myapp"> <filter> <component name="example_filter_1"/> </filter> ... </handlers>
либо предназначены для обработки группы запросов с URL, соответствующим заданному регулярному выражению:
<handlers urlPrefix="/myapp"> <filter url="/.*"> <component name="example_filter_2"/> </filter> ... </handlers>
Если контейнер нашёл более одного фильтра для текущего запроса, они будут выполнены в той же очерёдности, в которой были добавлены в конфигурационный файл.
Для передачи управления следующему по очереди фильтру (или целевому обработчику/сервлету, если фильтр единственный или последний в очереди), текущий фильтр вызывает функцию next
, переданную ему через список параметров. Для прерывания цепочки фильтр может возвратить управление без вызова функции next
.
В общем, каждый фильтр получает управление два раза: до передачи управления следующему фильтру или целевому обработчику/сервлету, а также после окончания работы следующему фильтра или обработчика/сервлета. Фильтр может изменить тело ответа (response) и/или заголовки (HTTP headers) как до, так и после работы целевого обработчика/сервлета при условии, что тело и заголовки ещё на отправлены клиенту.
Аутентификация
Аутентификация осуществляется специальными фильтрами. В состав Fastcgi Container включены фильтры для следующих типов аутентификации: Basic access authentication
, Form authentication
, и Delegated authentication
.
Последний из названных типов делегирует процесс аутентификации HTTP серверу, ожидая от него идентификатор пользователя, переданный как стандартная CGI переменная REMOTE_USER
.
Два других типа осуществляют аутентификацию, используя предоставленный Security Realm
.
Как и в случае обычных фильтров, загрузка в контейнер осуществляется динамически:
<modules> <module name="auth" path="/usr/local/lib64/fastcgi3/fastcgi3-authenticator.so"/> ... </modules> <components> <component name="form_authenticator" type="auth:form-authenticator"> <form-page>/login</form-page> <realm>example_realm</realm> <logger>daemon-logger</logger> <store-request>true</store-request> </component> <component name="basic_authenticator" type="auth:basic-authenticator"> <realm>example_realm</realm> <logger>daemon-logger</logger> </component> <component name="delegated_authenticator" type="auth:delegated-authenticator"> <realm>example_realm</realm> <logger>daemon-logger</logger> </component> ... </components>
Фильтр аутентификации, как правило, указывается первым в цепочке фильтров:
<handlers urlPrefix="/myapp"> <filter url="/.*"> <component name="form_authenticator"/> </filter> ... </handlers>
Для своей работы фильтры аутентификации требуют Security Realm
, который должен быть реализован в приложении как расширение класса fastcgi::security::Realm
:
class Realm : public fastcgi::Component { public: Realm(std::shared_ptr<fastcgi::ComponentContext> context); virtual ~Realm(); virtual void onLoad() override; virtual void onUnload() override; virtual std::shared_ptr<Subject> authenticate(const std::string& username, const std::string& credentials); virtual std::shared_ptr<Subject> getSubject(const std::string& username); const std::string& getName() const; protected: std::string name_; std::shared_ptr<fastcgi::Logger> logger_; };
Пример простой реализации Security Realm
с заданием списка пользователей непосредственно в конфигурационном файле:
class ExampleRealm : virtual public fastcgi::security::Realm { public: ExampleRealm(std::shared_ptr<fastcgi::ComponentContext> context); virtual ~ExampleRealm(); virtual void onLoad() override; virtual void onUnload() override; virtual std::shared_ptr<fastcgi::security::Subject> authenticate(const std::string& username, const std::string& credentials) override; virtual std::shared_ptr<fastcgi::security::Subject> getSubject(const std::string& username) override; private: std::unordered_map<std::string, std::shared_ptr<UserData>> users_; }; ExampleRealm::ExampleRealm(std::shared_ptr<fastcgi::ComponentContext> context) : fastcgi::security::Realm(context) { const fastcgi::Config *config = context->getConfig(); const std::string componentXPath = context->getComponentXPath(); std::vector<std::string> users; config->subKeys(componentXPath+"/users/user[count(@name)=1]", users); for (auto& u : users) { std::string username = config->asString(u + "/@name", ""); std::shared_ptr<UserData> data = std::make_shared<UserData>(); data->password = config->asString(u + "/@password", ""); std::vector<std::string> roles; config->subKeys(u+"/role[count(@name)=1]", roles); for (auto& r : roles) { data->roles.push_back(config->asString(r + "/@name", "")); } users_.insert({username, std::move(data)}); } } ExampleRealm::~ExampleRealm() { ; } void ExampleRealm::onLoad() { fastcgi::security::Realm::onLoad(); } void ExampleRealm::onUnload() { fastcgi::security::Realm::onUnload(); } std::shared_ptr<fastcgi::security::Subject> ExampleRealm::authenticate(const std::string& username, const std::string& credentials) { std::shared_ptr<fastcgi::security::Subject> subject; auto it = users_.find(username); if (users_.end()!=it && it->second && credentials==it->second->password) { subject = std::make_shared<fastcgi::security::Subject>(); for (auto &r : it->second->roles) { subject->setPrincipal(std::make_shared<fastcgi::security::Principal>(r)); } subject->setReadOnly(); } return subject; } std::shared_ptr<fastcgi::security::Subject> ExampleRealm::getSubject(const std::string& username) { std::shared_ptr<fastcgi::security::Subject> subject; auto it = users_.find(username); if (users_.end()!=it && it->second) { subject = std::make_shared<fastcgi::security::Subject>(); for (auto &r : it->second->roles) { subject->setPrincipal(std::make_shared<fastcgi::security::Principal>(r)); } subject->setReadOnly(); } return subject; }
Его загрузка в контейнер аналогична загрузке других компонентам приложения:
<modules> <module name="example" path="./example.so"/> ... </modules> <components> <component name="example_realm" type="example:example-realm"> <name>Example Realm</name> <logger>daemon-logger</logger> <users> <user name="test1" password="1234"> <role name="ROLE1"/> <role name="ROLE2"/> <role name="ROLE3"/> </user> <user name="test2" password="5678"> <role name="ROLE1"/> <role name="ROLE4"/> </user> </users> </component> ... </components>
Для корректной работы фильтр аутентификации Form Authentication
требует активации поддержки сессий. При этом сессии используются для сохранения начального запроса от клиентов не прошедших аутентификацию.
Авторизация
Для декларативной авторизации используется элемент <security-constraints>
в конфигурационном файле:
<security-constraints> <constraint url=".*" role="ROLE1"/> <constraint url="/servlet" role="ROLE2"/> </security-constraints>
Авторизация может осуществляется программно. Для этой цели классы fastcgi::Request
и fastcgi::HttpRequest
предоставляют методы:
std::shared_ptr<security::Subject> Request::getSubject() const; bool Request::isUserInRole(const std::string& roleName) const; template<class T> bool Request::isUserInRole(const std::string &roleName) { return getSubject()->hasPrincipal<T>(roleName); }
Если клиент не аутентифицирован, метод getSubject()
возвращает указатель на объект-«аноним» с пустым множеством ролей и возвращающим true
при вызове следующего метода:
bool security::Subject::isAnonymous() const;
Сессии
Контейнер предоставляет реализацию Simple Session Manager
. Для его активации в конфигурационный файл нужно добавить следующее:
<modules> <module name="manager" path="/usr/local/lib64/fastcgi3/fastcgi3-session-manager.so"/> ... </modules> <components> <component name="session-manager" type="manager:simple-session-manager"> <logger>daemon-logger</logger> </component> ... </components> <session attach="true" component="session-manager"> <timeout>30</timeout> </session>
Для доступа к текущей сессии класс fastcgi::Request
предоставляет метод:
std::shared_ptr<Session> Request::getSession();
Среди прочего, класс fastcgi::Session
предоставляет следующие методы:
virtual void setAttribute(const std::string &name, const core::any &value); virtual core::any getAttribute(const std::string &name) const; virtual bool hasAttribute(const std::string &name) const; virtual void removeAttribute(const std::string& name); virtual void removeAllAttributes(); std::type_info const& type(const std::string &name) const; std::size_t addListener(ListenerType f); void removeListener(std::size_t index);
Simple Session Manager
не имеет поддержки кластера контейнеров, поэтому в случае использования более одного контейнера на балансировщике нагрузки следует настроить режим «sticky sessions».
В целом, использование сессий следует избегать в системах, от которых ожидается высокая производительность при большой нагрузке, поскольку решения с сессиями плохо масштабируются.
Сервлеты
В дополнение к классам fastcgi::Request
и fastcgi::Handler
, контейнер предоставляет классы-оболочки fastcgi::HttpRequest
, fastcgi::HttpResponse
и fastcgi::Servlet
.
В приложении можно использовать как «старые» так и «новые» классы.
C++ Server Pages и Page Compiler
Page Compiler является форком из проекта POCO, и предназначен для трансляции HTML страниц со специальными директивами (C++ server pages, CPSP) в сервлеты.
Простой пример C++ server page:
<%@ page class="TimeHandler" %> <%@ component name="TestServlet" %> <%! #include <chrono> %> <% auto p = std::chrono::system_clock::now(); auto t = std::chrono::system_clock::to_time_t(p); %> <html> <head> <title>Time Sample</title> </head> <body> <h1>Time Sample</h1> <p><%= std::ctime(&t) %></p> </body> </html>
Подробное описание директив доступно на GitHub проекта.
ссылка на оригинал статьи https://habrahabr.ru/post/280814/
Добавить комментарий