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
现在,我们可以:
- 使用
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.hpp
- server_http.hpp
- server_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 证书,你需要自己手动创建它们。
参考资料
出自:https://www.shiyanlou.com/courses/568/labs/1985/document