Token Validation in Electron

Created on: September 28, 2024

Answered using OpenAI 01 Mini by OpenAI01

Question

const { app, BrowserWindow, session } = require('electron');

function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
devTools: false, // 禁用主窗口开发者工具
},
});

// 禁用 F12 和 Ctrl+Shift+I 开发者工具快捷键
win.webContents.on('before-input-event', (event, input) => {
if (input.key === 'F12' || (input.control && input.shift && input.key === 'I')) {
event.preventDefault(); // 禁止 F12 和 Ctrl+Shift+I
}
});

// 处理新窗口的打开行为
win.webContents.setWindowOpenHandler(({ url }) => {
// 如果你想禁止所有新窗口弹出,可以返回 { action: 'deny' }
return {
action: 'allow', // 允许新窗口打开
overrideBrowserWindowOptions: {
webPreferences: {
devTools: false, // 禁用新窗口的开发者工具
nodeIntegration: false,
contextIsolation: true,
}
}
};
});

// 处理新窗口的创建
app.on('web-contents-created', (event, contents) => {
// 拦截所有窗口和 WebView 创建事件
if (contents.getType() === 'webview' || contents.getType() === 'window') {
// 禁用新窗口的开发者工具
contents.on('before-input-event', (event, input) => {
if (input.key === 'F12' || (input.control && input.shift && input.key === 'I')) {
event.preventDefault(); // 禁用新窗口的快捷键
}
});
contents.on('devtools-opened', () => {
contents.closeDevTools(); // 强制关闭任何已打开的开发者工具
});
}
});


win.loadURL( 'https://foo.bar' );
}

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
===========
在 win.loadURL( 'https://foo.bar' );之前加一段程序,先弹出一个输入框,输入一个Token字符串,这个字符串有2部分组成,中间用"mclubmclub"隔开,例如20240101mclubmclubhttp://foo.bar,前一部分是这个Token字符串的expireDate,后面一部分是Token后面win.loadURL的首页。输入完字符串后js要判断如果过期则提示用户并退出系统,如果没过期则load首页。

Thought

添加日期/输入框

我在 Electron 应用中添加了一个对话框,要求用户输入一个包含 Token 和 URL 的字符串。然后,我检查日期是否过期,并相应加载 URL。

探索用户输入的路径

在Electron主进程中,直接使用 dialog.showInputBox 可能不可行。考虑创建含输入框的浏览器窗口或使用同步方法。通常,Electron应用通过渲染窗口获取用户输入,或借助node模块如 prompt-syncinquirer 实现。

首先展示输入提示

创建输入窗口

由于 Electron 没有内置的提示输入功能,我正在创建一个模态 BrowserWindow 以获取输入。这样我可以将输入发送回主进程。这样做可以简单有效地实现功能。

探索方式

我正在探索如何异步处理输入,尽管主窗口尚未创建。这让我思考是否可以使用 Electron 的 showMessageBox,最后决定创建一个独立窗口来获取用户输入。

完善窗口功能

首先,我在代码中修改了createWindow函数以显示一个模态窗口来获取token,并使用一个异步函数获取token,然后处理获取到的token,最后创建主窗口并加载URL。

创建独立窗口

我正在创建一个独立的输入窗口来获取用户的令牌,确保这个窗口可以通过IPC与主进程交互。

Answer

To achieve the desired functionality—prompting the user for a token, validating it, and then loading the appropriate URL—you can follow these steps:

  1. Create a Modal Input Window: Since Electron doesn't provide a built-in input dialog, you'll need to create a separate BrowserWindow that serves as a modal dialog to collect the token from the user.

  2. Use IPC (Inter-Process Communication): Communicate between the main process and the renderer process to send and receive the token.

  3. Validate the Token: Parse the token to extract the expiration date and URL, then validate whether the token is still valid.

  4. Load the Main Window or Exit: Based on the validation, either load the main window with the specified URL or alert the user and exit the application.

Below is a comprehensive implementation of these steps.


1. Project Structure

To organize the application, set up the following file structure:

your-app/
├── main.js
├── preload.js
├── inputPreload.js
├── input.html
├── index.html
├── package.json
  • main.js: Main process script.
  • preload.js: Preload script for the main window.
  • inputPreload.js: Preload script for the input dialog.
  • input.html: HTML for the input dialog.
  • index.html: (Optional) HTML for the main window if needed.
  • package.json: Your project's configuration.

2. main.js

This is the main process script where the application lifecycle is managed.

