在 Apex 和 SOQL 中掌握 Salesforce 字段级安全性 (FLS)

背景与应用场景

作为一名 Salesforce 开发人员,我们日常工作不仅仅是构建功能强大的应用程序,更重要的是确保这些应用程序的安全性。在 Salesforce 平台的众多安全特性中,Field-Level Security (FLS),即字段级安全性,是最基本也是最关键的一道防线。它允许管理员精细地控制不同用户(通过 Profiles 配置文件或 Permission Sets 权限集)对特定对象上各个字段的可见性(Read Access)和可编辑性(Edit Access)。

在标准 Salesforce UI 中,FLS 会被自动强制执行。例如,如果一个用户的配置文件被设置为对“客户”对象的“年收入 (AnnualRevenue)”字段只读,那么他们在客户记录页面上就无法编辑这个字段。然而,一个常见的误区是认为 Apex 代码也会自动遵循这些规则。事实恰恰相反,Apex 默认在系统模式 (System Mode) 下运行,这意味着它会忽略用户的 FLS 设置,拥有对所有字段的读写权限。这既是 Apex 强大的原因,也是一个巨大的安全隐患。

想象以下场景:

  • 敏感数据保护:一个自定义的 Visualforce 页面或 Lightning Web Component (LWC) 需要更新客户信息。如果后台的 Apex 控制器没有检查 FLS,一个本应没有权限的用户可能会通过这个自定义界面,间接修改到他本无权编辑的敏感字段,例如合同金额或客户评级。
  • 数据集成:一个外部系统通过 REST API 调用 Apex 服务来创建或更新记录。如果该 Apex 服务没有强制执行 FLS,它可能会写入一些调用用户本不应看到的字段,从而造成数据污染或泄露。
  • 合规性要求:在金融、医疗等行业,对数据访问有严格的法律法规要求(如 GDPR, HIPAA)。在代码中强制执行 FLS 是确保系统满足这些合规性要求不可或缺的一环。

因此,作为专业的 Salesforce 开发人员,我们必须在代码层面主动、显式地强制执行 FLS,确保我们的自定义逻辑和自动化流程与管理员设定的安全策略保持一致,构建一个真正安全可靠的系统。


原理说明

在 Apex 中强制执行 FLS 的核心思想是:在执行任何数据操作(查询或 DML)之前,先检查当前运行用户的权限。Salesforce 平台为我们提供了多种强大的工具来实现这一目标,主要分为三大类:

1. Schema Describe 方法

这是最经典、最灵活的方式。通过 `Schema` 命名空间下的方法,我们可以动态地获取任何对象和字段的元数据描述信息,其中就包含了权限相关的属性。关键对象和方法包括:

  • `DescribeSObjectResult`:描述一个 sObject 的元数据,例如它是否可创建、可查询等。
  • `DescribeFieldResult`:描述一个字段的元数据,这是我们检查 FLS 的核心。它提供了以下关键方法:
    • `isAccessible()`:检查当前用户是否对该字段有读取权限。
    • `isCreateable()`:检查当前用户是否对该字段有创建权限。
    • `isUpdateable()`:检查当前用户是否对该字段有更新权限。

这种方法的优点是极其灵活,你可以在执行 SOQL 查询前动态构建查询字符串,只包含用户可访问的字段;也可以在执行 DML 操作前,逐个检查字段的写入权限。缺点是代码会相对繁琐一些。

2. SOQL 中的 `WITH SECURITY_ENFORCED` 子句

这是一个相对较新的特性,极大地简化了在 SOQL 查询中强制 FLS 的过程。你只需在 SOQL 查询语句的末尾添加 `WITH SECURITY_ENFORCED` 关键字,Salesforce 平台就会在执行查询时自动检查当前用户对查询中所有字段的读取权限。如果用户对任何一个字段没有读取权限,查询将不会返回结果,而是直接抛出一个 `System.QueryException` 异常,提示“field is not accessible”。

这种方法代码简洁,意图明确,是当前在 SOQL 查询中执行 FLS 检查的最佳实践。

3. Security.stripInaccessible 方法

