C++ 开发 Web 服务框架 - HTTPS 的原理及其 Web 框架的设计与实(三)

    xiaoxiao2021-04-15  29

    C++ 开发 Web 服务框架 - HTTPS 的原理及其 Web 框架的设计与实现

    一、概述

    项目介绍

    服务器开发中 Web 服务是一个基本的代码单元,将服务端的请求和响应部分的逻辑抽象出来形成框架,能够做到最高级别的框架级代码复用。本次项目将综合使用 C++11 及 Boost 中的 Asio 实现 HTTP 和 HTTPS 的服务器框架。

    项目涉及的知识点

    C++基本知识 面向对象模板命名空间常用 IO 库 C++11 相关 lambda expressionstd::shared_ptrstd::make_sharedstd::unordered_mapstd::regexstd::smatchstd::regex_matchstd::functionstd::thread Boost Asio 相关 boost::asio::io_serviceboost::asio::ip::tcp::socketboost::asio::ip::tcp::v4()boost::asio::ip::tcp::endpointboost::asio::ip::tcp::acceptorboost::asio::streambufboost::asio::async_readboost::asio::async_read_untilboost::asio::async_writeboost::asio::transfer_exactlyboost::asio::ssl::streamboost::asio::ssl::stream_base::serverboost::asio::ssl::contextboost::asio::ssl::context::sslv23boost::asio::ssl::context::pemboost::system::error_code

    编译环境提示

    本次实验中的代码使用了 C++11 标准库中的正则表达式库,在 g++ 4.9 之前, regex 库并不支持 ECMAScript 的正则语法,因此需要将 g++ 版本升级至 4.9 以上。

    // 下面的这段代码可以测试你的编译器对正则表达式的支持情况 #include <iostream> #include <regex> int main() { std::regex r1("S"); printf("S works.\n"); std::regex r2("."); printf(". works.\n"); std::regex r3(".+"); printf(".+ works.\n"); std::regex r4("[0-9]"); printf("[0-9] works.\n"); return 0; }

    如果你的运行结果遇到了下图所示的错误,说明你确实需要升级你的 g++ 了:

    使用 g++ -v 可以查看到当前编译器版本:

    如果你最后一行中的 gcc version 显示的是 4.8.x,那么你需要手动将编译器版本升级至 4.9 以上,方法如下:

    # 安装 add-apt-repository 工具 sudo apt-get install software-properties-common # 增加源 sudo add-apt-repository ppa:ubuntu-toolchain-r/test # 更新源 sudo apt-get update # 更新安装 sudo apt-get upgrade # 安装 gcc/g++ 4.9 sudo apt-get install gcc-4.9 g++-4.9 # 更新链接 sudo updatedb sudo ldconfig sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 48 \ --slave /usr/bin/g++ g++ /usr/bin/g++-4.8 \ --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-4.8 \ --slave /usr/bin/gcc-nm gcc-nm /usr/bin/gcc-nm-4.8 \ --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-4.8 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.9 49 \ --slave /usr/bin/g++ g++ /usr/bin/g++-4.9 \ --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-4.9 \ --slave /usr/bin/gcc-nm gcc-nm /usr/bin/gcc-nm-4.9 \ --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-4.9

    二、实现 HTTPS 框架

    在上一节实验中,我们已经实现了 HTTP 的框架,利用这个框架,我们便能更加方便的进行框架级的复用。Boost Asio 包含了一个类及类模板用于对基本的 SSL 进行支持,这个类使我们实现 HTTPS 服务器成为可能。从实现上来看,我们只需要对已经存在的流再进行一层加密封装,比如加密 TCP Socket。这个过程异常的简单,我们只需稍加利用即可实现整个HTTPS 的框架,如下:

    // // server_https.hpp // web_server // created by changkun at shiyanlou.com // #ifndef SERVER_HTTPS_HPP #define SERVER_HTTPS_HPP #include "server_http.hpp" #include <boost/asio/ssl.hpp> namespace ShiyanlouWeb { // 定义 HTTPS 类型 typedef boost::asio::ssl::stream<boost::asio::ip::tcp::socket> HTTPS; // 定义 HTTPS 服务, 模板类型为 HTTPS template<> class Server<HTTPS> : public ServerBase<HTTPS> { public: // 一个 HTTPS 的服务器比 HTTP 服务器多增加了两个参数,一个是证书文件,另一个是私钥文件 Server(unsigned short port, size_t num_threads, const std::string& cert_file, const std::string& private_key_file) : ServerBase<HTTPS>::ServerBase(port, num_threads), context(boost::asio::ssl::context::sslv23) { // 使用证书文件 context.use_certificate_chain_file(cert_file); // 使私钥文件, 相比之下需要多传入一个参数来指明文件的格式 context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem); } private: // 和 HTTP 服务器相比,需要多定义一个 ssl context 对象 boost::asio::ssl::context context; // HTTPS 服务器和 HTTP 服务器相比 // 其区别在于对 socket 对象的构造方式有所不同 // HTTPS 会在 socket 这一步中对 IO 流进行加密 // 因此实现的 accept() 方法需要对 socket 用 ssl context 初始化 void accept() { // 为当前连接创建一个新的 socket // Shared_ptr 用于传递临时对象给匿名函数 // socket 类型会被推导为: std::shared_ptr<HTTPS> auto socket = std::make_shared<HTTPS>(m_io_service, context); acceptor.async_accept( (*socket).lowest_layer(), [this, socket](const boost::system::error_code& ec) { // 立即启动并接受一个新连接 accept(); // 处理错误 if(!ec) { (*socket).async_handshake(boost::asio::ssl::stream_base::server, [this, socket](const boost::system::error_code& ec) { if(!ec) process_request_and_respond(socket); }); } }); } }; } #endif /* SERVER_HTTPS_HPP */

    在上面整个过程中,我们仅仅只是重新实现了 accept() 方法,将启用一个 HTTPS 服务器需要的两个文件传递给了 Boost Asio,就完成了整个服务器框架。

    三、开发 HTTPS 服务器

    我们已经在上一节实验中写过了 handler.hpp,这个文件中实现了服务器资源的访问和处理逻辑。而这部分逻辑,本质上是独立于服务器类型而存在的,因此我们根本不需要在进行任何开发,只需将 main_http.cpp 中的服务器类型修改就能获得一个完整的 HTTPS 服务器:

    // // main_https.cpp // web_server // created by changkun at shiyanlou.com // #include "server_https.hpp" #include "handler.hpp" using namespace ShiyanlouWeb; int main() { //HTTPS 服务运行在 12345 端口,并启用四个线程 Server<HTTPS> server(12345, 4, "server.crt", "server.key"); start_server<Server<HTTPS>>(server); return 0; }

    在这个服务器上,我们额外传入了 HTTPS 服务器需要的证书和秘钥文件。

    为了编译我们的 HTTPS 服务器,在 Makefile 中增加对 HTTPS 服务器的编译选项:

    # # Makefile # web_server # # created by changkun at shiyanlou.com # CXX = g++ EXEC_HTTP = server_http EXEC_HTTPS = server_https SOURCE_HTTP = main_http.cpp SOURCE_HTTPS = main_https.cpp OBJECTS_HTTP = main_http.o OBJECTS_HTTPS = main_https.o LDFLAGS_COMMON = -std=c++11 -O3 -pthread -lboost_system LDFLAGS_HTTP = LDFLAGS_HTTPS = -lssl -lcrypto LPATH_COMMON = -I/usr/include/boost LPATH_HTTP = LPATH_HTTPS = -I/usr/include/openssl LLIB_COMMON = -L/usr/lib all: make http make https http: $(CXX) $(SOURCE_HTTP) $(LDFLAGS_COMMON) $(LDFLAGS_HTTP) $(LPATH_COMMON) $(LPATH_HTTP) $(LLIB_COMMON) $(LLIB_HTTP) -o $(EXEC_HTTP) https: $(CXX) $(SOURCE_HTTPS) $(LDFLAGS_COMMON) $(LDFLAGS_HTTPS) $(LPATH_COMMON) $(LPATH_HTTPS) $(LLIB_COMMON) $(LLIB_HTTPS) -o $(EXEC_HTTPS) clean: rm -f $(EXEC_HTTP) $(EXEC_HTTPS) *.o

    这时,我们的整个目录树为:

    src ├── Makefile ├── handler.hpp ├── main_http.cpp ├── main_https.cpp ├── server_base.hpp ├── server_http.hpp ├── server_https.hpp └── www ├── index.html └── test.html

    现在,我们可以:

    使用 make 一次性编译 http 和 https 服务器;使用 make http 单独编译 http 服务器;使用 make https 单独编译 https 服务器.

    完成了编译后还不够,我们还需要对创建 HTTPS 服务器所需的证书。

    四、创建证书文件

    第一步:生成私钥

    openssl 工具包提供了一个生成 RSA 私钥和 CSR(Certificate Signing Request) 文件的工具。这使得我们可以将其用于生成自签名的证书,从而用于供给 HTTPS 服务器使用。

    首先,就是要生成 RSA 私钥。我们生成一个 1024 位的 RSA 秘钥,并使用三重 DES 加密方式,并按照 PEM 格式存储(在库中我们指定了私钥的格式是boost::asio::ssl::context::pem):

    openssl genrsa -des3 -out server.key 1024

    如图所示,在产生 server.key 时,我们还被要求设置密码,这个密码保护了当别人尝试访问这个私钥时,需要提供密码(作为演示,不妨设置成 123456): 

    完成后,可以看到产生了 server.key 这个文件。

    第二步:生成 CSR

    私钥生成后,就可以据此生成一个 CSR 文件了:

    openssl req -new -key server.key -out server.csr

    在生成 CSR 文件的过程中,会被要求输入刚才我们设置的保护密码,同时还需要输入一些相关的信息,例如这个证书会被用在哪个域名下。最后会要求设置一个 challenge passwrod,通常不去设置这个密码。如图:

    第三步:从秘钥中移除密码

    如果证书有密码,那么每次使用证书时都讲需要输入一次密码,这不是很方便,况且,秘钥证书位于我们服务器上,不太容易被泄露,因此我们可以将秘钥中的密码移除,首先我们先保存一份秘钥的备份:

    cp server.key server.key.old openssl rsa -in server.key.old -out server.key

    第四步:生成自签名证书

    最后,生成一个自签名的证书,并设置证书的过期时间为一年:

    openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

    至此,我们便完成了所有的步骤,现在我们可以运行服务器:

    ./server_https

    然后在浏览器中访问现在这个运行在 12345 端口的 https 服务器了,输入:https://localhost:12345

    这时,我们会看到浏览器正在告诉我们这个链接不安全。

    这是由于我们的证书是自签名的产生的原因。一般情况下,自签名的 SSL 证书可以随意的签发,没有第三方监督的审核,并不能收到浏览器的信任。这就非常容易造成伪造证书的中间人攻击,从而导致劫持 SSL 加密流量。

    我们刚才在创建证书的时候,指定了这个证书会被用于 shiyanlou.com 这个域名,而实际上我们在访问时,访问的 URL 是 localhost,这时浏览器识别到这个不同,也就阻止了这次连接。

    为了测试,我们可以将本次连接添加新人列表中,增加一个安全例外:

    这样我们就能看到使用 HTTPS 访问到的资源内容了:

    可惜的是,我们依然不能做到像『正经』厂商一样,让那一把小锁变成绿色:

    原因就如同之前我们所提到的那样,SSL 证书受到第三方监管,浏览器信任的证书一般来自国外的几个指明 SSL 证书签发商,而这种证书的签发往往需要向签发商支付一定的费用,虽然也有诸如 StartSSL 这样的提供免费 SSL 证书的签发商,但由于我们没有域名进行测试,这里就不再赘述了。

    总结

    经过本次项目,我们走过了很多艰难的历程。首先,我们基于 C++11 和 Boost Asio 的诸多特性,开发了一个 HTTP 服务器的 Web 框架,为了测试我们的框架,我们编写了自己的 HTTP 服务器。

    我们的设计非常巧妙,在完成 HTTP 服务器 Web 框架和相关测试代码后,进一步扩展为 HTTPS 时,只使用了极少的代码量便完成了整个框架的开发。

    我们的开发的框架一共包含三个文件:

    server_base.hppserver_http.hppserver_https.hpp

    而我们基于这个框架,开发了简易的 http 和 https 的 web 服务器,但我们依然复用了服务器实际逻辑的代码,写在了:

    handler.hpp

    之中。此外,我们基于这套框架实现的 http 和 https 服务器在本质上,只有一行代码的不同:

    // http server: Server<HTTP> server(12345, 4); // https server: Server<HTTPS> server(12345, 4, "server.crt", "server.key");

    作为参考,这里附上本次项目中全部的代码,你可以使用 wget 来获取:

    http://labfile.oss.aliyuncs.com/courses/568/web_server.zip

    值得一提的是,里面没有包含 SSL 证书,你需要自己手动创建它们。

    参考资料

    C++ 11/14/17 标准在各编译器下支持情况 C++ 标准库文档 创建自签名 SSL 证书 出自:https://www.shiyanlou.com/courses/568/labs/1985/document
    转载请注明原文地址: https://ju.6miu.com/read-671609.html

    最新回复(0)