目录
本来我们可以使用短信验证的,但是由于国家政策,个人无法再使用这个短信业务,只有企业才有短信服务资质

这样子的话,我们就不能使用短信来进行这个验证了。
因此,我们这里采用了邮件来发送我们的验证码
一.libcurl
安装libcurl
sudo apt install libcurl4-openssl-dev
libcurl 是一个功能强大且广泛使用的客户端网络传输库,它以跨平台、开源、轻量级和高性能著称,是许多应用程序和工具实现网络通信的基础。
什么是 libcurl?
libcurl 是由 Daniel Stenberg 于 1997 年开始开发的自由软件,采用 MIT/X 衍生许可证,允许在商业和开源项目中自由使用。它本质上是一个提供网络协议支持的函数库,开发者可以通过调用其 API,在应用程序中快速添加文件上传、下载、协议交互等功能,而无需从头实现复杂的网络协议细节。
libcurl 也是著名的命令行工具 curl 背后的核心引擎------curl 命令实际上是基于 libcurl 构建的一个可执行程序,展示了该库的主要功能。
主要特点
-
跨平台:支持 Windows、Linux、macOS、Android、iOS 以及各种 Unix 变体,几乎能在所有主流操作系统上运行。
-
多协议支持:内置对超过 20 种网络协议的支持,包括 HTTP/HTTPS、FTP/FTPS、SFTP、SCP、SMTP、POP3、IMAP、Telnet、TFTP、Gopher、MQTT 等,覆盖了大多数常见的数据传输需求。
-
易用且灵活:提供简单直观的 C API(同时有众多语言的绑定),开发者可以方便地设置请求选项(如 URL、超时、认证信息、代理等),并接收回调来获取数据。
-
高性能:支持异步传输(通过多接口)、连接复用、管道化、断点续传等机制,能高效处理大量并发请求。
-
安全性:通过集成 OpenSSL、GnuTLS、mbedTLS 等库,支持 SSL/TLS 加密(HTTPS、FTPS 等),确保数据传输安全。同时支持各种认证方式(Basic、Digest、NTLM、Kerberos 等)。
核心功能
-
数据发送与接收:向服务器发送请求(GET、POST、PUT 等),并接收响应数据,支持上传和下载任意大小的文件。
-
代理支持:允许通过 HTTP、SOCKS4、SOCKS5 代理进行通信,方便在受限网络环境中使用。
-
Cookie 管理:能够存储、发送和接收 HTTP Cookie,模拟浏览器的会话状态。
-
重定向处理:自动或可控地跟随 HTTP 重定向。
-
超时控制:设置连接超时、传输超时等,避免长时间阻塞。
-
进度回调:通过回调函数获取传输进度,可用于显示进度条或取消操作。
-
多种协议混合:可以在同一个会话中操作不同协议(例如先通过 FTP 下载文件,再通过 HTTP 上传数据)。
架构组成
libcurl 包含两个主要的接口层:
-
简单接口(easy interface):以同步方式处理单个传输任务,是最常用的部分,只需初始化一个"easy handle",设置选项,然后执行即可。
-
多接口(multi interface):提供异步传输能力,允许在单个线程中同时管理多个连接,适用于需要高并发或非阻塞操作的场景。
此外,libcurl 还提供了 C++ 风格的接口和数十种编程语言(如 Python、PHP、Java、Go、Ruby 等)的绑定,使得开发者可以在熟悉的环境中直接使用 libcurl 的功能。
常见应用场景
-
网络数据抓取与爬虫:用于从网站或 API 获取数据,支持处理复杂的 HTTP 请求头和 Cookie。
-
API 客户端:许多软件内部通过 libcurl 与 RESTful 或 SOAP API 交互,实现自动化数据交换。
-
文件下载/上传工具:如下载管理器、FTP 客户端、云存储同步工具等,利用其断点续传和多协议特性。
-
邮件客户端:通过 SMTP、POP3、IMAP 协议发送和接收邮件。
-
嵌入式系统:由于其轻量级和跨平台特性,常被用于路由器、智能家居设备等嵌入式环境的网络通信。
-
测试与调试工具:开发人员常用 libcurl 模拟各种网络请求,测试服务端的行为。
优势与生态
-
成熟稳定:历经二十多年发展,代码质量高,漏洞修复及时,被无数项目验证过。
-
活跃的社区:官方持续更新,添加新协议和功能,同时有大量文档、教程和示例。
-
高度可配置:编译时可选择启用/禁用某些协议、SSL 库、特性,以适应不同场景(例如最小化体积或最大化功能)。
二.使用libcurl进行邮件验证码发送
2.1.启动邮箱授权码
我们采用的是通过自己的邮箱向他人邮箱发送验证码的方式。
具体实现上,我们使用 libcurl 库构建一个客户端,通过该客户端向自己的邮箱(例如 QQ 邮箱或 163 邮箱)发起发送邮件的请求。自己的邮箱在接收到该请求后,会将邮件转发至目标收件人
需要注意的是,客户端在连接邮箱服务器时,并不能直接使用邮箱的登录密码进行身份验证,而是必须使用邮箱授权码。
授权码是邮箱服务商为第三方客户端提供的专用密码,用于保障账户安全,避免直接暴露用户的真实密码。该授权码需提前在邮箱账户的安全设置中生成,并配置到客户端中,作为连接邮箱服务器时的认证凭据。
我们这里提供了QQ邮箱和163邮箱的邮箱授权码的获取方式,在我们的客户端编写的时候,大家只需要选择一种邮箱即可。
2.1.1.QQ邮箱
我们打开QQ邮箱,然后点击右上角的设置,

