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

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

相关推荐
江城开朗的豌豆2 小时前
React vs Vue:谁在性能赛道上更胜一筹?
前端·javascript·react.js
美酒没故事°2 小时前
旧vue3项目集成electron
前端·javascript·electron
GISer_Jing2 小时前
大前端——Taro、React-Native、Electron 大前端
前端·javascript·electron·taro
晓得迷路了2 小时前
栗子前端技术周刊第 100 期 - Chrome DevTools MCP、IEEE 编程语言榜单...
前端·javascript
丰年稻香2 小时前
Electron 安全实践:渲染进程如何安全使用主进程的三方库能力
javascript·安全·electron
秋田君3 小时前
Electron 安装踩坑实录
前端·javascript·electron
EndingCoder3 小时前
构建RESTful API:用户管理示例
linux·javascript·node.js
暖木生晖3 小时前
Javascript变量介绍
开发语言·javascript·ecmascript
心.c3 小时前
JavaScript继承详讲
开发语言·javascript·ecmascript