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 握手流程

我画了一张图,帮你理清握手过程。这个流程我调试过无数次,每次出问题基本都是证书或密码套件不匹配。

客户端 服务端 ① ClientHello(支持的TLS版本、密码套件) ② ServerHello + 证书 + ServerHelloDone ③ 客户端验证证书,生成预主密钥(Pre-Master Secret) ④ ClientKeyExchange(用服务器公钥加密的预主密钥) ⑤ 双方各自计算会话密钥,发送ChangeCipherSpec ⑥ 加密通信开始(对称加密)

嗯,这里要注意:第③步客户端验证证书时,会检查证书是否由可信 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 通信套上了一层加密壳。你掌握了这个壳的用法,就能写出安全的网络应用了。

公众号:蓝海资料掘金营,微信 deep3321