对于一些使用 Electron开发的app, 需要获取一些系统权限,比如录屏权限, 获取摄像头权限,麦克风等等,类似于以下界面:
那么Electron App 应该如何申请呢?
首先我们明确一下macOS中基础权限的分类,可以分为以下几种:
- 隐私权限(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>
- 系统功能权限
<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>
- 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>
- 硬件权限
<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启动时,可以使用代码控制权限申请,会得到类似的对话框:
然后点击系统设置,即可跳转到系统界面,点击手动打开相应的权限,即可完成系统权限的设置.
备注:
那么哪些权限需要我们手动申请呢(可以通过代码开启)?
- 媒体权限
const { systemPreferences } = require('electron')
// 摄像头
await systemPreferences.askForMediaAccess('camera')
// 麦克风
await systemPreferences.askForMediaAccess('microphone')
// 屏幕录制
// 注意:屏幕录制权限需要用户在系统设置中手动授权
systemPreferences.getMediaAccessStatus('screen')
- 通知权限
const { Notification } = require('electron')
// 请求通知权限
async function requestNotificationPermission() {
if (!Notification.isSupported()) return false
const permission = await Notification.requestPermission()
return permission === 'granted'
}
- 辅助功能权限
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
}
// 正常执行需要权限的功能...
}