53、HTTPS基础:SSL/TLS协议简介、OpenSSL库基本使用
说到网络编程,HTTP 大家肯定都熟。但 HTTP 是明文传输的,你发的数据包就像明信片,谁都能看。我早年做项目时,就吃过这个亏——用户登录信息在公网上裸奔,被中间人截获了。从那以后,我对加密传输就特别上心。
HTTPS,说白了就是 HTTP + SSL/TLS。SSL/TLS 在传输层之上加了一层加密隧道。今天我们就来聊聊这个隧道是怎么工作的,以及怎么用 OpenSSL 库在 C 代码里把它搭起来。
SSL/TLS 协议到底干了什么?
SSL/TLS 协议的核心就三件事:
- 身份认证:确认你连接的是真服务器,不是冒牌货
- 密钥协商:双方安全地商量出一个对称加密密钥
- 数据加密:用协商好的密钥加密后续通信内容
你想想看,如果每次通信都用同一个密钥,那密钥泄露就全完了。所以 TLS 用了一套混合加密体系——非对称加密来交换密钥,对称加密来加密数据。这样既安全又高效。
核心要点:TLS 握手阶段使用非对称加密(RSA/ECDHE),数据传输阶段使用对称加密(AES/ChaCha20)。
一次典型的 TLS 握手流程
我画了一张图,帮你理清握手过程。这个流程我调试过无数次,每次出问题基本都是证书或密码套件不匹配。
嗯,这里要注意:第③步客户端验证证书时,会检查证书是否由可信 CA 签发、域名是否匹配、证书是否过期。我曾经在生产环境遇到过证书过期导致握手失败的情况,排查了半天才发现是证书忘了续期。
OpenSSL 库的基本使用
OpenSSL 是 C 语言里最常用的 SSL/TLS 库。它功能很全,但 API 设计得有点复杂。我刚开始用的时候也被绕晕过。其实核心就几个步骤:
1. 初始化库
#include <openssl/ssl.h>
#include <openssl/err.h>
SSL_library_init();
OpenSSL_add_all_algorithms();
SSL_load_error_strings();
这三行是固定套路。我个人习惯在程序启动时就调用一次,后面就不用再管了。
2. 创建 SSL 上下文
SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
if (!ctx) {
// 处理错误
}
服务端用 TLS_server_method(),客户端用 TLS_client_method()。别搞反了,我见过有人把服务端写成客户端方法,结果握手一直失败。
3. 加载证书和私钥(服务端)
SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM);
SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM);
// 验证私钥是否与证书匹配
if (!SSL_CTX_check_private_key(ctx)) {
// 私钥与证书不匹配
}
警告:私钥文件一定要设置严格的权限,建议 600。我曾经把私钥权限设成 644,结果被同事不小心 cat 出来发到了群里……
4. 建立 SSL 连接
// 假设已经建立了 TCP 连接,得到 socket fd
SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, sockfd);
// 服务端
SSL_accept(ssl);
// 客户端
SSL_connect(ssl);
这一步是把 SSL 层绑定到已有的 TCP socket 上。之后的数据读写就用 SSL_read() 和 SSL_write() 代替 read() 和 write()。
5. 数据传输与清理
// 发送数据
SSL_write(ssl, "Hello, HTTPS!", 13);
// 接收数据
char buf[1024];
int len = SSL_read(ssl, buf, sizeof(buf));
// 关闭连接
SSL_shutdown(ssl);
SSL_free(ssl);
SSL_CTX_free(ctx);
一个完整的客户端示例
下面这个例子,是我在实际项目中提取出来的简化版。它连接一个 HTTPS 服务器,发送一个 GET 请求,然后打印响应。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
int main() {
// 1. 初始化 OpenSSL
SSL_library_init();
OpenSSL_add_all_algorithms();
SSL_load_error_strings();
// 2. 创建 SSL 上下文
SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
if (!ctx) {
fprintf(stderr, "SSL_CTX_new failed\n");
return 1;
}
// 3. 建立 TCP 连接
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct hostent *host = gethostbyname("www.example.com");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(443);
memcpy(&addr.sin_addr, host->h_addr, host->h_length);
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("connect");
return 1;
}
// 4. 绑定 SSL 到 socket
SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, sock);
if (SSL_connect(ssl) <= 0) {
ERR_print_errors_fp(stderr);
return 1;
}
// 5. 发送 HTTP 请求
const char *req = "GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n";
SSL_write(ssl, req, strlen(req));
// 6. 读取响应
char buf[4096];
int len;
while ((len = SSL_read(ssl, buf, sizeof(buf) - 1)) > 0) {
buf[len] = '\0';
printf("%s", buf);
}
// 7. 清理
SSL_shutdown(ssl);
SSL_free(ssl);
close(sock);
SSL_CTX_free(ctx);
return 0;
}
小提示:编译时需要链接 OpenSSL 库:gcc -o https_client https_client.c -lssl -lcrypto
常见问题与避坑指南
| 问题 | 原因 | 解决方法 |
|---|---|---|
| SSL_connect 返回 -1 | 证书验证失败或密码套件不匹配 | 检查证书链是否完整,服务端是否支持客户端提供的密码套件 |
| SSL_read 返回 0 | 对端关闭了连接 | 检查是否调用了 SSL_shutdown,或者网络异常断开 |
| 证书验证警告 | 自签名证书或过期证书 | 生产环境使用正规 CA 签发的证书,测试环境可以设置 SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL) |
| 内存泄漏 | 没有正确释放 SSL 对象 | 确保每个 SSL_new 对应一个 SSL_free,每个 SSL_CTX_new 对应一个 SSL_CTX_free |
我记得有一次线上服务突然大量报 SSL 握手超时。查了半天,发现是证书文件被运维误删了。从那以后,我写了个监控脚本,每分钟检查一次证书文件是否存在、是否过期。
总结一下
SSL/TLS 其实没那么神秘。它就是一套加密握手协议,加上 OpenSSL 这个工具库。你只要记住:
- 初始化库 → 创建上下文 → 加载证书 → 绑定 socket → 握手 → 读写数据 → 清理
- 证书和私钥是命根子,一定要保护好
- 错误处理不能偷懒,每个 API 调用都要检查返回值
说白了,HTTPS 编程就是给普通的 TCP 通信套上了一层加密壳。你掌握了这个壳的用法,就能写出安全的网络应用了。