进去之后我们点击发信设置

然后往下翻,找到账号管理,并进入


开启之后我们会得到下面这个授权码,大家一定要记住了啊!!!

这个授权码就是我们需要的!!!必须好好保存啊,它只显示一次。
如果你忘了,也可以点击下面这个再次生成另外一个

生成另外一个之后如果之前的不用了,可以去下面这个地方对之前那个邮箱授权码进行停用

2.1.2.网易邮箱(163邮箱)
网易邮箱的就更容易了

点进去

这里有2种,那么我们2个里面选一个开启即可。

点击继续开启,我们就得到了我们的邮箱授权码,同样是只显示一次

后续我们也可以在下面进行授权码的管理

2.2.测试程序
接下来我们就使用libcurl库来进行我们的邮箱验证码发送
注意我们这里是使用163邮箱来作为发送方,所以这个邮箱验证码必须是163邮箱的。
test.cpp
cpp
// 包含libcurl头文件
#include <curl/curl.h>
#include <iostream>
#include <sstream>
// 回调函数:当libcurl需要发送邮件数据时调用
// 参数buffer:libcurl提供的缓冲区,用于写入数据
// size * nitems:缓冲区大小(字节数)
// userdata:用户自定义数据,这里指向一个stringstream对象,存储邮件内容
size_t callback(char *buffer, size_t size, size_t nitems, void *userdata)
{
// 将userdata转换为stringstream指针
std::stringstream *ss = (std::stringstream*)userdata,;
// 从stringstream中读取最多 size*nitems 字节到buffer
ss->read(buffer, size * nitems);
// 返回实际读取的字节数,告诉libcurl本次提供了多少数据
return ss->gcount();
}
int main()
{
// 邮箱账号配置(请使用实际有效的账号和授权码)
const std::string username = "JzSan521@163.com";// 你的邮箱名
const std::string password = "UBTtCJ76iezXC9ws"; // 你获取的邮箱授权码
const std::string url = "smtps://smtp.163.com:465"; // 使用SMTPS协议,端口465
const std::string from = "JzSan521@163.com"; // 发件人邮箱,也就是我们自己的邮箱
const std::string to = "270988047@qq.com"; // 收件人邮箱
// 1. 初始化libcurl全局环境
auto ret = curl_global_init(CURL_GLOBAL_DEFAULT);
if (ret != CURLE_OK) {
std::cout << "curl_global_init failed: " << curl_easy_strerror(ret) << std::endl;
return -1;
}
// 2. 创建一个easy句柄,用于后续操作
auto curl = curl_easy_init();
if (curl == nullptr) {
std::cout << "curl_easy_init failed" << std::endl;
return -1;
}
// 3. 设置SMTP相关参数
// 设置SMTP服务器URL
ret = curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
if (ret != CURLE_OK) {
std::cout << "curl_easy_setopt CURLOPT_URL failed: " << curl_easy_strerror(ret) << std::endl;
return -1;
}
// 设置登录用户名
ret = curl_easy_setopt(curl, CURLOPT_USERNAME, username.c_str());
if (ret != CURLE_OK) {
std::cout << "curl_easy_setopt CURLOPT_USERNAME failed: " << curl_easy_strerror(ret) << std::endl;
return -1;
}
// 设置登录密码(或授权码)
ret = curl_easy_setopt(curl, CURLOPT_PASSWORD, password.c_str());
if (ret != CURLE_OK) {
std::cout << "curl_easy_setopt CURLOPT_PASSWORD failed: " << curl_easy_strerror(ret) << std::endl;
return -1;
}
// 设置发件人地址(MAIL FROM)
ret = curl_easy_setopt(curl, CURLOPT_MAIL_FROM, from.c_str());
if (ret != CURLE_OK) {
std::cout << "curl_easy_setopt CURLOPT_MAIL_FROM failed: " << curl_easy_strerror(ret) << std::endl;
return -1;
}
// 构建收件人列表(RCPT TO)
struct curl_slist *cs = nullptr;
cs = curl_slist_append(cs, to.c_str()); // 可多次append添加多个收件人
ret = curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, cs);
if (ret != CURLE_OK) {
std::cout << "curl_easy_setopt CURLOPT_MAIL_RCPT failed: " << curl_easy_strerror(ret) << std::endl;
return -1;
}
// 准备邮件内容(包括头部和正文)
std::stringstream ss;
// 通常邮件需要包含From和To头部,但libcurl的MAIL_FROM和MAIL_RCPT已指定信封地址,
// 许多邮件服务器仍要求邮件内容中包含这些头部,此处注释掉了,可根据需要取消注释。
// ss << "From: " << from << "\r\n";
// ss << "To: " << to << "\r\n";
ss << "Subject: 验证码\r\n"; // 邮件主题
ss << "Content-Type: text/html\r\n"; // 内容类型为HTML
ss << "\r\n"; // 头部与正文之间的空行
ss << "<html><body><p>你的验证码: <b>5678</b></p><p>验证码将在5分钟后失效.</p></body></html>\r\n"; // HTML正文
// 设置读取数据的源(即上面的stringstream)
ret = curl_easy_setopt(curl, CURLOPT_READDATA, (void*)&ss);
if (ret != CURLE_OK) {
std::cout << "curl_easy_setopt CURLOPT_READDATA failed: " << curl_easy_strerror(ret) << std::endl;
return -1;
}
// 设置读取数据的回调函数
ret = curl_easy_setopt(curl, CURLOPT_READFUNCTION, callback);
if (ret != CURLE_OK) {
std::cout << "curl_easy_setopt CURLOPT_READFUNCTION failed: " << curl_easy_strerror(ret) << std::endl;
return -1;
}
// 设置为上传模式(发送邮件本质是向服务器上传数据)
ret = curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
if (ret != CURLE_OK) {
std::cout << "curl_easy_setopt CURLOPT_UPLOAD failed: " << curl_easy_strerror(ret) << std::endl;
return -1;
}
// 4. 执行邮件发送请求
ret = curl_easy_perform(curl);
if (ret != CURLE_OK) {
std::cout << "curl_easy_perform failed: " << curl_easy_strerror(ret) << std::endl;
return -1;
}
// 5. 清理资源
curl_slist_free_all(cs); // 释放收件人列表
curl_easy_cleanup(curl); // 清理easy句柄
curl_global_cleanup(); // 清理全局环境
return 0;
}
makefile
cpp
test:test.cpp
g++ -std=c++17 $^ -o $@ -lpthread -lcurl -lssl -lcrypto
.PHONY:clean
clean:
rm -f test
我们运行之后,去QQ邮箱看看

