웹엔진 위젯 클라이언트 인증서 예시
Qt WebEngine 및 QSslServer 을 사용한 간단한 클라이언트 인증서 인증 시나리오.
이 예제에서는 클라이언트 인증서 인증 워크플로우를 보여드리겠습니다. 제시된 인증 시나리오는 예를 들어 기능을 처리하기 위한 웹 인터페이스를 제공하는 임베디드 장치에 대해 구현할 수 있습니다. 관리자는 Qt WebEngine 기반 클라이언트를 사용하여 임베디드 디바이스를 유지 관리하고 사용자 지정 SSL 인증서를 사용하여 인증합니다. 연결은 SSL 소켓으로 암호화됩니다. 임베디드 디바이스는 QSslSocket
을 사용하여 인증 및 암호화를 처리합니다. 이렇게 하면 관리자는 자격 증명을 입력할 필요 없이 장치에서 인식하는 적절한 인증서를 선택하기만 하면 됩니다.
이 예에서는 워크플로우를 보여주기 위해 매우 간단하고 최소한의 접근 방식에 중점을 두었습니다. QSslSocket 은 리소스가 제한된 임베디드 디바이스에서 완전한 HTTPS 서버를 실행할 필요가 없으므로 낮은 수준의 솔루션이라는 점에 유의하세요.
인증서 만들기
이 예제에는 이미 생성된 인증서가 제공되지만 새 인증서를 생성하는 방법을 살펴봅시다. OpenSSL 도구를 사용하여 서버와 클라이언트에 대한 인증서를 만듭니다.
먼저 인증서 서명 요청 CSR
을 만들고 서명합니다. CA 개인 키를 사용하여 클라이언트와 서버에 대한 로컬 인증서를 모두 서명하고 발급합니다.
openssl req -out ca.pem -new -x509 -nodes -keyout ca.key
참고: 기본 인증서 유효 기간인 30일을 재정의하려면 -days
옵션을 지정하세요.
이제 클라이언트와 서버에 대한 두 개의 개인 키를 만들어 보겠습니다:
openssl genrsa -out client.key 2048
openssl genrsa -out server.key 2048
다음으로 두 개의 인증서 서명 요청이 필요합니다:
openssl req -key client.key -new -out client.req
openssl req -key server.key -new -out server.req
이제 CSR에서 두 인증서를 발급해 보겠습니다:
openssl x509 -req -in client.req -out client.pem -CA ca.pem -CAkey ca.key
openssl x509 -req -in server.req -out server.pem -CA ca.pem -CAkey ca.key
클라이언트 인증서 주체 및 일련 번호가 인증 중에 선택할 수 있도록 표시됩니다. 일련 번호는 인쇄할 수 있습니다:
openssl x509 -serial -noout -in client.pem
클라이언트 구현
이제 웹 브라우저 클라이언트를 구현할 수 있습니다.
인증서와 개인 키를 로드하고 QSslCertificate 및 QSslKey 인스턴스를 만드는 것으로 시작합니다.
QFile certFile(":/resources/client.pem"); certFile.open(QIODevice::ReadOnly); const QSslCertificate cert(certFile.readAll(), QSsl::Pem); QFile keyFile(":/resources/client.key"); keyFile.open(QIODevice::ReadOnly); const QSslKey sslKey(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, "");
이제 인증서와 개인 키를 QWebEngineClientCertificateStore 에 추가합니다.
QWebEngineProfile::defaultProfile()->clientCertificateStore()->add(cert, sslKey);
인증서를 처리하려면 QWebEnginePage 의 인스턴스를 만들고 두 개의 싱글 QWebEnginePage::certificateError 과 QWebEnginePage::selectClientCertificate 에 연결해야 합니다. 첫 번째는 자체 서명된 서버 인증서가 인증서 오류를 트리거하므로 인증을 진행하기 위해 수락해야 하므로 첫 번째 인증서만 필요합니다. 프로덕션 환경에서는 자체 서명된 인증서를 사용하지 않으므로 이 예에서는 적절한 인증서를 제공하지 않기 위해 QWebEngineCertificateError 을 처리합니다. 개인 키는 비밀이므로 절대 공개해서는 안 됩니다.
QWebEnginePage page; QObject::connect(&page, &QWebEnginePage::certificateError, [](QWebEngineCertificateError e) { e.acceptCertificate(); });
QWebEnginePage::selectClientCertificate 에 대한 처리는 QDialog 을 표시하고 QListWidget 에 선택할 수 있는 클라이언트 인증서 목록을 표시합니다. 그런 다음 사용자가 선택한 인증서가 QWebEngineClientCertificateSelection::select 호출로 전달됩니다.
QObject::connect( &page, &QWebEnginePage::selectClientCertificate, &page, [&cert](QWebEngineClientCertificateSelection selection) { QDialog dialog; QVBoxLayout *layout = new QVBoxLayout; QLabel *label = new QLabel(QLatin1String("Select certificate")); QListWidget *listWidget = new QListWidget; listWidget->setSelectionMode(QAbstractItemView::SingleSelection); QPushButton *button = new QPushButton(QLatin1String("Select")); layout->addWidget(label); layout->addWidget(listWidget); layout->addWidget(button); QObject::connect(button, &QPushButton::clicked, [&dialog]() { dialog.accept(); }); const QList<QSslCertificate> &list = selection.certificates(); for (const QSslCertificate &cert : list) { listWidget->addItem(cert.subjectDisplayName() + " : " + cert.serialNumber()); } dialog.setLayout(layout); if (dialog.exec() == QDialog::Accepted) selection.select(list[listWidget->currentRow()]); else selection.selectNone(); });
마지막으로 QWebEnginePage 에 대한 QWebEngineView 를 만들고 서버 URL을 로드하여 페이지를 표시합니다.
QWebEngineView view(&page); view.setUrl(QUrl("https://localhost:5555")); view.resize(800, 600); view.show();
서버 구현하기
임베디드 디바이스의 경우 최소한의 HTTPS 서버를 개발하겠습니다. QSslServer 을 사용하여 들어오는 연결을 처리하고 QSslSocket 인스턴스를 제공할 수 있습니다. 이를 위해 QSslServer 인스턴스를 생성하고 클라이언트 설정과 유사하게 서버 인증서와 개인 키를 로드합니다. 그런 다음 QSslCertificate 및 QSslKey 개체를 적절히 생성합니다. 또한 서버가 클라이언트가 제시한 인증서의 유효성을 검사할 수 있도록 CA 인증서가 필요합니다. CA 및 로컬 인증서는 QSslConfiguration 로 설정하고 나중에 서버에서 사용합니다.
QSslServer server; QSslConfiguration configuration(QSslConfiguration::defaultConfiguration()); configuration.setPeerVerifyMode(QSslSocket::VerifyPeer); QFile keyFile(":/resources/server.key"); keyFile.open(QIODevice::ReadOnly); QSslKey key(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey); configuration.setPrivateKey(key); QList<QSslCertificate> localCerts = QSslCertificate::fromPath(":/resources/server.pem"); configuration.setLocalCertificateChain(localCerts); QList<QSslCertificate> caCerts = QSslCertificate::fromPath(":resources/ca.pem"); configuration.addCaCertificates(caCerts); server.setSslConfiguration(configuration);
다음으로 서버가 포트에서 들어오는 연결을 수신 대기하도록 설정합니다. 5555
if (!server.listen(QHostAddress::LocalHost, 5555)) qFatal("Could not start server on localhost:5555"); else qInfo("Server started on localhost:5555");
당사는 QTcpServer::pendingConnectionAvailable 신호에 람다 함수를 제공하여 들어오는 연결에 대한 처리를 구현합니다. 이 신호는 인증이 성공하고 socket
TLS 암호화가 시작된 후에 트리거됩니다.
QObject::connect(&server, &QTcpServer::pendingConnectionAvailable, [&server]() { QTcpSocket *socket = server.nextPendingConnection(); Q_ASSERT(socket); QPointer<Request> request(new Request); QObject::connect(socket, &QAbstractSocket::disconnected, socket, [socket, request]() mutable { delete request; socket->deleteLater(); });
위에서 사용된 Request
객체는 메모리 관리를 돕기 위해 QPointer 을 사용하기 때문에 QByteArray 을 감싸는 간단한 래퍼입니다. 이 객체는 들어오는 HTTP 데이터를 수집합니다. 요청이 완료되거나 소켓이 종료되면 삭제됩니다.
struct Request : public QObject { QByteArray m_data; };
요청에 대한 응답은 요청된 URL에 따라 달라지며, 소켓을 통해 HTML 페이지 형태로 다시 전송됩니다. GET
루트 요청의 경우 관리자에게 Access Granted
메시지와 Exit
HTML 버튼이 표시됩니다. 관리자가 이 버튼을 클릭하면 클라이언트가 또 다른 요청을 보냅니다. 이번에는 /exit
상대 URL을 사용하여 서버 종료를 트리거합니다.
QObject::connect(socket, &QTcpSocket::readyRead, socket, [socket, request]() mutable { request->m_data.append(socket->readAll()); if (!request->m_data.endsWith("\r\n\r\n")) return; socket->write(http_ok); socket->write(html_start); if (request->m_data.startsWith("GET / ")) { socket->write("<p>ACCESS GRANTED !</p>"); socket->write("<p>You reached the place, where no one has gone before.</p>"); socket->write("<button onclick=\"window.location.href='/exit'\">Exit</button>"); } else if (request->m_data.startsWith("GET /exit ")) { socket->write("<p>BYE !</p>"); socket->write("<p>Have good day ...</p>"); QTimer::singleShot(0, &QCoreApplication::quit); } else { socket->write("<p>There is nothing to see here.</p>"); } socket->write(html_end); delete request; socket->disconnectFromHost(); });
예제를 실행하려면 server
를 시작한 다음 client
을 시작합니다. 인증서를 선택하면 Access Granted
페이지가 표시됩니다.
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.