반응형
실시간 채팅 애플리케이션 보안 기능 추가
이전 단계에서는 실시간 채팅 애플리케이션의 기본 기능을 완성했습니다. 이번 단계에서는 보안 기능을 추가하여 애플리케이션을 더 안전하게 만들겠습니다. SSL/TLS를 사용하여 서버와 클라이언트 간의 통신을 암호화합니다.
SSL/TLS 개요
SSL(Secure Sockets Layer)과 TLS(Transport Layer Security)는 네트워크 통신을 보호하기 위한 프로토콜입니다. 이를 통해 데이터가 전송 중에 도청되거나 변조되지 않도록 보호합니다.
설정 파일 준비
- 서버 인증서와 키:
server.crt
,server.key
파일이 필요합니다. - 클라이언트 인증서와 키 (선택 사항): 클라이언트 인증서를 사용하여 추가적인 보안을 제공할 수 있습니다.
서버 코드 업데이트
ChatServer.h
#ifndef CHATSERVER_H
#define CHATSERVER_H
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast.hpp>
#include <memory>
#include <unordered_set>
#include <unordered_map>
#include <string>
#include <nlohmann/json.hpp>
namespace net = boost::asio;
namespace ssl = net::ssl;
namespace websocket = boost::beast::websocket;
using tcp = net::ip::tcp;
class ClientSession;
class ChatServer : public std::enable_shared_from_this<ChatServer> {
public:
ChatServer(net::io_context& ioc, ssl::context& ssl_ctx, tcp::endpoint endpoint)
: acceptor_(ioc), ssl_ctx_(ssl_ctx), socket_(ioc) {
acceptor_.open(endpoint.protocol());
acceptor_.set_option(net::socket_base::reuse_address(true));
acceptor_.bind(endpoint);
acceptor_.listen();
do_accept();
}
void broadcast(const nlohmann::json& message);
void join(std::shared_ptr<ClientSession> session, const std::string& username);
void leave(std::shared_ptr<ClientSession> session);
void updateUserList();
private:
void do_accept();
tcp::acceptor acceptor_;
ssl::context& ssl_ctx_;
tcp::socket socket_;
std::unordered_set<std::shared_ptr<ClientSession>> sessions_;
std::unordered_map<std::string, std::shared_ptr<ClientSession>> user_sessions_;
};
#endif // CHATSERVER_H
ClientSession.h
#ifndef CLIENTSESSION_H
#define CLIENTSESSION_H
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast.hpp>
#include <memory>
#include "ChatServer.h"
namespace net = boost::asio;
namespace ssl = net::ssl;
namespace websocket = boost::beast::websocket;
using tcp = net::ip::tcp;
class ClientSession : public std::enable_shared_from_this<ClientSession> {
public:
ClientSession(tcp::socket socket, ssl::context& ssl_ctx, ChatServer& server)
: ws_(std::move(socket), ssl_ctx), server_(server) {}
void start();
void send(const nlohmann::json& message);
private:
void do_handshake();
void do_read();
void do_write();
void handle_message(const std::string& message);
websocket::stream<ssl::stream<tcp::socket>> ws_;
ChatServer& server_;
boost::beast::flat_buffer buffer_;
std::vector<std::string> write_msgs_;
std::string username_;
};
#endif // CLIENTSESSION_H
ChatServer.cpp
#include "ChatServer.h"
#include "ClientSession.h"
void ChatServer::broadcast(const nlohmann::json& message) {
for (auto& session : sessions_) {
session->send(message);
}
}
void ChatServer::join(std::shared_ptr<ClientSession> session, const std::string& username) {
sessions_.insert(session);
user_sessions_[username] = session;
// 알림 메시지를 생성하여 모든 사용자에게 브로드캐스트
nlohmann::json notification;
notification["type"] = "join";
notification["username"] = username;
broadcast(notification);
updateUserList();
}
void ChatServer::leave(std::shared_ptr<ClientSession> session) {
sessions_.erase(session);
for (auto it = user_sessions_.begin(); it != user_sessions_.end(); ++it) {
if (it->second == session) {
// 알림 메시지를 생성하여 모든 사용자에게 브로드캐스트
nlohmann::json notification;
notification["type"] = "leave";
notification["username"] = it->first;
broadcast(notification);
user_sessions_.erase(it);
break;
}
}
updateUserList();
}
void ChatServer::updateUserList() {
nlohmann::json user_list_message;
user_list_message["type"] = "user_list";
for (const auto& user_session : user_sessions_) {
user_list_message["users"].push_back(user_session.first);
}
broadcast(user_list_message);
}
void ChatServer::do_accept() {
acceptor_.async_accept(socket_, [this](boost::system::error_code ec) {
if (!ec) {
auto session = std::make_shared<ClientSession>(std::move(socket_), ssl_ctx_, *this);
session->start();
}
do_accept();
});
}
ClientSession.cpp
#include "ClientSession.h"
#include <nlohmann/json.hpp>
void ClientSession::start() {
do_handshake();
}
void ClientSession::do_handshake() {
auto self(shared_from_this());
ws_.next_layer().async_handshake(ssl::stream_base::server, [this, self](boost::system::error_code ec) {
if (!ec) {
ws_.async_accept([this, self](boost::system::error_code ec) {
if (!ec) {
do_read();
} else {
server_.leave(shared_from_this());
}
});
} else {
server_.leave(shared_from_this());
}
});
}
void ClientSession::send(const nlohmann::json& message) {
auto self(shared_from_this());
net::post(ws_.get_executor(), [this, self, message]() {
bool write_in_progress = !write_msgs_.empty();
write_msgs_.push_back(message.dump());
if (!write_in_progress) {
do_write();
}
});
}
void ClientSession::do_read() {
auto self(shared_from_this());
ws_.async_read(buffer_, [this, self](boost::system::error_code ec, std::size_t length) {
if (!ec) {
auto message = boost::beast::buffers_to_string(buffer_.data());
handle_message(message);
buffer_.consume(length);
do_read();
} else {
server_.leave(shared_from_this());
}
});
}
void ClientSession::do_write() {
auto self(shared_from_this());
ws_.async_write(net::buffer(write_msgs_.front()), [this, self](boost::system::error_code ec, std::size_t length) {
if (!ec) {
write_msgs_.erase(write_msgs_.begin());
if (!write_msgs_.empty()) {
do_write();
}
} else {
server_.leave(shared_from_this());
}
});
}
void ClientSession::handle_message(const std::string& message) {
auto json_message = nlohmann::json::parse(message);
if (json_message.contains("username")) {
username_ = json_message["username"].get<std::string>();
server_.join(shared_from_this(), username_);
}
if (json_message.contains("message")) {
nlohmann::json broadcast_message;
broadcast_message["type"] = "message";
broadcast_message["username"] = username_;
broadcast_message["message"] = json_message["message"];
server_.broadcast(broadcast_message);
}
}
main.cpp
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include "ChatServer.h"
namespace net = boost::asio;
namespace ssl = net::ssl;
using tcp = net::ip::tcp;
int main() {
try {
net::io_context ioc{1};
ssl::context ctx{ssl::context::tlsv12};
ctx.set_options(ssl::context::default_workarounds
| ssl::context::no_sslv2
| ssl::context::single_dh_use);
ctx.use_certificate_chain_file("server.crt");
ctx.use_private_key_file("server.key", ssl::context::pem);
tcp::endpoint endpoint{net::ip::make_address("0.0.0.0"), 12345};
std::make_shared<ChatServer>(ioc, ctx, endpoint)->run();
ioc.run();
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
return 0;
}
클라이언트 코드 업데이트
ChatClient.h
#ifndef CHATCLIENT_H
#define CHATCLIENT_H
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <
boost/beast.hpp>
#include <memory>
#include <string>
#include <queue>
#include <iostream>
namespace net = boost::asio;
namespace ssl = net::ssl;
namespace websocket = boost::beast::websocket;
using tcp = net::ip::tcp;
class ChatClient : public std::enable_shared_from_this<ChatClient> {
public:
ChatClient(net::io_context& ioc, ssl::context& ssl_ctx, const tcp::resolver::results_type& endpoints)
: resolver_(ioc), ws_(ioc, ssl_ctx), endpoints_(endpoints) {}
void connect();
void disconnect();
void sendMessage(const std::string& message);
void setUsername(const std::string& username);
private:
void doRead();
void doWrite();
void doHandshake();
std::string formatMessage(const std::string& message);
tcp::resolver resolver_;
websocket::stream<ssl::stream<tcp::socket>> ws_;
tcp::resolver::results_type endpoints_;
boost::beast::flat_buffer buffer_;
std::queue<std::string> writeMessages_;
std::string username_;
};
#endif // CHATCLIENT_H
ChatClient.cpp
#include "ChatClient.h"
#include <nlohmann/json.hpp>
void ChatClient::connect() {
auto self(shared_from_this());
net::async_connect(ws_.next_layer().next_layer(), endpoints_,
[this, self](boost::system::error_code ec, tcp::endpoint) {
if (!ec) {
doHandshake();
} else {
std::cerr << "Connect failed: " << ec.message() << std::endl;
}
});
}
void ChatClient::doHandshake() {
auto self(shared_from_this());
ws_.next_layer().async_handshake(ssl::stream_base::client, [this, self](boost::system::error_code ec) {
if (!ec) {
ws_.async_handshake("localhost", "/",
[this, self](boost::system::error_code ec) {
if (!ec) {
doRead();
} else {
std::cerr << "Handshake failed: " << ec.message() << std::endl;
}
});
} else {
std::cerr << "SSL Handshake failed: " << ec.message() << std::endl;
}
});
}
void ChatClient::disconnect() {
auto self(shared_from_this());
ws_.async_close(websocket::close_code::normal,
[this, self](boost::system::error_code ec) {
if (ec) {
std::cerr << "Close failed: " << ec.message() << std::endl;
}
});
}
void ChatClient::sendMessage(const std::string& message) {
auto self(shared_from_this());
net::post(ws_.get_executor(), [this, self, message]() {
bool writeInProgress = !writeMessages_.empty();
writeMessages_.push(formatMessage(message));
if (!writeInProgress) {
doWrite();
}
});
}
void ChatClient::setUsername(const std::string& username) {
username_ = username;
}
std::string ChatClient::formatMessage(const std::string& message) {
nlohmann::json jsonMessage;
jsonMessage["username"] = username_;
jsonMessage["message"] = message;
return jsonMessage.dump();
}
void ChatClient::doRead() {
auto self(shared_from_this());
ws_.async_read(buffer_,
[this, self](boost::system::error_code ec, std::size_t length) {
if (!ec) {
std::cout << "Received: " << boost::beast::buffers_to_string(buffer_.data()) << std::endl;
buffer_.consume(length);
doRead();
} else {
std::cerr << "Read failed: " << ec.message() << std::endl;
disconnect();
}
});
}
void ChatClient::doWrite() {
auto self(shared_from_this());
ws_.async_write(net::buffer(writeMessages_.front()),
[this, self](boost::system::error_code ec, std::size_t length) {
if (!ec) {
writeMessages_.pop();
if (!writeMessages_.empty()) {
doWrite();
}
} else {
std::cerr << "Write failed: " << ec.message() << std::endl;
disconnect();
}
});
}
main.cpp
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include "ChatClient.h"
namespace net = boost::asio;
namespace ssl = net::ssl;
using tcp = net::ip::tcp;
int main() {
try {
net::io_context ioc;
ssl::context ctx{ssl::context::tlsv12};
ctx.set_verify_mode(ssl::verify_none); // 또는 서버의 인증서를 검증하도록 설정할 수 있습니다.
tcp::resolver resolver(ioc);
auto endpoints = resolver.resolve("localhost", "12345");
auto client = std::make_shared<ChatClient>(ioc, ctx, endpoints);
std::cout << "Enter your username: ";
std::string username;
std::getline(std::cin, username);
client->setUsername(username);
client->connect();
std::thread t([&ioc]() { ioc.run(); });
std::string message;
while (std::getline(std::cin, message)) {
client->sendMessage(message);
}
client->disconnect();
t.join();
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
return 0;
}
설명
위의 코드는 SSL/TLS를 사용하여 서버와 클라이언트 간의 통신을 암호화한 예제입니다.
- ChatServer 클래스:
- SSL 컨텍스트를 설정하여 서버가 SSL/TLS 통신을 지원하도록 합니다.
do_accept()
메서드에서 SSL/TLS 스트림을 초기화합니다.
- ClientSession 클래스:
- SSL/TLS 핸드셰이크를 수행하여 안전한 연결을 설정합니다.
- ChatClient 클래스:
- SSL 컨텍스트를 설정하여 클라이언트가 SSL/TLS 통신을 지원하도록 합니다.
doHandshake()
메서드를 추가하여 SSL/TLS 핸드셰이크를 수행합니다.
이제 스물여덟 번째 날의 학습을 마쳤습니다. SSL/TLS를 사용하여 실시간 채팅 애플리케이션의 통신을 암호화하는 방법을 학습했습니다.
질문이나 피드백이 있으면 언제든지 댓글로 남겨 주세요. 내일은 "프로젝트: 최적화 및 테스트"에 대해 학습하겠습니다.
반응형
'-----ETC----- > C++ 네트워크 프로그래밍 시리즈' 카테고리의 다른 글
[C++ 네트워크 프로그래밍] Day 30: 프로젝트: 배포 및 유지보수 (0) | 2024.08.01 |
---|---|
[C++ 네트워크 프로그래밍] Day 27: 프로젝트: 실시간 채팅 기능 구현 (0) | 2024.08.01 |
[C++ 네트워크 프로그래밍] Day 25: 프로젝트: 서버 개발 (1) (0) | 2024.08.01 |
[C++ 네트워크 프로그래밍] Day 26: 프로젝트: 서버 개발 (2) (0) | 2024.08.01 |
[C++ 네트워크 프로그래밍] Day 24: 프로젝트: 클라이언트 개발 (2) (0) | 2024.08.01 |