electron在windows系统上的用户授权
我们经常在开发electron项目时,当遇到一些敏感操作需要进行用户授权时(弹出windows安全中心弹窗,要求输入当前用户登录命令,从而确认是本人操作),往往不太好处理。
这是因为electron在macos上提供了直接的api可供调用systemPreferences.promptTouchID。
但是在windows上,electron却没有提供相关的api,那么我们如何才能唤起windows安全中心的授权弹窗呢。
解决方案
windows系统提供了CredUIPromptForCredentialsW api可直接唤起系统自带的凭证输入弹窗。并进行密码身份认证。但是electron并无法直接调用这个系统级的api。此时该怎么办呢。
我们可以通过c++去调用windows系统的api,写一段c++脚本去调用这个api,唤起凭证输入框进行身份认证。然后再通过node-gyp去编译这个原生模块,编译后会生成一个.node的模块,该模块可通过node.js直接调用。
此时,我们可以着手准备写一个本地包文件来处理这个事情
cpp
// Include security_defs.h first to define SECURITY_WIN32
#include "security_defs.h"
#include <napi.h>
#ifdef _WIN32
#include <windows.h>
#include <wincred.h>
#include <lmcons.h>
#pragma comment(lib, "credui.lib")
#pragma comment(lib, "advapi32.lib")
#endif
Napi::Boolean PromptCredentials(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
#ifdef _WIN32
try {
// Convert message if provided
std::string message = "Enter your password to continue";
// Get theme parameter if provided ('dark' or 'light')
std::string theme = "system";
if (info.Length() > 0 && info[0].IsString()) {
message = info[0].As<Napi::String>().Utf8Value();
}
// Get theme parameter
if (info.Length() > 1 && info[1].IsString()) {
theme = info[1].As<Napi::String>().Utf8Value();
}
// Convert to wide string
int wideLen = MultiByteToWideChar(CP_UTF8, 0, message.c_str(), -1, NULL, 0);
if (wideLen <= 0) {
return Napi::Boolean::New(env, false);
}
wchar_t* wideMessage = new wchar_t[wideLen];
MultiByteToWideChar(CP_UTF8, 0, message.c_str(), -1, wideMessage, wideLen);
// Setup credential UI to match the dark Windows Security Center style
CREDUI_INFOW credInfo;
ZeroMemory(&credInfo, sizeof(credInfo));
credInfo.cbSize = sizeof(credInfo);
credInfo.hwndParent = NULL;
credInfo.pszMessageText = L"请输入您的Windows登录密码以继续";
credInfo.pszCaptionText = L"Windows 安全中心";
credInfo.hbmBanner = NULL;
// 准备Windows凭据缓冲区
PVOID outAuthBuffer = NULL;
ULONG outAuthBufferSize = 0;
BOOL save = FALSE;
// Get current username and prepare password buffer
wchar_t username[CREDUI_MAX_USERNAME_LENGTH + 1] = { 0 };
wchar_t password[CREDUI_MAX_PASSWORD_LENGTH + 1] = { 0 };
DWORD usernameSize = CREDUI_MAX_USERNAME_LENGTH + 1;
GetUserNameW(username, &usernameSize);
// Set flags for credential dialog
DWORD flags = CREDUI_FLAGS_GENERIC_CREDENTIALS | CREDUI_FLAGS_ALWAYS_SHOW_UI |
CREDUI_FLAGS_KEEP_USERNAME | CREDUI_FLAGS_DO_NOT_PERSIST;
// Add theme-specific flags
if (theme == "dark") {
// Dark theme settings - similar to Chrome's dark mode
flags |= CREDUI_FLAGS_EXPECT_CONFIRMATION; // Adds additional styling for modern look
} else if (theme == "light") {
// Light theme settings - standard Windows credential dialog
// No additional flags needed for light theme
} else {
// Default/system theme - add confirmation flag for better styling
flags |= CREDUI_FLAGS_EXPECT_CONFIRMATION;
}
// Show credential dialog with appropriate style
DWORD result = CredUIPromptForCredentialsW(
&credInfo,
L"Windows",
NULL,
0,
username,
CREDUI_MAX_USERNAME_LENGTH + 1,
password,
CREDUI_MAX_PASSWORD_LENGTH + 1,
&save,
flags);
// Clean up
delete[] wideMessage;
// Process result
if (result == ERROR_CANCELLED) {
return Napi::Boolean::New(env, false);
}
// Simple validation - if password is provided and dialog wasn't canceled
bool success = (result == NO_ERROR && wcslen(password) > 0);
// Verify credentials by attempting to login
if (success) {
HANDLE hToken = NULL;
success = LogonUserW(
username,
NULL, // Domain name, NULL for local accounts
password,
LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT,
&hToken
);
if (success && hToken != NULL) {
CloseHandle(hToken);
}
// Clear the password from memory
SecureZeroMemory(password, sizeof(password));
}
return Napi::Boolean::New(env, success);
} catch (...) {
return Napi::Boolean::New(env, false);
}
#else
// Non-Windows platform
return Napi::Boolean::New(env, false);
#endif
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "promptCredentials"),
Napi::Function::New(env, PromptCredentials));
return exports;
}
NODE_API_MODULE(win_auth, Init)
这里需要用到一个头文件security_defs.h
cpp
#ifndef SECURITY_DEFS_H
#define SECURITY_DEFS_H
// This header defines SECURITY_WIN32 macro required by security.h
#define SECURITY_WIN32
#endif // SECURITY_DEFS_H
我们还需要添加一个gyp配置文件
javascript
{
"targets": [
{
"target_name": "win_auth",
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"sources": [ "win_auth.cc" ],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS", "SECURITY_WIN32" ],
"conditions": [
["OS=='win'", {
"libraries": [ "-ladvapi32.lib", "-lcredui.lib" ],
"msvs_settings": {
"VCCLCompilerTool": {
"ExceptionHandling": 1,
"AdditionalOptions": [ "/D SECURITY_WIN32", "/utf-8" ]
}
}
}]
]
}
]
}
此时,我们需要将这个c++模块通过node-gyp编译成node模块。需要先准备环境
环境准备
全局安装node-gyp:
npm install -g node-gyp
node-gyp 需要两个核心依赖:Python 和 C++ 编译工具链。
安装python3.7+
python下载地址
安装 Visual Studio 的 C++ 构建工具。
Visual Studio下载地址
Visual Studio下载完成后,需要勾选win10/win11的sdk以及勾选 "C++ 生成工具" 或勾选 "使用 C++ 的桌面开发"进行安装(安装完成后最好重启一下电脑,让配置生效)
编译
此时,就可以进行编译了,允许node-gyp rebuild, 此时会在根目录下生成build文件夹,文件夹下的build/Release/xxx.node(xxx就是自己定义的包名)就是你需要的原生node模块
引用
接下来,我们需要暴露一个js方法,供业务侧去调用,在此js方法内,就会去调用我们编译好的build/Release/xxx.node模块,从而唤起希望凭证验证弹窗
index.js
javascript
const os = require('os');
let nativeModule = null;
if (os.platform() === 'win32') {
try {
nativeModule = require('./build/Release/win_auth.node');
} catch (e) {
console.error('无法加载Windows认证模块:', e);
}
}
/**
* 显示Windows认证对话框
* @param {string} message 提示信息
* @param {string} theme 主题色,可选值为 'light' | 'dark',默认根据系统自动判断
* @returns {Promise<boolean>} 认证是否成功
*/
function promptCredentials(message, theme) {
return new Promise((resolve) => {
if (!nativeModule) {
resolve(false);
return;
}
try {
// 传递主题参数到原生模块
const result = nativeModule.promptCredentials(message, theme);
resolve(result);
} catch (e) {
console.error('认证错误:', e);
resolve(false);
}
});
}
module.exports = {
promptCredentials
};
准备打包
npm init 准备package.json文件,修改入口文件,添加安装包时的允许命令
javascript
{
"name": "fellou-win-auth",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"install": "node-gyp rebuild"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"node-addon-api": "^8.5.0",
"node-gyp": "^11.4.2"
}
}
此时就可以通过本地包的方式引用这个包文件了,在项目的package.json的依赖中添加此包文件
"win-auth": "file:.../win-auth",
运行npm install,就会自动安装此包文件
使用
在业务代码中定义一个方法,方法中去调用该方法
javascript
try {
const winAuth = require("win-auth")
// 调用Windows认证,传入主题参数
const result = await winAuth.promptCredentials(tip || "请输入您的Windows登录密码以继续", theme || 'dark');
return result;
} catch (e) {
console.error("windows_auth_failed", e);
return false;
}
到此,就已经完成了全部工作了