一点问题都没有啊。果然收到了我们发送的邮件。
三.二次封装libcurl验证码通知服务
那么后续我们会经常使用到这个邮箱验证码通知服务,所以我们需要对它进行二次封装
email.hpp
cpp
#pragma once
// 描述: 基于libcurl的邮件发送客户端,用于发送验证码邮件
// 依赖: libcurl库, log.h (需提供LOG_ERROR, LOG_DEBUG宏定义)
#include <curl/curl.h> // libcurl库头文件,提供CURL相关函数和类型
#include <iostream> // 标准输入输出流(此处未直接使用,但可能用于调试)
#include <sstream> // 字符串流,用于构建邮件内容
#include <memory> // 智能指针支持
#include "logger.hpp" // 日志宏定义(需提供LOG_ERROR、LOG_DEBUG等)
namespace IMS
{
// 邮件服务配置信息结构体
struct mail_settings
{
std::string username; // 邮箱用户名(即邮箱账号)
std::string password; // 邮箱授权码(不是登录密码,需在邮箱设置中获取)
std::string url; // SMTP服务器URL,例如163邮箱为 smtps://smtp.163.com:465,QQ邮箱为 smtps://smtp.qq.com:465
std::string from; // 发件人邮箱地址(通常与username相同)
};
// 验证码发送接口抽象基类
// 定义统一的发送接口,方便扩展其他发送方式(如短信等)
class CodeClient
{
public:
CodeClient() = default; // 默认构造函数
virtual ~CodeClient() = default; // 虚析构函数,确保派生类正确释放资源
// 纯虚函数:发送验证码到指定目标
// @param to 收件地址(如邮箱地址、手机号)
// @param code 验证码内容
// @return 发送成功返回true,失败返回false
virtual bool send(const std::string& to, const std::string& code) = 0;
};
// 邮件客户端类,继承自CodeClient,使用libcurl通过SMTP协议发送验证码邮件
class MailClient : public CodeClient
{
public:
using ptr = std::shared_ptr<MailClient>; // 智能指针类型别名,便于管理对象生命周期
// 构造函数:初始化全局配置,保存邮件服务器配置信息
// @param settings 邮件服务器配置结构体
MailClient(const mail_settings& settings) : _settings(settings) {
// 初始化libcurl全局环境,该函数必须在任何libcurl函数之前调用
// CURL_GLOBAL_DEFAULT 表示使用默认全局初始化选项
auto ret = curl_global_init(CURL_GLOBAL_DEFAULT);
if (ret != CURLE_OK) {
// 若初始化失败,记录错误日志并终止程序(因为后续所有操作都无法进行)
LOG_ERROR("初始化CURL全局配置失败: {}", curl_easy_strerror(ret));
abort(); // 全局初始化失败则终止程序
}
}
// 析构函数:释放libcurl全局配置资源
~MailClient() {
curl_global_cleanup(); // 清理全局环境,必须在程序结束时调用
}
// 发送邮件(实现CodeClient接口)
// @param to 收件人邮箱地址
// @param code 验证码
// @return 成功返回true,失败返回false
virtual bool send(const std::string& to, const std::string& code) override
{
// 创建一个easy句柄,代表一个libcurl会话
auto curl = curl_easy_init();
if (curl == nullptr) {
LOG_ERROR("构造CURL操作句柄失败!");
return false;
}
// 设置超时选项,防止网络问题导致程序长时间阻塞
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L); // 连接超时时间(秒),超过则放弃连接
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); // 整个请求的总超时时间(秒)
// 注意:以下SSL验证选项仅适用于测试开发阶段,实际生产环境应启用验证以确保安全性
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); // 不验证对等端SSL证书(跳过证书验证)
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); // 不验证主机名(跳过主机名验证)
// 设置SMTP服务器URL(包含协议、主机和端口)
auto ret = curl_easy_setopt(curl, CURLOPT_URL, _settings.url.c_str());
if (ret != CURLE_OK) {
LOG_ERROR("设置CURL的CURLOPT_URL参数失败: {}", curl_easy_strerror(ret));
curl_easy_cleanup(curl); // 清理句柄
return false;
}
// 设置SMTP认证用户名(邮箱账号)
ret = curl_easy_setopt(curl, CURLOPT_USERNAME, _settings.username.c_str());
if (ret != CURLE_OK) {
LOG_ERROR("设置CURL的CURLOPT_USERNAME参数失败: {}", curl_easy_strerror(ret));
curl_easy_cleanup(curl);
return false;
}
// 设置SMTP认证密码(邮箱授权码)
ret = curl_easy_setopt(curl, CURLOPT_PASSWORD, _settings.password.c_str());
if (ret != CURLE_OK) {
LOG_ERROR("设置CURL的CURLOPT_PASSWORD参数失败: {}", curl_easy_strerror(ret));
curl_easy_cleanup(curl);
return false;
}
// 设置发件人邮箱地址(对应SMTP的MAIL FROM命令)
ret = curl_easy_setopt(curl, CURLOPT_MAIL_FROM, _settings.from.c_str());
if (ret != CURLE_OK) {
LOG_ERROR("设置CURL的CURLOPT_MAIL_FROM参数失败: {}", curl_easy_strerror(ret));
curl_easy_cleanup(curl);
return false;
}
// 构建收件人列表(对应SMTP的RCPT TO命令)
// 使用curl_slist链表结构,可添加多个收件人
struct curl_slist *cs = nullptr;
cs = curl_slist_append(cs, to.c_str()); // 将收件人地址添加到链表
ret = curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, cs);
if (ret != CURLE_OK) {
LOG_ERROR("设置CURL的CURLOPT_MAIL_RCPT参数失败: {}", curl_easy_strerror(ret));
curl_slist_free_all(cs); // 释放收件人列表
curl_easy_cleanup(curl);
return false;
}
// 构造邮件正文(包含头部和HTML内容)
auto body = codeBody(to, code); // body 是一个 std::stringstream 对象
// 设置数据读取源(即邮件内容的流对象),libcurl将通过回调函数从中读取数据
ret = curl_easy_setopt(curl, CURLOPT_READDATA, (void*)&body);
if (ret != CURLE_OK) {
LOG_ERROR("设置CURL的CURLOPT_READDATA参数失败: {}", curl_easy_strerror(ret));
curl_slist_free_all(cs);
curl_easy_cleanup(curl);
return false;
}
// 设置数据读取回调函数,libcurl在需要发送邮件数据时会调用该函数
ret = curl_easy_setopt(curl, CURLOPT_READFUNCTION, &MailClient::callback);
if (ret != CURLE_OK) {
LOG_ERROR("设置CURL的CURLOPT_READFUNCTION参数失败: {}", curl_easy_strerror(ret));
curl_slist_free_all(cs);
curl_easy_cleanup(curl);
return false;
}
// 指示这是一个上传操作(发送邮件本质是向服务器上传数据)
ret = curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
if (ret != CURLE_OK) {
LOG_ERROR("设置CURL的CURLOPT_UPLOAD参数失败: {}", curl_easy_strerror(ret));
curl_slist_free_all(cs);
curl_easy_cleanup(curl);
return false;
}
// 执行邮件发送请求(阻塞直到完成或出错)
ret = curl_easy_perform(curl);
if (ret != CURLE_OK) {
LOG_ERROR("请求邮件服务器失败: {}", curl_easy_strerror(ret));
curl_slist_free_all(cs);
curl_easy_cleanup(curl);
return false;
}
// 清理资源
curl_slist_free_all(cs); // 释放收件人列表内存
curl_easy_cleanup(curl); // 释放easy句柄
LOG_DEBUG("发送邮件成功: {}-{}", to, code); // 记录调试信息
return true;
}
private:
// 构造邮件正文(HTML格式)
// @param to 收件人地址(可用于个性化,此处未使用但保留)
// @param code 验证码
// @return 包含完整邮件内容的字符串流对象
std::stringstream codeBody(const std::string& to, const std::string& code)
{
std::stringstream ss;
// 邮件头部
ss << "Subject: " << _title << "\r\n"; // 邮件主题
ss << "Content-Type: text/html\r\n"; // 内容类型为HTML
ss << "\r\n"; // 空行分隔头部和正文(必须)
// 邮件正文(HTML格式)
ss << "<html><body><p>你的验证码: <b>" << code << "</b></p>"
<< "<p>验证码将在5分钟后失效.</p></body></html>\r\n";
return ss;
}
// libcurl读取数据回调函数(静态成员函数)
// 该函数由libcurl调用,用于从自定义数据源(stringstream)中读取邮件数据并写入缓冲区
// @param buffer libcurl提供的输出缓冲区
// @param size 每个数据单元的大小(字节数)
// @param nitems 数据单元的个数,缓冲区总大小为 size * nitems
// @param userdata 用户自定义数据指针,此处指向 std::stringstream 对象
// @return 实际写入缓冲区的字节数(返回0表示数据结束)
static size_t callback(char *buffer, size_t size, size_t nitems, void *userdata)
{
std::stringstream *ss = (std::stringstream*)userdata;
ss->read(buffer, size * nitems); // 从流中读取数据
return ss->gcount(); // 返回实际读取的字节数
}
private:
const std::string _title = "验证码"; // 邮件标题固定前缀
mail_settings _settings; // 邮件服务器配置信息
};
} // namespace IMS
我们来写一个测试程序看看
cpp
#include <memory>
#include "../../../common/email.hpp"
int main()
{
// 初始化日志(调试模式,输出到控制台)
IMS::init_logger(false, "", 0); // 调试模式,日志级别 trace
// 1. 构造邮件服务配置
IMS::mail_settings settings = {
.username = "JzSan521@163.com",
.password = "UBTtCJ76iezXC9ws",
.url = "smtps://smtp.163.com:465",
.from = "JzSan521@163.com",
};
// 2. 实例化邮件客户端对象
auto mail = std::make_unique<IMS::MailClient>(settings);
// 3. 发送邮件
bool ret = mail->send("270988047@qq.com", "6789");
if (!ret) {
std::cout << "发送邮件失败!" << std::endl;
}
return 0;
}

可以看到,非常的完美,当然,我们可以将验证码换成1234看看

一点问题都没有!!