electron在windows系统上如何进行用户授权

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;
}

到此,就已经完成了全部工作了

相关推荐
沙子迷了蜗牛眼3 分钟前
当展示列表使用 URL.createObjectURL 的创建临时图片、视频无法加载问题
java·前端·javascript·vue.js
Hi_kenyon17 分钟前
VUE3套用组件库快速开发(以Element Plus为例)三
前端·javascript·vue.js
J总裁的小芒果18 分钟前
原生Table写一行保证两条数据
javascript·vue.js·elementui
jqq66627 分钟前
解析ElementPlus打包源码(五、copyFiles)
前端·javascript·vue.js
怣疯knight39 分钟前
Docker Desktop 4.55.0版本安装成功教程
windows·docker
还不秃顶的计科生1 小时前
defaultdict讲解
开发语言·javascript·ecmascript
花归去1 小时前
echarts 柱状图包含右侧进度
开发语言·前端·javascript
沐浴露z2 小时前
学习通“只能录入不能粘贴” 解决方案与原理分析
javascript
Sapphire~2 小时前
Vue3-03 熟悉src文件夹及Vue文件格式
前端·javascript·vue.js
liulilittle2 小时前
VEthernet 框架实现 tun2socks 的技术原理
网络·windows·c#·信息与通信·通信