// main.js const { app, BrowserWindow, ipcMain, dialog } = require('electron'); const path = require('path'); let mainWindow; function createMainWindow(loadURL) { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true, devTools: false, // Disable dev tools }, }); mainWindow.loadURL(loadURL); // Disable F12 and Ctrl+Shift+I mainWindow.webContents.on('before-input-event', (event, input) => { if ( input.key === 'F12' || (input.control && input.shift && input.key.toLowerCase() === 'i') ) { event.preventDefault(); } }); // Handle new window behavior mainWindow.webContents.setWindowOpenHandler(({ url }) => { return { action: 'allow', overrideBrowserWindowOptions: { webPreferences: { devTools: false, nodeIntegration: false, contextIsolation: true, }, }, }; }); // Handle all web contents app.on('web-contents-created', (event, contents) => { if (contents.getType() === 'webview' || contents.getType() === 'window') { contents.on('before-input-event', (event, input) => { if ( input.key === 'F12' || (input.control && input.shift && input.key.toLowerCase() === 'i') ) { event.preventDefault(); } }); contents.on('devtools-opened', () => { contents.closeDevTools(); }); } }); } function createInputWindow() { return new Promise((resolve, reject) => { const inputWindow = new BrowserWindow({ width: 400, height: 200, parent: null, modal: true, show: false, webPreferences: { preload: path.join(__dirname, 'inputPreload.js'), nodeIntegration: false, contextIsolation: true, }, }); inputWindow.loadFile('input.html'); inputWindow.once('ready-to-show', () => { inputWindow.show(); }); // Listen for the token submission ipcMain.once('token-submission', (event, token) => { inputWindow.close(); resolve(token); }); // Handle window close without submission inputWindow.on('closed', () => { reject(new Error('Input window closed without submission.')); }); }); } function validateToken(token) { const separator = 'mclubmclub'; const parts = token.split(separator); if (parts.length !== 2) { return { valid: false, message: 'Invalid token format.' }; } const [expireDateStr, url] = parts; const expireDate = new Date( expireDateStr.slice(0, 4), parseInt(expireDateStr.slice(4, 6)) - 1, expireDateStr.slice(6, 8) ); const currentDate = new Date(); // Set the time of both dates to midnight to compare only dates expireDate.setHours(0, 0, 0, 0); currentDate.setHours(0, 0, 0, 0); if (isNaN(expireDate.getTime())) { return { valid: false, message: 'Invalid expiration date.' }; } if (currentDate > expireDate) { return { valid: false, message: 'Token has expired.' }; } // Optionally, validate the URL format try { new URL(url); } catch (e) { return { valid: false, message: 'Invalid URL in token.' }; } return { valid: true, url }; } async function initializeApp() { try { const token = await createInputWindow(); const validation = validateToken(token); if (!validation.valid) { await dialog.showMessageBox({ type: 'error', title: 'Invalid Token', message: validation.message, }); app.quit(); return; } // Token is valid, create the main window with the specified URL createMainWindow(validation.url); } catch (error) { // Handle errors such as window closed without submission dialog.showMessageBox({ type: 'error', title: 'Error', message: error.message, }); app.quit(); } } app.whenReady().then(initializeApp); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });

Explanation:

  1. Main Window Creation (createMainWindow):

    • Configures the main BrowserWindow with desired settings.
    • Disables developer tools and certain keyboard shortcuts.
    • Handles new window creation to apply the same restrictions.
  2. Input Window Creation (createInputWindow):

    • Creates a modal BrowserWindow to prompt the user for the token.
    • Uses IPC to receive the token once the user submits it.
  3. Token Validation (validateToken):

    • Splits the token by the separator mclubmclub.
    • Parses the expiration date and compares it with the current date.
    • Validates the URL format.
  4. Application Initialization (initializeApp):

    • Orchestrates the flow: prompts for the token, validates it, and proceeds accordingly.

3. inputPreload.js

This preload script bridges the input window's renderer process with the main process securely using IPC.

// inputPreload.js const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('electronAPI', { submitToken: (token) => ipcRenderer.send('token-submission', token), });

4. input.html

This HTML file provides the user interface for the token input dialog.

<!-- input.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Enter Token</title> <style> body { font-family: Arial, sans-serif; padding: 20px; } #tokenInput { width: 100%; padding: 8px; margin-bottom: 10px; } #submitBtn { padding: 8px 16px; } #errorMsg { color: red; margin-top: 10px; } </style> </head> <body> <h2>Please Enter Your Token</h2> <input type="text" id="tokenInput" placeholder="e.g., 20240101mclubmclubhttp://foo.bar" /> <br /> <button id="submitBtn">Submit</button> <div id="errorMsg"></div> <script> const submitBtn = document.getElementById('submitBtn'); const tokenInput = document.getElementById('tokenInput'); const errorMsg = document.getElementById('errorMsg'); submitBtn.addEventListener('click', () => { const token = tokenInput.value.trim(); if (!token) { errorMsg.textContent = 'Token cannot be empty.'; return; } window.electronAPI.submitToken(token); }); // Optionally, handle Enter key tokenInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { submitBtn.click(); } }); </script> </body> </html>

Explanation:

  • User Interface: Provides a simple form with an input field and a submit button.
  • Validation: Ensures that the token field is not empty before submission.
  • IPC Communication: Sends the token to the main process upon submission.

5. preload.js

This preload script can be used for the main window if you need to expose any APIs. For this specific functionality, it can remain empty or include necessary APIs as per your application's requirements.

// preload.js const { contextBridge } = require('electron'); // Expose APIs to the renderer process if needed contextBridge.exposeInMainWorld('myAPI', { // Define your APIs here });

6. package.json

Ensure your package.json is set up correctly to use Electron and includes necessary scripts.

{ "name": "your-app", "version": "1.0.0", "description": "Electron app with token validation", "main": "main.js", "scripts": { "start": "electron ." }, "devDependencies": { "electron": "^25.0.0" }, "dependencies": {} }

Installation:

  1. Initialize the Project:

    npm init -y
  2. Install Electron:

    npm install --save-dev electron
  3. Run the Application:

    npm start

7. Additional Considerations

  • Security:

    • Context Isolation: Ensures that the renderer process is isolated from the main process, enhancing security.
    • Preload Scripts: Use preload scripts to expose only the necessary APIs to the renderer.
  • Error Handling:

    • The application handles cases where the input window is closed without submission.
    • It also validates the token format and URL, providing appropriate error messages.
  • User Experience:

    • The input dialog is modal, preventing interaction with other windows until the token is submitted.
    • Provides immediate feedback if the token is invalid or expired.

By following the above implementation, your Electron application will prompt the user for a token, validate its expiration date, and load the specified URL if the token is valid. If the token is invalid or expired, the user will be alerted, and the application will exit gracefully.

Share this Q&A