就如前篇所说,在经过用Shell脚本编写的过后,还是决定改成用其他语言编写一个PAM模块来实现我所需要的功能吧!
说是SMS短信来实现两步验证的功能,但是受限于短信接口不仅需要钱且申请有些繁杂,这不就使用Gotify、Ntfy这些实时消息推送服务来实现需求。
其实这个类似的功能就有一款由Google开发的Google Authenticator就能实现,而且对于Google Authenticator这款App的使用也不是什么难题。
源码
pam_extra_auth.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <unistd.h>
#include <syslog.h>
#include <curl/curl.h>
#include <json-c/json.h>
#include <fcntl.h>
#include <errno.h>
// 配置参数默认值
#define DEFAULT_PROMPT "请输入接收到的验证码: "
#define DEFAULT_CONFIG_FILE "/etc/pam_extra_auth.conf"
#define DEFAULT_GOTIFY_URL "https://gotify.cyzwb.com/"
#define DEFAULT_GOTIFY_KEY "your_api_key_here"
#define DEFAULT_NTFY_URL "https://ntfy.cyzwb.com/"
#define DEFAULT_NTFY_KEY "your_api_key_here"
#define DEFAULT_NTFY_TOPIC "your_api_key_here"
#define CODE_EXPIRE_SECONDS 300 // 验证码有效期5分钟
typedef struct {
char *prompt; // 提示信息
char *gotify_url; // Gotify API地址
char *gotify_key; // Gotify API密钥
char *ntfy_url; // Ntfy API地址
char *ntfy_key; // Ntfy API密钥
char *ntfy_topic; // Ntfy TOPIC
} pam_extra_config_t;
// 用于存储HTTP响应
struct memory_struct {
char *memory;
size_t size;
};
/**
* 加载配置文件
*/
static int load_config(pam_extra_config_t *config) {
FILE *fp = fopen(DEFAULT_CONFIG_FILE, "r");
if (!fp) {
// 使用默认配置
config->prompt = strdup(DEFAULT_PROMPT);
config->gotify_url = strdup(DEFAULT_GOTIFY_URL);
config->gotify_key = strdup(DEFAULT_GOTIFY_KEY);
config->ntfy_url = strdup(DEFAULT_NTFY_URL);
config->ntfy_key = strdup(DEFAULT_NTFY_KEY);
config->ntfy_topic = strdup(DEFAULT_NTFY_TOPIC);
return PAM_SUCCESS;
}
char line[256];
while (fgets(line, sizeof(line), fp)) {
// 跳过注释和空行
if (line[0] == '#' || line[0] == '\n') continue;
char key[64], value[192];
if (sscanf(line, "%63s = %191[^\n]", key, value) == 2) {
if (strcmp(key, "prompt") == 0) {
free(config->prompt);
config->prompt = strdup(value);
} else if (strcmp(key, "gotify_url") == 0) {
free(config->gotify_url);
config->gotify_url = strdup(value);
} else if (strcmp(key, "gotify_key") == 0) {
free(config->gotify_key);
config->gotify_key = strdup(value);
} else if (strcmp(key, "ntfy_url") == 0) {
free(config->ntfy_url);
config->ntfy_url = strdup(value);
} else if (strcmp(key, "ntfy_key") == 0) {
free(config->ntfy_key);
config->ntfy_key = strdup(value);
} else if (strcmp(key, "ntfy_topic") == 0) {
free(config->ntfy_topic);
config->ntfy_topic = strdup(value);
}
}
}
fclose(fp);
return PAM_SUCCESS;
}
/**
* HTTP响应回调函数
*/
static size_t write_memory_callback(void *contents, size_t size, size_t nmemb, void *userp) {
size_t realsize = size * nmemb;
struct memory_struct *mem = (struct memory_struct *)userp;
char *ptr = realloc(mem->memory, mem->size + realsize + 1);
if(!ptr) {
/* 内存分配失败 */
syslog(LOG_ERR, "内存分配失败");
return 0;
}
mem->memory = ptr;
memcpy(&(mem->memory[mem->size]), contents, realsize);
mem->size += realsize;
mem->memory[mem->size] = 0;
return realsize;
}
/**
* 发起JSON格式的POST请求
* @param url 请求URL
* @param json_obj JSON对象
* @param api_key 请求密钥
* @return HTTP响应内容,需要调用者free释放
*/
static char* http_post_json(const char *url, json_object *json_obj, const char *api_key) {
CURL *curl_handle;
CURLcode res;
struct memory_struct chunk;
struct curl_slist *headers = NULL;
const char *json_str = json_object_to_json_string(json_obj);
// 初始化响应存储
chunk.memory = malloc(1);
chunk.size = 0;
// 初始化libcurl
curl_global_init(CURL_GLOBAL_ALL);
curl_handle = curl_easy_init();
if(curl_handle) {
// 设置请求URL
curl_easy_setopt(curl_handle, CURLOPT_URL, url);
// 设置自定义请求头 - 包含API密钥和JSON内容类型
char auth_header[128];
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", api_key);
headers = curl_slist_append(headers, auth_header);
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl_handle, CURLOPT_HTTPHEADER, headers);
// 设置POST数据(JSON字符串)
curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDS, json_str);
curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDSIZE, strlen(json_str));
// 设置响应回调函数
curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_memory_callback);
curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&chunk);
// 开启HTTPS支持
curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 2L);
// 设置超时时间
curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, 10L);
// 执行请求
res = curl_easy_perform(curl_handle);
// 检查请求是否成功
if(res != CURLE_OK) {
syslog(LOG_ERR, "curl请求失败: %s", curl_easy_strerror(res));
free(chunk.memory);
chunk.memory = NULL;
}
// 清理资源
curl_easy_cleanup(curl_handle);
curl_slist_free_all(headers);
} else {
syslog(LOG_ERR, "初始化curl失败");
free(chunk.memory);
chunk.memory = NULL;
}
curl_global_cleanup();
return chunk.memory;
}
/**
* 生成安全的6位验证码(使用/dev/urandom)
*/
static char* generate_code() {
char *code = malloc(7);
if (!code) {
syslog(LOG_ERR, "内存分配失败");
return NULL;
}
int urandom_fd = open("/dev/urandom", O_RDONLY);
if (urandom_fd == -1) {
syslog(LOG_ERR, "无法打开/dev/urandom: %s", strerror(errno));
free(code);
return NULL;
}
unsigned char bytes[6];
ssize_t bytes_read = read(urandom_fd, bytes, sizeof(bytes));
if (bytes_read != sizeof(bytes)) {
syslog(LOG_ERR, "读取/dev/urandom失败: %s", strerror(errno));
close(urandom_fd);
free(code);
return NULL;
}
for(int i = 0; i < 6; i++) {
code[i] = '0' + (bytes[i] % 10);
}
code[6] = '\0';
close(urandom_fd);
return code;
}
/**
* 生成并发送验证码
*/
static char* generate_and_send_code(const char *username, const pam_extra_config_t *config) {
// 生成验证码
char *code = generate_code();
if (!code) {
syslog(LOG_ERR, "生成验证码失败");
return NULL;
}
char message[128];
snprintf(message, sizeof(message), "用户 %s 的SSH登录验证码:%s(5分钟内有效)", username, code);
syslog(LOG_INFO, "生成验证码: %s for user %s", code, username);
// 发送验证码到Gotify
{
const char *url = config->gotify_url;
const char *api_key = config->gotify_key;
json_object *json_obj = json_object_new_object();
json_object_object_add(json_obj, "title", json_object_new_string("SSH登录验证码"));
json_object_object_add(json_obj, "priority", json_object_new_int(5));
json_object_object_add(json_obj, "message", json_object_new_string(message));
char *response = http_post_json(url, json_obj, api_key);
if(response) {
syslog(LOG_DEBUG, "Gotify响应: %s", response);
free(response);
} else {
syslog(LOG_ERR, "Gotify请求失败");
}
json_object_put(json_obj);
}
// 发送验证码到Ntfy
{
const char *url = config->ntfy_url;
const char *api_key = config->ntfy_key;
const char *topic = config->ntfy_topic;
json_object *json_obj = json_object_new_object();
json_object_object_add(json_obj, "title", json_object_new_string("SSH登录验证码"));
json_object_object_add(json_obj, "topic", json_object_new_string(topic));
json_object_object_add(json_obj, "message", json_object_new_string(message));
char *response = http_post_json(url, json_obj, api_key);
if(response) {
syslog(LOG_DEBUG, "Ntfy响应: %s", response);
free(response);
} else {
syslog(LOG_ERR, "Ntfy请求失败");
}
json_object_put(json_obj);
}
return code;
}
/**
* 验证逻辑 - 验证码比对+过期检查
*/
static int verify_code(const char *input, const char *expected_code, time_t generate_time) {
if (!input || !expected_code) {
return PAM_AUTH_ERR;
}
// 检查验证码是否过期
if (time(NULL) - generate_time > CODE_EXPIRE_SECONDS) {
syslog(LOG_WARNING, "验证码已过期");
return PAM_AUTH_ERR;
}
return strcmp(input, expected_code) == 0 ? PAM_SUCCESS : PAM_AUTH_ERR;
}
/**
* PAM认证函数
*/
PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
int retval;
const char *username;
char *input = NULL;
char *expected_code = NULL;
time_t code_generate_time;
pam_extra_config_t config = {0};
struct pam_conv *conv;
const char *session_reuse_env = NULL;
openlog("pam_extra_auth", LOG_PID | LOG_CONS, LOG_AUTH);
// 静默未使用参数,避免编译错误
(void)flags; (void)argc; (void)argv;
// 检查是否是Session复用请求
session_reuse_env = pam_getenv(pamh, "SSH_SESSION_REUSE");
if (session_reuse_env != NULL && strcmp(session_reuse_env, "1") == 0) {
syslog(LOG_INFO, "Session复用请求,跳过验证码验证");
closelog();
return PAM_SUCCESS;
}
// 获取登录用户名
retval = pam_get_user(pamh, &username, "用户名: ");
if (retval != PAM_SUCCESS) {
syslog(LOG_ERR, "无法获取用户名: %s", pam_strerror(pamh, retval));
closelog();
return retval;
}
// 加载配置
load_config(&config);
// 获取对话函数
retval = pam_get_item(pamh, PAM_CONV, (const void **)&conv);
if (retval != PAM_SUCCESS || !conv) {
syslog(LOG_ERR, "无法获取对话函数: %s", pam_strerror(pamh, retval));
goto cleanup;
}
// =========== 核心修复:先生成并发送验证码 ===========
expected_code = generate_and_send_code(username, &config);
if (!expected_code) {
syslog(LOG_ERR, "生成并发送验证码失败");
retval = PAM_SYSTEM_ERR;
goto cleanup;
}
code_generate_time = time(NULL);
// =========== 再弹出输入框要求用户输入 ===========
struct pam_message msg = {
.msg_style = PAM_PROMPT_ECHO_OFF,
.msg = config.prompt
};
const struct pam_message *msgp = &msg;
struct pam_response *resp = NULL;
retval = conv->conv(1, &msgp, &resp, conv->appdata_ptr);
if (retval != PAM_SUCCESS || !resp) {
syslog(LOG_ERR, "获取用户输入失败: %s", pam_strerror(pamh, retval));
goto cleanup;
}
input = resp->resp;
// 执行认证逻辑
retval = verify_code(input, expected_code, code_generate_time);
if (retval == PAM_SUCCESS) {
syslog(LOG_INFO, "用户 %s 验证码验证通过", username);
} else {
syslog(LOG_WARNING, "用户 %s 验证码验证失败", username);
}
cleanup:
// 清理资源
free(config.prompt);
free(config.gotify_url);
free(config.gotify_key);
free(config.ntfy_url);
free(config.ntfy_key);
free(config.ntfy_topic);
free(expected_code);
if (resp) {
if (resp->resp) free(resp->resp);
free(resp);
}
closelog();
return retval;
}
/**
* 修复pam_sm_setcred实现
*/
PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) {
openlog("pam_extra_auth", LOG_PID | LOG_CONS, LOG_AUTH);
// 静默未使用参数,避免编译警告
(void)pamh; (void)argc; (void)argv;
// 检查标志是否有效
if (flags & ~(PAM_SILENT | PAM_ESTABLISH_CRED | PAM_DELETE_CRED |
PAM_REINITIALIZE_CRED | PAM_REFRESH_CRED)) {
syslog(LOG_ERR, "无效的标志位: %d", flags);
closelog();
return PAM_SYSTEM_ERR; // 替代PAM_BAD_CONSTANT,兼容旧版本PAM库
}
// 对于我们的验证码模块,不需要设置任何凭据
// 只需要根据标志返回相应的成功状态
switch (flags & ~PAM_SILENT) {
case PAM_ESTABLISH_CRED:
case PAM_REINITIALIZE_CRED:
case PAM_REFRESH_CRED:
syslog(LOG_INFO, "凭据设置成功");
closelog();
return PAM_SUCCESS;
case PAM_DELETE_CRED:
syslog(LOG_INFO, "凭据删除成功");
closelog();
return PAM_SUCCESS;
default:
syslog(LOG_ERR, "不支持的标志位: %d", flags);
closelog();
return PAM_SYSTEM_ERR;
}
}
// 其他PAM接口函数保持不变
PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) {
(void)pamh; (void)flags; (void)argc; (void)argv;
return PAM_SUCCESS;
}
PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) {
(void)pamh; (void)flags; (void)argc; (void)argv;
return PAM_SUCCESS;
}
PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) {
(void)pamh; (void)flags; (void)argc; (void)argv;
return PAM_SUCCESS;
}
PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) {
(void)pamh; (void)flags; (void)argc; (void)argv;
return PAM_SUCCESS;
}
// 模块符号定义
#ifdef PAM_STATIC
struct pam_module _pam_extra_auth_modstruct = {
"pam_extra_auth",
pam_sm_authenticate,
pam_sm_acct_mgmt,
pam_sm_setcred,
pam_sm_open_session,
pam_sm_close_session,
pam_sm_chauthtok
};
#endif
pam_extra_auth.conf
# 提示信息 prompt = 请输入接收到的验证码: # Gotify API地址 gotify_url = https://gotify.cyzwb.com/message # Gotify API密钥 gotify_key = AxxxxxxxR # Ntfy API地址 ntfy_url = https://ntfy.cyzwb.com # Ntfy API密钥 ntfy_key = tk_8xxxxi # Ntfy TOPIC ntfy_topic = 2xxxf
Makefile
CC = gcc CFLAGS = -fPIC -Wall -Wextra -Werror=implicit-function-declaration LIBS = -lpam -lcurl -ljson-c all: pam_extra_auth.so pam_extra_auth.so: pam_extra_auth.c $(CC) $(CFLAGS) -shared -o $@ $^ $(LIBS) install: pam_extra_auth.so install -m 755 pam_extra_auth.so /lib/security/ install -m 600 pam_extra_auth.conf /etc/ clean: rm -f pam_extra_auth.so
编译并安装
make make install
启用
- 编辑
/etc/pam.d/sshd文件,添加如下语句:# Standard Un*x authentication. @include common-auth ### 自定义SSH登录验证 2026-05-12 1352 auth required pam_extra_auth.so # Disallow non-root logins when /etc/nologin exists. account required pam_nologin.so
注意顺序,我预期值是先输入用户密码才发送接收验证码来输入。
- 编辑
/etc/ssh/sshd_config文件,将如下参数设置为下列所示:### 2026-05-12 PasswordAuthentication yes KbdInteractiveAuthentication yes # 启用键盘交互认证 ChallengeResponseAuthentication yes # 启用挑战-响应认证
貌似需要配置键盘交互认证,这样xshell之类的软件才能使用。
总结
在一天的使用中未发现有什么较大的问题,但根据其原理还是知晓有几处问题的。
- 网络出问题时就无法登录进行维护了,至少对于Gotify、ntfy等服务端来说宕机了还能靠迁移恢复来接收验证码。但是若是机器的出网网络出问题的时候,需要登录进去维护网络是就陷入死循环了。
ChiuYut
2026年05月14日