Введение
Все предыдущие примеры цикла использовали InsecureChannelCredentials()/InsecureServerCredentials() — полностью незащищённое соединение, явно отмеченное как неприемлемое для реального использования. В статье разберём настройку TLS-шифрования транспорта и аутентификации через токены, передаваемые в метаданных запроса (статья 328), аналогично концепциям из десктопного и MQTT циклов про безопасность (статьи 215, 318).
Концепция
TLS для gRPC настраивается через SslCredentials (клиент) и SslServerCredentials (сервер), которым передаются сертификаты — аналогично HTTPS, TLS защищает сами передаваемые данные от перехвата и подмены на сетевом уровне. Аутентификация поверх уже зашифрованного TLS-соединения обычно реализуется через CallCredentials — объект, автоматически добавляющий аутентификационный токен (например, JWT, аналогично статье про JWT-токены из основного цикла) в метаданные каждого исходящего запроса, без необходимости вручную добавлять его в каждом отдельном вызове, как это требовалось бы для метаданных из статьи 328.
Пример кода: Безопасность gRPC
// server/telemetry_server.cpp — настройка TLS на сервере
#include <grpcpp/grpcpp.h>
#include <fstream>
#include <sstream>
std::string readFile(const std::string &path)
{
std::ifstream file(path);
std::stringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
void runSecureServer()
{
grpc::SslServerCredentialsOptions sslOptions;
sslOptions.pem_root_certs = readFile("certs/ca.crt");
grpc::SslServerCredentialsOptions::PemKeyCertPair keyCertPair;
keyCertPair.private_key = readFile("certs/server.key");
keyCertPair.cert_chain = readFile("certs/server.crt");
sslOptions.pem_key_cert_pairs.push_back(keyCertPair);
auto credentials = grpc::SslServerCredentials(sslOptions);
TelemetryServiceImpl service;
grpc::ServerBuilder builder;
builder.AddListeningPort("0.0.0.0:50051", credentials);
builder.RegisterService(&service);
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
server->Wait();
}
// client/telemetryclient.h — настройка TLS-канала и автоматическая вставка токена аутентификации
#pragma once
#include <grpcpp/grpcpp.h>
#include <grpcpp/security/credentials.h>
// CallCredentials — объект, вызываемый библиотекой gRPC перед КАЖДЫМ запросом
// для получения актуальных метаданных аутентификации
class TokenAuthMetadata : public grpc::MetadataCredentialsPlugin
{
public:
explicit TokenAuthMetadata(const std::string &token) : m_token(token) {}
grpc::Status GetMetadata(grpc::string_ref serviceUrl, grpc::string_ref methodName,
const grpc::AuthContext &channelAuthContext,
std::multimap<grpc::string, grpc::string> *metadata) override
{
metadata->insert(std::make_pair("authorization", "Bearer " + m_token));
return grpc::Status::OK;
}
private:
std::string m_token;
};
std::shared_ptr<grpc::Channel> createSecureChannel(const std::string &serverAddress,
const std::string &caCert,
const std::string &authToken)
{
grpc::SslCredentialsOptions sslOptions;
sslOptions.pem_root_certs = caCert;
auto channelCredentials = grpc::SslCredentials(sslOptions);
auto callCredentials = grpc::MetadataCredentialsFromPlugin(
std::make_unique<TokenAuthMetadata>(authToken));
// Объединение TLS (channel credentials) и аутентификации токеном (call credentials) —
// оба механизма работают независимо и применяются совместно к каждому запросу
auto combinedCredentials = grpc::CompositeChannelCredentials(channelCredentials, callCredentials);
return grpc::CreateChannel(serverAddress, combinedCredentials);
}
// server/telemetry_server.cpp — проверка токена на стороне сервера (через интерсептор или вручную)
grpc::Status GetLatestReading(grpc::ServerContext *context,
const telemetry::ReadingRequest *request,
telemetry::SensorReading *response) override
{
const auto &metadata = context->client_metadata();
auto authHeader = metadata.find("authorization");
if (authHeader == metadata.end()) {
return grpc::Status(grpc::StatusCode::UNAUTHENTICATED, "Токен авторизации отсутствует");
}
std::string token(authHeader->second.data(), authHeader->second.size());
if (token.substr(0, 7) != "Bearer " || !isValidToken(token.substr(7))) {
return grpc::Status(grpc::StatusCode::UNAUTHENTICATED, "Недействительный токен");
}
// ... остальная логика метода после успешной проверки токена ...
return grpc::Status::OK;
}
Пояснения к коду
grpc::SslServerCredentials/grpc::SslCredentials настраиваются через структуры опций, содержащие содержимое сертификатов (а не пути к файлам напрямую, что требует явного чтения файлов через readFile()) — это даёт TLS-защиту транспортного уровня, аналогичную HTTPS, независимо от дальнейшей аутентификации конкретного пользователя/устройства. TokenAuthMetadata, наследующий grpc::MetadataCredentialsPlugin, реализует автоматическую вставку токена в заголовок authorization каждого исходящего запроса через переопределение GetMetadata() — этот механизм вызывается библиотекой gRPC автоматически перед каждым запросом, избавляя от необходимости вручную добавлять метаданные аутентификации в каждом отдельном месте кода, где выполняется вызов, в отличие от примера из статьи 328 с ручным context.AddMetadata().
CompositeChannelCredentials() объединяет два независимых механизма — channelCredentials (TLS-шифрование транспорта) и callCredentials (аутентификация конкретного вызывающего) — это разделение ответственности аналогично двухуровневой защите MQTT из статьи 318 (TLS отдельно от аутентификации по логину/паролю), и оба механизма работают совместно, не заменяя друг друга.
Подводные камни
- Использование одного из двух механизмов защиты без другого — TLS без аутентификации защищает данные от перехвата по дороге, но не ограничивает, кто может подключиться к серверу вообще; аутентификация без TLS передаёт сам токен в открытом виде, что делает аутентификацию бессмысленной (токен может быть перехвачен и использован злоумышленником) — аналогично выводу из статьи 318, для реальной защиты нужны оба механизма одновременно, а не выбор только одного из них.
- Хранение токена аутентификации непосредственно в коде клиентского приложения (аналогично проблеме хранения паролей из статей 215, 310, 318) — для production-приложений токен должен быть получен через отдельный, защищённый процесс аутентификации пользователя (логин/пароль, OAuth) и обычно имеет ограниченный срок действия, требующий механизма обновления (refresh), а не быть статично зашитым в код приложения на неопределённо долгий срок.
- Самоподписанные сертификаты без правильной настройки доверия на стороне клиента (аналогично подводному камню из статьи 318 про MQTT с TLS) — клиент должен явно указать корневой сертификат (
pem_root_certs), которому доверяет, через тот же файлca.crt, что использовался при генерации серверного сертификата, иначе TLS-handshake завершится ошибкой проверки сертификата. - Реализация проверки токена внутри каждого отдельного метода сервиса вручную (как в показанном упрощённом примере) — для сервиса с большим количеством методов это приводит к значительному дублированию кода проверки аутентификации; gRPC поддерживает серверные интерсепторы (interceptors), позволяющие централизовать такую проверку в одном месте, выполняемую автоматически для всех вызовов методов сервиса, что аналогично централизованной middleware-логике аутентификации в REST API фреймворках, и предпочтительнее дублирования проверки в каждом отдельном методе.