此方法主要用于 DML 操作前的“数据清洗”。当你有一批 sObject 记录(例如,从 LWC 组件传递到 Apex 控制器),并且准备将它们插入或更新到数据库时,可以使用 `Security.stripInaccessible` 方法。该方法会接收一个 sObject 列表和一个操作类型(如 `AccessType.CREATABLE` 或 `AccessType.UPDATABLE`),然后返回一个新的 sObject 列表。在这个新的列表中,所有当前用户没有相应权限的字段都已经被自动移除了。

这样,你就可以安全地对返回的列表执行 DML 操作,而不用担心因为权限不足而导致 `DmlException`。它完美地补充了 `WITH SECURITY_ENFORCED`,形成了从读到写完整的 FLS 强制执行闭环。


示例代码

以下示例均来自 Salesforce 官方文档,展示了如何在不同场景下正确地实施 FLS。

示例一:使用 Schema Describe 方法在 SOQL 执行前检查字段权限

在这个例子中,我们动态构建一个 SOQL 查询,确保只查询当前用户有权访问的字段。这在需要处理多个字段,且权限各不相同的复杂场景中非常有用。

// 要检查的字段列表
List<String> fields = new List<String>{'Name', 'AnnualRevenue', 'OwnerId'};

// 获取 Account 对象上所有字段的描述信息 Map
Map<String, Schema.SObjectField> fieldMap = Schema.SObjectType.Account.fields.getMap();

// 存储用户可访问的字段
List<String> accessibleFields = new List<String>();

// 遍历需要检查的字段列表
for (String fieldName : fields) {
    // 检查用户是否对该字段有读取权限 (isAccessible)
    if (fieldMap.get(fieldName).getDescribe().isAccessible()) {
        accessibleFields.add(fieldName);
    }
}

// 动态构建 SOQL 查询语句,只包含可访问的字段
// String.join 方法会将列表元素用逗号连接起来
String query = 'SELECT ' + String.join(accessibleFields, ',') + ' FROM Account';

// 使用 Database.query 执行动态查询
// 这样可以确保查询本身是安全的,不会因为访问无权限字段而失败
List<sObject> results = Database.query(query);
System.debug(results);

示例二:使用 `WITH SECURITY_ENFORCED` 简化 SOQL 查询

这是目前推荐的方式。代码更简洁,可读性更高。它将权限检查的责任交给了 Salesforce 平台。

// 在 try-catch 块中执行查询,以便捕获可能的权限异常
try {
    // 直接在 SOQL 查询末尾添加 WITH SECURITY_ENFORCED
    // 如果运行此代码的用户对 Name 或 Phone 字段没有读取权限,
    // Salesforce 将会抛出 QueryException
    List<Account> accs = [
        SELECT Name, Phone
        FROM Account
        WITH SECURITY_ENFORCED
    ];
    System.debug('查询成功,用户拥有所有字段的访问权限。');
    System.debug(accs);

} catch (System.QueryException e) {
    // 捕获异常并处理
    // e.getMessage() 通常会包含类似 "No access to field Phone. Either the user doesn't have access or the field doesn't exist." 的信息
    System.debug('查询失败,因为用户缺少对某些字段的访问权限: ' + e.getMessage());
}

示例三:使用 `Security.stripInaccessible` 在 DML 操作前清洗数据

这个例子展示了如何在执行 `update` 操作前,安全地移除用户无权编辑的字段,防止 DML 失败。

// 假设这是从某个外部来源(如 LWC)获取的记录列表
List<Account> accounts = [SELECT Id, Name, AnnualRevenue, TickerSymbol FROM Account LIMIT 2];

// 模拟用户输入,尝试更新多个字段
for (Account acc : accounts) {
    acc.Name = 'New Name From Code'; // 假设用户有权更新名称
    acc.AnnualRevenue = 5000000;   // 假设用户无权更新年收入
}

// 使用 stripInaccessible 方法进行数据清洗
// AccessType.UPDATABLE 指定了我们要检查的是更新权限
// 方法会返回一个新的 sObject 列表,其中不包含用户无权更新的字段
SObjectAccessDecision decision = Security.stripInaccessible(
    AccessType.UPDATABLE,
    accounts
);

