Salesforce 开发人员指南:在 Apex 中实施字段级安全性 (FLS)
背景与应用场景
在 Salesforce 平台中,数据安全是基石。一个设计良好的安全模型能够确保合适的用户在合适的时机访问到合适的数据。Salesforce 提供了多层次的数据安全模型,包括组织级安全、对象级安全、记录级安全以及我们今天要深入探讨的主题——Field-Level Security (字段级安全性, FLS)。
作为一名 Salesforce 开发人员,我们经常编写 Apex 代码来处理业务逻辑、自动化流程或构建自定义用户界面。然而,一个常见的误区是认为 Apex 会自动遵循我们在用户界面上设置的所有安全规则。事实并非如此。默认情况下,Apex 在所谓的系统模式 (system mode)下运行,这意味着它会忽略对象级和字段级安全性。这既是其强大之处(允许后台进程处理所有数据),也是一个巨大的潜在安全风险。
想象以下应用场景:
- 薪酬管理:在自定义的 `Employee__c` 对象上有一个 `Salary__c` 字段。公司的政策规定,只有人力资源部门的用户才能查看和编辑此字段。一个普通的员工在查看自己的记录时不应该看到这个字段。
- 销售折扣:在 `Opportunity` 对象上有一个 `Max_Discount__c` 字段,只有销售经理才有权限查看。如果一个销售代表通过自定义的 LWC 组件执行了某个 Apex 方法,而该方法未经检查就返回了所有字段,那么这个敏感的折扣信息就可能被泄露。
- 合规性要求:在金融或医疗行业,法规(如 GDPR、HIPAA)严格规定了对个人身份信息 (PII) 的访问控制。例如,`Contact` 对象上的 `SSN__c` (社会安全号码) 字段必须受到严格的 FLS 控制,只有经过授权的后台系统或特定角色的用户才能访问。
如果我们的 Apex 代码不主动检查并强制执行 FLS,那么上述所有安全设置都将被绕过。用户可能会通过我们编写的自定义功能,无意中访问到他们本不应看到或修改的数据。因此,理解并学会在 Apex 中正确实施 FLS,是每一位 Salesforce 开发人员的核心职责和必备技能。
原理说明
从根本上说,Field-Level Security (FLS) 控制着用户对特定对象上单个字段的访问权限。这些权限通过 Profiles (简档) 和 Permission Sets (权限集) 进行配置,主要分为两个级别:
- Visible (可见):用户是否有权限在页面布局、报表和 API 查询结果中看到这个字段。如果一个字段不可见,那么它自然也是不可编辑的。
- Read-Only (只读):如果字段可见,该设置决定用户是否可以编辑该字段的值。
作为开发人员,我们需要关注的是 Apex 的执行上下文。如前所述,Apex 默认在系统模式下运行,无视用户的 FLS 设置。这意味着,一段简单的 SOQL 查询,如 `SELECT Id, Name, Salary__c FROM Employee__c`,无论执行该代码的用户是否有权访问 `Salary__c` 字段,该查询都会成功返回其值。
为了解决这个问题,Salesforce 提供了多种机制,让我们可以在 Apex 中强制执行 FLS,将代码的执行上下文从“系统模式”的特权状态,转变为尊重当前用户权限的用户模式 (user mode)行为。主要有以下三种方式:
1. 使用 `Schema.DescribeFieldResult` 进行显式检查
这是最传统也是最灵活的方法。通过 Salesforce 的 Schema 类,我们可以动态地获取任何对象或字段的元数据描述。`DescribeFieldResult` 对象包含了一系列布尔方法,如 `isAccessible()`(可访问/可见)、`isCreateable()`(可创建)和 `isUpdateable()`(可更新)。我们可以在执行 SOQL 查询或 DML 操作之前,调用这些方法来检查当前用户是否具备相应权限。
2. 在 SOQL 查询中使用 `WITH SECURITY_ENFORCED`
这是在 API v45.0 (Spring '19) 中引入的现代化、简洁的 FLS 实施方式。只需在 SOQL `SELECT` 语句的末尾添加 `WITH SECURITY_ENFORCED` 子句,Salesforce 就会在查询执行时自动检查当前用户对查询中所有字段的 FLS 可见性权限。如果用户对任何一个字段没有读取权限,查询将直接抛出一个 `System.QueryException` 异常,而不会返回任何数据。这是一种“全有或全无”的强制执行模式。
3. 使用 `Security.stripInaccessible` 方法进行数据清理
在 API v48.0 (Winter '20) 中引入,`stripInaccessible` 方法提供了一种更为优雅的处理方式。它接收一个 sObject 列表作为输入,并返回一个新的 sObject 列表。在这个新的列表中,所有当前用户无权访问的字段都已被自动移除。与 `WITH SECURITY_ENFORCED` 不同,它不会抛出异常,而是“静默地”清理数据。这在准备将数据返回给 Lightning 组件或在执行 DML 更新前移除无权修改的字段时非常有用。
了解这三种方法的适用场景和行为差异,是编写安全可靠的 Apex 代码的关键。
示例代码
以下代码示例均来自 Salesforce 官方文档,展示了如何在 Apex 中应用上述原理。
示例 1: 使用 Schema Describe 检查字段权限
在执行 DML 更新操作前,检查用户是否对 `Opportunity` 对象的 `Name` 和 `Description` 字段有更新权限。这种方法提供了最精细的控制。
// 假设我们有一个 Opportunity 列表需要更新 List<Opportunity> opportunities = [SELECT Id, Name, Description FROM Opportunity LIMIT 1]; // 获取 Opportunity 对象的 sObject Token Schema.SObjectType s = Schema.getGlobalDescribe().get('Opportunity'); // 获取对象所有字段的描述信息 Map Map<String, Schema.SObjectField> fieldMap = s.getDescribe().fields.getMap(); // 检查 Name 字段是否可更新 if (!fieldMap.get('Name').getDescribe().isUpdateable()) { // 如果不可更新,可以记录错误、通知用户或采取其他措施 // 这里我们只是在调试日志中输出一条消息 System.debug('User does not have permission to update the Name field.'); // 也可以给页面添加错误信息 // opportunities[0].addError('You do not have permission to update the Name field.'); return; // 终止操作 } // 检查 Description 字段是否可更新 if (!fieldMap.get('Description').getDescribe().isUpdateable()) { System.debug('User does not have permission to update the Description field.'); return; // 终止操作 } // 只有当所有权限检查通过后,才执行 DML 操作 try { update opportunities; } catch (DmlException e) { // 处理 DML 异常 System.debug('An error occurred during the DML operation: ' + e.getMessage()); }
示例 2: 在 SOQL 查询中强制执行 FLS
使用 `WITH SECURITY_ENFORCED` 来确保查询只返回用户有权查看的字段。如果用户无权访问 `AnnualRevenue` 或 `Industry` 字段,整个查询将失败并抛出异常。
try { // 在 SOQL 查询末尾添加 WITH SECURITY_ENFORCED // 这个查询将自动检查用户对 Account 的对象级读取权限 // 以及对 Id, Name, AnnualRevenue, Industry 字段的字段级读取权限 List<Account> accounts = [ SELECT Id, Name, AnnualRevenue, Industry FROM Account WITH SECURITY_ENFORCED ]; // 如果代码能执行到这里,说明用户权限检查通过 // 可以安全地处理返回的 accounts 列表 for(Account acc : accounts) { System.debug('Account Name: ' + acc.Name); } } catch (System.QueryException e) { // 如果用户缺少任何一个字段的 FLS 权限,就会捕获到这个异常 System.debug('Query failed because of missing FLS permissions: ' + e.getMessage()); // 在实际应用中,这里应该向用户显示一条友好的错误消息 }
示例 3: 使用 `stripInaccessible` 清理数据
从查询结果中移除用户无权访问的字段,然后再将数据用于 DML 操作或返回给前端。这种方法可以防止 FLS 异常,并确保只处理授权数据。
// 首先,执行一个标准的 SOQL 查询(在系统模式下) // 这会获取所有字段的值,无论当前用户权限如何 List<Contact> contacts = [SELECT Id, LastName, Email, AssistantName, Phone FROM Contact]; // 使用 Security.stripInaccessible 方法来移除当前用户无权访问的字段 // 第一个参数是访问级别检查类型,这里我们检查 READ 权限 // 第二个参数是 sObject 列表 // 这个方法会返回一个新的、经过清理的 sObject 列表 SObjectAccessDecision decision = Security.stripInaccessible( AccessType.READABLE, contacts ); // 获取清理后的 sObject 列表 List<Contact> strippedContacts = decision.getRecords(); // 打印被移除的字段,用于调试和审计 System.debug('Stripped fields for FLS: ' + decision.getRemovedFields().get('Contact')); // 现在,strippedContacts 列表是安全的,可以返回给 LWC 或 Visualforce 页面 // 或者用于后续的 DML 操作 // 例如,如果用户无权访问 AssistantName,那么在 strippedContacts 列表中的每个 Contact 记录上 // AssistantName 字段的值都将为 null,即使数据库中有值 // 另一个常见用例是在更新前清理数据 // 假设用户提交了一个包含 Phone 字段的 Contact 对象 Contact contactToUpdate = new Contact(Id = '003_some_id', Phone = '123-456-7890'); List<Contact> singleContactList = new List<Contact>{ contactToUpdate }; // 检查 UPDATE 权限 SObjectAccessDecision updateDecision = Security.stripInaccessible( AccessType.UPDATABLE, singleContactList ); // 使用清理后的列表进行更新 // 如果用户没有 Phone 字段的编辑权限,该字段会从对象中移除,从而防止 DML 失败 update updateDecision.getRecords();
注意事项
权限与 Governor Limits
频繁调用 `Schema.getDescribe()` 方法,尤其是在循环中,可能会消耗大量 CPU 时间并触及 Governor Limits。最佳实践是在类的开头或方法开始时,一次性获取所需对象的所有字段描述,并将其缓存在一个 `Map` 中供后续使用。
API 版本
`WITH SECURITY_ENFORCED` 需要 Apex 类或触发器的 API 版本为 45.0 或更高。`Security.stripInaccessible` 需要 API 版本为 48.0 或更高。在旧代码库中工作时,请务必检查 API 版本。
错误处理
使用 `WITH SECURITY_ENFORCED` 时,必须结合 `try-catch` 块来处理可能抛出的 `System.QueryException`。否则,一个权限不足的用户就可能导致整个事务失败。你应该为用户提供清晰的错误提示,而不是让他们看到一个不友好的系统错误页面。
动态 SOQL 的限制
`WITH SECURITY_ENFORCED` 子句不能在动态 SOQL 查询(即使用 `Database.query()` 方法)中使用。对于动态查询,你必须回退到使用 `Security.stripInaccessible` 或手动的 `Schema.DescribeFieldResult` 检查。
`stripInaccessible` 的行为
请记住,`stripInaccessible` 是“静默”操作。它不会告诉你为什么一个字段被移除了,只会移除它。如果你需要根据权限的有无来执行不同的业务逻辑,那么 `Schema.DescribeFieldResult` 的显式检查仍然是更好的选择。
测试的重要性
在编写 Apex 测试类时,必须使用 `System.runAs(user)` 方法来模拟不同权限的用户。创建具有不同简档和权限集的测试用户,并验证你的代码在各种权限场景下是否都能按预期工作(例如,正确抛出异常、正确清理数据、或阻止 DML 操作)。
总结与最佳实践
作为 Salesforce 开发人员,我们构建的功能直接影响用户与数据的交互方式,因此,我们有责任确保这些交互是安全的。忽略 Apex 中的 Field-Level Security 是一个严重的安全疏忽,可能导致数据泄露和不合规的风险。
以下是关于在 Apex 中实施 FLS 的最佳实践总结:
优先选择声明式安全:
始终首先利用 Profiles 和 Permission Sets 来配置 FLS。代码应该强制执行这些已有的配置,而不是在代码中硬编码安全逻辑。默认强制执行:
养成在所有数据访问和操作代码中检查 FLS 的习惯。不要假设你的代码总是在安全的上下文中被调用。拥抱现代方法:
对于新的开发项目,优先使用 `WITH SECURITY_ENFORCED` 进行只读数据查询。它简洁、意图明确,并且能快速失败,这通常是期望的安全行为。安全地处理输入和输出:
在从外部来源(如 LWC)接收 sObject 记录并准备进行 DML 操作时,或在将数据发送到客户端之前,使用 `Security.stripInaccessible` 来清理数据。这可以有效防止非法数据更新和敏感数据泄露。保留精细控制的工具:
当需要基于特定字段权限执行复杂逻辑或提供自定义错误消息时,使用 `Schema.DescribeFieldResult` 方法 (`isAccessible`, `isUpdateable` 等)。编写全面的安全测试:
利用 `System.runAs()` 创建覆盖高权限用户和低权限用户的测试用例,确保你的安全逻辑在所有情况下都坚不可摧。
通过遵循这些原则和实践,我们可以构建出既功能强大又安全可靠的 Salesforce 应用,保护客户的数据,并维护平台的完整性。
评论
发表评论