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

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

一、概述

项目介绍

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

项目涉及的知识点

  • C++基本知识
    • 面向对象
    • 模板
    • 命名空间
    • 常用 IO 库
  • C++11 相关
    • lambda expression
    • std::shared_ptr
    • std::make_shared
    • std::unordered_map
    • std::regex
    • std::smatch
    • std::regex_match
    • std::function
    • std::thread
  • Boost Asio 相关
    • boost::asio::io_service
    • boost::asio::ip::tcp::socket
    • boost::asio::ip::tcp::v4()
    • boost::asio::ip::tcp::endpoint
    • boost::asio::ip::tcp::acceptor
    • boost::asio::streambuf
    • boost::asio::async_read
    • boost::asio::async_read_until
    • boost::asio::async_write
    • boost::asio::transfer_exactly
    • boost::asio::ssl::stream
    • boost::asio::ssl::stream_base::server
    • boost::asio::ssl::context
    • boost::asio::ssl::context::sslv23
    • boost::asio::ssl::context::pem
    • boost::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

现在,我们可以:

  1. 使用 make 一次性编译 http 和 https 服务器;
  2. 使用 make http 单独编译 http 服务器;
  3. 使用 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 时,只使用了极少的代码量便完成了整个框架的开发。

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

  1. server_base.hpp
  2. server_http.hpp
  3. server_https.hpp

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

  1. 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 证书,你需要自己手动创建它们。

参考资料



出自:https://www.shiyanlou.com/courses/568/labs/1985/document
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值