// 获取被移除的字段列表,用于调试或记录
System.debug('被移除的无权限字段: ' + decision.getRemovedFields());

// 对清洗后的记录执行 DML 更新操作
// 因为 AnnualRevenue 字段已被移除,所以这个 update 操作不会因为 FLS 权限问题而失败
try {
    update decision.getRecords();
    System.debug('记录更新成功!');
} catch (System.DmlException e) {
    System.debug('更新失败: ' + e.getMessage());
}

注意事项

  • 权限层次结构:请记住,FLS 是 Salesforce 安全模型的一部分。即使用户对某个字段有读/写权限,如果他们对该字段所在的对象没有基本的读取或编辑权限(通过 Profile 或 Permission Set 设置的 CRUD 权限),或者记录级别的共享设置(Sharing Settings)不允许他们访问该记录,他们最终仍然无法访问该字段。FLS 是在对象和记录级权限满足之后才生效的额外限制。
  • API 限制与性能:虽然 `Schema` describe 调用非常有用,但在一个事务中大量调用可能会消耗 CPU 时间。对于频繁执行的代码(如 Trigger),可以考虑将 describe 结果静态缓存起来,避免在同一次事务中重复查询元数据。
  • 错误处理:强制执行 FLS 意味着你的代码可能会因为权限不足而“失败”。因此,必须使用 `try-catch` 块来妥善处理 `QueryException`(由 `WITH SECURITY_ENFORCED` 抛出)和 `DmlException`(由尝试写入无权限字段引起)。优雅地处理这些异常,并向用户提供清晰的反馈,是提升用户体验的关键。
  • 系统模式的本质:再次强调,Apex 默认在系统模式下运行。这意味着,如果你不采取上述任何一种措施,你的代码将完全绕过 FLS。这在某些后台批量处理场景中是必要的,但对于用户触发的交互式操作,几乎总是需要显式地强制执行 FLS。
  • API 版本:`WITH SECURITY_ENFORCED` 和 `Security.stripInaccessible` 等较新的安全特性需要较高的 API 版本。在开发时请确保你的 Apex 类或 Trigger 使用的 API 版本支持这些特性。

总结与最佳实践

在 Salesforce 开发中,安全性不是一个可选项,而是一个必须项。字段级安全性 (FLS) 是数据保护的基石,而作为开发人员,我们有责任在代码中尊重并强制执行这些由管理员设定的规则。

以下是总结的最佳实践:

  1. 默认使用 `WITH SECURITY_ENFORCED`:对于所有新的 SOQL 查询,都应优先使用此子句。它是最简单、最直接的 FLS 强制执行方式,让你的代码更安全、更易读。
  2. DML 操作前使用 `stripInaccessible`:在处理来自用户界面或其他不可信来源的数据时,执行 DML 操作前务必使用 `Security.stripInaccessible`。这可以预防性地消除因 FLS 权限不足导致的 DML 错误。
  3. 谨慎使用 Schema Describe:当需要构建复杂的动态逻辑,例如根据用户权限动态渲染页面组件或构建非常复杂的查询时,`Schema` describe 方法是你的强大工具。但对于常规的读写操作,优先选择前两种更现代化的方法。
  4. 封装安全检查逻辑:在大型项目中,可以考虑创建一个共享的 Apex 工具类(例如 `SecurityUtil.cls`),将常用的权限检查逻辑封装成可重用的方法。这有助于保持代码库的一致性和可维护性。
  5. 遵循最小权限原则:无论是设计数据模型还是编写代码,始终遵循最小权限原则。只请求和操作你真正需要的字段,而不是简单地 `SELECT *`(SOQL 也不支持),这不仅是性能上的好习惯,也是安全上的好习惯。

通过将这些原则和技术融入你的日常开发实践中,你将能够构建出既功能丰富又安全可靠的 Salesforce 应用程序,赢得客户和用户的信任。

评论

此博客中的热门博文

Salesforce Einstein AI 编程实践:开发者视角下的智能预测

Salesforce 登录取证:深入解析用户访问监控与安全

Salesforce Experience Cloud 技术深度解析:构建社区站点 (Community Sites)