Bootstrap

electron 如何申请 Mac 系统权限

对于一些使用 Electron开发的app, 需要获取一些系统权限,比如录屏权限, 获取摄像头权限,麦克风等等,类似于以下界面:

在这里插入图片描述

那么Electron App 应该如何申请呢?
首先我们明确一下macOS中基础权限的分类,可以分为以下几种:

  1. 隐私权限(Private Permissions) :
<!-- entitlements.mac.plist -->
<dict>
    <!-- 摄像头 -->
    <key>com.apple.security.device.camera</key>
    <true/>
    
    <!-- 麦克风 -->
    <key>com.apple.security.device.microphone</key>
    <true/>
    
    <!-- 位置信息 -->
    <key>com.apple.security.personal-information.location</key>
    <true/>
    
    <!-- 通讯录 -->
    <key>com.apple.security.personal-information.addressbook</key>
    <true/>
    
    <!-- 日历 -->
    <key>com.apple.security.personal-information.calendars</key>
    <true/>
    
    <!-- 照片 -->
    <key>com.apple.security.personal-information.photos-library</key>
    <true/>
    
    <!-- 屏幕录制 -->
    <key>com.apple.security.screen-recording</key>
    <true/>
    
    <!-- 辅助功能 -->
    <key>com.apple.security.automation.apple-events</key>
    <true/>
</dict>

  1. 系统功能权限
<dict>
    <!-- 网络访问 -->
    <key>com.apple.security.network.client</key>
    <true/>
    
    <!-- 作为服务器接收连接 -->
    <key>com.apple.security.network.server</key>
    <true/>
    
    <!-- 文件访问 -->
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
    
    <!-- USB访问 -->
    <key>com.apple.security.device.usb</key>
    <true/>
    
    <!-- 蓝牙访问 -->
    <key>com.apple.security.device.bluetooth</key>
    <true/>
    
    <!-- 打印权限 -->
    <key>com.apple.security.print</key>
    <true/>
</dict>

  1. App Sandbox 相关权限:
<dict>
    <!-- 启用沙箱 -->
    <key>com.apple.security.app-sandbox</key>
    <true/>
    
    <!-- 读取下载文件夹 -->
    <key>com.apple.security.files.downloads.read-write</key>
    <true/>
    
    <!-- 读写用户选择的文件 -->
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
    
    <!-- 读写图片文件夹 -->
    <key>com.apple.security.files.pictures.read-write</key>
    <true/>
    
    <!-- 读写音乐文件夹 -->
    <key>com.apple.security.files.music.read-write</key>
    <true/>
</dict>

  1. 硬件权限
<dict>
    <!-- 音频输入 -->
    <key>com.apple.security.device.audio-input</key>
    <true/>
    
    <!-- HID设备访问 -->
    <key>com.apple.security.device.usb</key>
    <true/>
    
    <!-- 打印机访问 -->
    <key>com.apple.security.print</key>
    <true/>
</dict>

那么基础权限请求方式为:

const { systemPreferences } = require('electron')

// 检查和请求屏幕录制权限
async function requestScreenCapture() {
  // 检查权限状态
  const status = systemPreferences.getMediaAccessStatus('screen')
  
  if (status !== 'granted') {
    // 请求权限
    const granted = await systemPreferences.askForMediaAccess('screen')
    return granted
  }
  
  return true
}


辅助权限的请求方式为:

const { app } = require('electron')

// 检查辅助功能权限
function checkAccessibilityPermission() {
  return systemPreferences.isTrustedAccessibilityClient(false)
}

// 请求辅助功能权限
function requestAccessibilityPermission() {
  return systemPreferences.isTrustedAccessibilityClient(true)
}

完善的权限管理类为:

class MacPermissions {
  constructor() {
    this.systemPreferences = require('electron').systemPreferences
  }

  async checkPermission(type) {
    switch(type) {
      case 'screen':
        return this.systemPreferences.getMediaAccessStatus('screen')
      case 'camera':
        return this.systemPreferences.getMediaAccessStatus('camera')
      case 'microphone':
        return this.systemPreferences.getMediaAccessStatus('microphone')
      case 'accessibility':
        return this.systemPreferences.isTrustedAccessibilityClient(false)
    }
  }

  async requestPermission(type) {
    try {
      switch(type) {
        case 'screen':
        case 'camera':
        case 'microphone':
          return await this.systemPreferences.askForMediaAccess(type)
        case 'accessibility':
          return this.systemPreferences.isTrustedAccessibilityClient(true)
      }
    } catch(error) {
      console.error(`Error requesting ${type} permission:`, error)
      return false
    }
  }
}

同时需要一个build文件夹,文件夹地址与dist同级别:
在这里插入图片描述
在 build文件夹中, 需要一个 entitlements.mac.plist文件,文件中需要声明所需要的权限:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- 屏幕录制权限 -->
    <key>com.apple.security.screen-recording</key>
    <true/>
    
    <!-- 辅助功能权限 -->
    <key>com.apple.security.automation.apple-events</key>
    <true/>
    
    <!-- 摄像头访问权限 -->
    <key>com.apple.security.device.camera</key>
    <true/>
    
    <!-- 麦克风访问权限 -->
    <key>com.apple.security.device.microphone</key>
    <true/>
    
    <!-- 照片库访问权限 -->
    <key>com.apple.security.personal-information.photos-library</key>
    <true/>
    
    <!-- 位置信息访问权限 -->
    <key>com.apple.security.personal-information.location</key>
    <true/>
