55、HTTPS服务器:使用OpenSSL实现HTTPS服务器、证书加载
说实话,HTTP 明文传输在局域网里玩玩还行,真要上生产环境,那就是裸奔。我早年做过一个物联网网关项目,设备上报的数据里包含设备密钥,结果因为用了 HTTP,被中间人抓包抓了个精光……嗯,从那以后,凡是涉及敏感信息的通信,我必上 HTTPS。
这一讲,咱们就聊聊怎么用 OpenSSL 库,把一个普通的 TCP 服务器升级成 HTTPS 服务器。说白了,就是在 socket 层外面包一层 SSL/TLS 加密。
HTTPS 的本质是什么?
HTTPS 不是新协议,它就是 HTTP over SSL/TLS。你想想看,HTTP 是应用层,TCP 是传输层,SSL/TLS 夹在中间,负责加密和解密。
数据流向是这样的:
客户端 → SSL加密 → TCP传输 → SSL解密 → 服务器
所以,实现 HTTPS 服务器,核心就两步:
- 加载证书和私钥
- 用 SSL 库接管 socket 的读写
核心要点: OpenSSL 帮你处理了握手、密钥协商、加密解密这些脏活累活。你只需要在 accept() 之后,把普通的 fd 包装成 SSL 对象即可。
环境准备:装好 OpenSSL 开发库
在 Ubuntu 上,我习惯这样装:
sudo apt-get install libssl-dev
CentOS 的话:
sudo yum install openssl-devel
编译时记得链接 -lssl -lcrypto:
gcc https_server.c -o https_server -lssl -lcrypto
小提示: 如果你用的是 OpenSSL 1.1.0 以上版本,注意 API 有些变化。我遇到过因为版本不匹配导致 SSL_CTX_new() 返回 NULL 的情况,查了半天才发现是库版本太老。
第一步:生成自签名证书
生产环境肯定要用 CA 签发的证书,但咱们做实验,自签名就够了。我一般用一行命令搞定:
openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt -days 365 -nodes
这条命令会生成两个文件:
| 文件 | 用途 |
|---|---|
| server.key | 私钥,必须保密 |
| server.crt | 证书,发给客户端 |
警告: 私钥文件权限一定要设置为 600,否则 OpenSSL 会报错。我曾经因为权限问题在线上排查了半小时,结果就是 chmod 600 server.key 的事。
第二步:初始化 SSL 上下文
这是整个流程的起点。我习惯把初始化逻辑封装成一个函数:
#include <openssl/ssl.h>
#include <openssl/err.h>
SSL_CTX* init_ssl_context() {
// 初始化 SSL 库
SSL_library_init();
OpenSSL_add_all_algorithms();
SSL_load_error_strings();
// 创建 SSL 上下文,使用 TLS 服务端方法
const SSL_METHOD *method = TLS_server_method();
SSL_CTX *ctx = SSL_CTX_new(method);
if (!ctx) {
ERR_print_errors_fp(stderr);
return NULL;
}
return ctx;
}
这里要注意,TLS_server_method() 是 OpenSSL 1.1.0 之后推荐的用法。老版本用的是 SSLv23_server_method(),但那个已经过时了。
第三步:加载证书和私钥
证书和私钥加载,我建议分开做,方便排查问题:
int load_certificates(SSL_CTX *ctx, const char *cert_file, const char *key_file) {
// 加载证书
if (SSL_CTX_use_certificate_file(ctx, cert_file, SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stderr);
return -1;
}
// 加载私钥
if (SSL_CTX_use_PrivateKey_file(ctx, key_file, SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stderr);
return -1;
}
// 验证私钥和证书是否匹配
if (!SSL_CTX_check_private_key(ctx)) {
fprintf(stderr, "私钥和证书不匹配\n");
return -1;
}
return 0;
}
为什么一定要调用 SSL_CTX_check_private_key? 我遇到过证书和私钥不匹配的情况,客户端握手时直接报错。这个检查能帮你提前发现问题,而不是等到线上才暴露。
第四步:创建 SSL 对象并握手
当 accept() 返回一个新的 socket fd 后,我们需要把它包装成 SSL 对象:
SSL* ssl = SSL_new(ctx);
SSL_set_fd(ssl, client_fd);
// 开始 TLS 握手
if (SSL_accept(ssl) <= 0) {
ERR_print_errors_fp(stderr);
SSL_free(ssl);
close(client_fd);
return;
}
// 握手成功,可以收发数据了
char buffer[1024];
int bytes = SSL_read(ssl, buffer, sizeof(buffer) - 1);
if (bytes > 0) {
buffer[bytes] = '\0';
printf("收到: %s\n", buffer);
const char *response = "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello HTTPS!";
SSL_write(ssl, response, strlen(response));
}
// 关闭 SSL 连接
SSL_shutdown(ssl);
SSL_free(ssl);
close(client_fd);
你看,核心就是 SSL_accept() 代替了普通的 accept,SSL_read()/SSL_write() 代替了 read()/write()。其他逻辑几乎一模一样。
完整示例:一个极简 HTTPS 服务器
我把上面几段拼起来,就是一个能跑起来的 HTTPS 服务器了:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#define PORT 4433
int main() {
SSL_CTX *ctx = init_ssl_context();
if (!ctx) return -1;
if (load_certificates(ctx, "server.crt", "server.key") < 0) {
SSL_CTX_free(ctx);
return -1;
}
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(PORT),
.sin_addr.s_addr = INADDR_ANY
};
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, 10);
printf("HTTPS 服务器启动,端口: %d\n", PORT);
while (1) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &len);
SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, client_fd);
if (SSL_accept(ssl) <= 0) {
ERR_print_errors_fp(stderr);
} else {
char buf[1024] = {0};
SSL_read(ssl, buf, sizeof(buf));
printf("收到请求:\n%s\n", buf);
const char *resp = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, HTTPS!";
SSL_write(ssl, resp, strlen(resp));
}
SSL_shutdown(ssl);
SSL_free(ssl);
close(client_fd);
}
close(server_fd);
SSL_CTX_free(ctx);
return 0;
}
用浏览器访问 https://localhost:4433,会提示证书不安全,因为咱们用的是自签名证书。点「继续访问」就能看到 "Hello, HTTPS!" 了。
核心流程图
下面这张图,把 HTTPS 服务器的启动和请求处理流程串起来了:
避坑指南
我这些年踩过的坑,列几个典型的:
- 证书路径问题: 程序启动时,当前工作目录可能不是你放证书的目录。我建议用绝对路径,或者把证书路径做成配置项。
- SSL_accept 阻塞: 如果客户端连接后不发起握手,SSL_accept 会一直阻塞。生产环境一定要配合 select/poll/epoll 使用,或者设置超时。
- 内存泄漏: 每个 SSL 对象都要记得 SSL_free,每个 SSL_CTX 都要 SSL_CTX_free。我见过线上服务跑了一周后 OOM,查下来就是 SSL 对象没释放。
- 协议版本: 默认 TLS_server_method 支持 TLS 1.0 到 1.3。如果你只想用高版本,可以用
SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION)限制一下。
我的习惯: 在开发环境用自签名证书,在测试环境用 Let's Encrypt 的免费证书,生产环境才用商业 CA 签发的证书。这样分层,既省钱又安全。
好了,这一讲的内容就到这。HTTPS 服务器说白了就是给 socket 套了一层 SSL 壳,核心是证书加载和 SSL 对象管理。你只要把这两块搞明白,剩下的就是普通 socket 编程那套东西了。
公众号:蓝海资料掘金营,微信deep3321