</dict>
</plist>

配置好之后, 需要在 package.json中配置mac的属性:

{
  "build": {
    "mac": {
      "hardenedRuntime": true,
      "entitlements": "build/entitlements.mac.plist",
      "entitlementsInherit": "build/entitlements.mac.plist"
    }
  }
}

在打包时, electron-builder 会自动将这些权限配置应用到最终的应用程序中。

然后再App启动时,可以使用代码控制权限申请,会得到类似的对话框:

在这里插入图片描述

然后点击系统设置,即可跳转到系统界面,点击手动打开相应的权限,即可完成系统权限的设置.

备注:

那么哪些权限需要我们手动申请呢(可以通过代码开启)?

  1. 媒体权限
const { systemPreferences } = require('electron')

// 摄像头
await systemPreferences.askForMediaAccess('camera')

// 麦克风
await systemPreferences.askForMediaAccess('microphone')

// 屏幕录制
// 注意:屏幕录制权限需要用户在系统设置中手动授权
systemPreferences.getMediaAccessStatus('screen')
  1. 通知权限
const { Notification } = require('electron')

// 请求通知权限
async function requestNotificationPermission() {
  if (!Notification.isSupported()) return false
  
  const permission = await Notification.requestPermission()
  return permission === 'granted'
}

  1. 辅助功能权限
const { systemPreferences } = require('electron')

// 检查辅助功能权限
systemPreferences.isTrustedAccessibilityClient(false)

以下权限需要用户在系统设置中手动开启(无法通过代码直接请求):

文件系统权限:

  • 访问Documents、Desktop、Downloads等目录
  • 访问照片库
  • 访问通讯录
  • 访问日历
  • 访问提醒事项

系统权限:

  • 屏幕录制
  • 辅助功能
  • 完全磁盘访问权限
  • 自动化权限

那么如何引导用户开启呢?


const { dialog, shell } = require('electron')

class PermissionGuide {
  static async showSettingsGuide(permissionType) {
    const guides = {
      screen: {
        title: '需要屏幕录制权限',
        message: '请在系统设置中允许屏幕录制权限',
        prefPane: 'Privacy_ScreenCapture'
      },
      photos: {
        title: '需要照片访问权限',
        message: '请在系统设置中允许照片访问权限',
        prefPane: 'Privacy_Photos'
      },
      files: {
        title: '需要文件访问权限',
        message: '请在系统设置中允许文件访问权限',
        prefPane: 'Privacy_FilesAndFolders'
      },
      accessibility: {
        title: '需要辅助功能权限',
        message: '请在系统设置中允许辅助功能权限',
        prefPane: 'Privacy_Accessibility'
      }
    }

    const guide = guides[permissionType]
    if (!guide) return

    const result = await dialog.showMessageBox({
      type: 'info',
      title: guide.title,
      message: guide.message,
      buttons: ['打开系统设置', '取消']
    })

    if (result.response === 0) {
      // 打开系统设置对应页面
      shell.openExternal(`x-apple.systempreferences:com.apple.preference.security?${guide.prefPane}`)
    }
  }
}

// 完整的权限管理类
class PermissionManager {
  // 检查需要手动申请的权限
  async checkMediaPermission(type) {
    const status = await systemPreferences.getMediaAccessStatus(type)
    if (status === 'not-determined') {
      return await systemPreferences.askForMediaAccess(type)
    }
    return status === 'granted'
  }

  // 检查需要手动开启的权限
  async checkSystemPermission(type) {
    let status = false
    
    switch(type) {
      case 'screen':
        status = systemPreferences.getMediaAccessStatus('screen') === 'granted'
        break
      case 'accessibility':
        status = systemPreferences.isTrustedAccessibilityClient(false)
        break
      // 其他系统权限检查...
    }

    if (!status) {
      await PermissionGuide.showSettingsGuide(type)
    }

    return status
  }

  // 权限检查和请求的统一接口
  async ensurePermission(type) {
    // 需要手动申请的权限
    if (['camera', 'microphone'].includes(type)) {
      return await this.checkMediaPermission(type)
    }
    
    // 需要在系统设置中手动开启的权限
    if (['screen', 'accessibility', 'photos', 'files'].includes(type)) {
      return await this.checkSystemPermission(type)
    }

    return false
  }
}

// 使用示例
async function example() {
  const permissionManager = new PermissionManager()

  // 检查和请求摄像头权限
  const hasCameraPermission = await permissionManager.ensurePermission('camera')
  if (!hasCameraPermission) {
    console.log('未获得摄像头权限')
    return
  }

  // 检查屏幕录制权限
  const hasScreenPermission = await permissionManager.ensurePermission('screen')
  if (!hasScreenPermission) {
    console.log('未获得屏幕录制权限')
    return
  }

  // 正常执行需要权限的功能...
}
;