Apex 安全与共享模型
Apex 安全模型包括记录级、字段级和对象级安全机制。你可以使用 with sharing、without sharing 和 inherited sharing 关键字控制记录级安全模式。Apex 默认在用户模式下运行——用户权限和字段级安全被强制执行。API 67.0+ 中,WITH SECURITY_ENFORCED 子句已被 WITH USER_MODE 取代。
强制共享规则
共享规则始终默认强制执行。使用 with sharing/without sharing/inherited sharing 控制记录级安全。不想强制执行共享规则必须声明为 without sharing。共享规则与对象/字段级权限是不同的机制,可以共存。
public with sharing class CWith {
// 所有代码强制执行共享规则
Account a = [SELECT ...];
}
public without sharing class CWithout {
// 所有代码忽略共享规则——如同拥有 Modify All Data 权限
Account a = [SELECT ...];
public static void m() {
CWith.m(); // 调用 with sharing 类时共享规则被强制执行
// 调用返回后恢复 without sharing 模式
}
public class CInner {
// 内部类使用调用者的共享上下文——不从外部类继承
}
public class CInnerWithOut extends CWithout {
// 继承父类的 without sharing 设置
}
}
影响范围:SOQL/SOSL 查询可能返回比系统上下文更少的行;DML 操作可能因权限不足而失败(如指定了当前用户无权访问的外键值)。注意:所有引用 Pricebook2 的查询忽略 with sharing——返回所有价格手册。API 67.0+ 中无显式声明的类默认以 with sharing 运行。
强制对象与字段权限
Apex 默认在用户模式下运行。要忽略当前用户的 FLS 和对象权限,必须显式设置为系统模式。
SOQL/SOSL 访问模式:插入 WITH USER_MODE 或 WITH SYSTEM_MODE 子句。用户模式支持多态字段、处理 WHERE 子句中的所有条件、查找所有 FLS 错误,并支持 getInaccessibleFields() 方法。
// SOQL 用户模式
List<Account> acc = [SELECT Id FROM Account WITH USER_MODE];
// DML 用户模式——使用 as user/as system 关键字
Account acc = new Account(Name='test');
insert as user acc;
// Database 方法——使用 AccessLevel 参数
Database.insert(acc, AccessLevel.USER_MODE);
Database.query('SELECT Id FROM Account', AccessLevel.USER_MODE);
权限集提升(Developer Preview):AccessLevel.withPermissionSetId(permissionSetId) 在用户模式操作中附加指定权限集的权限。
stripInaccessible 方法与 Describe 安全方法
Security.stripInaccessible():通过剥离用户无法访问的字段和关系字段来强制执行字段级和对象级数据保护。实现优雅降级——省略不可访问字段而非直接报错。ID 字段永远不会被剥离。检查基于当前用户对指定操作(create/read/update/upsert)的字段级权限。
// 从查询结果中剥离不可访问字段
SObjectAccessDecision securityDecision = Security.stripInaccessible(
AccessType.READABLE,
[SELECT Name, BudgetedCost, ActualCost FROM Campaign]);
// 检查被移除的字段
if (securityDecision.getRemovedFields().get('Campaign').contains('ActualCost')) {
for (Campaign c : securityDecision.getRecords()) { /* BudgetedCost only */ }
}
// DML 前剥离——避免异常
SObjectAccessDecision securityDecision = Security.stripInaccessible(
AccessType.CREATABLE, newAccounts);
insert securityDecision.getRecords();
// 从子查询中剥离
SObjectAccessDecision decision = Security.stripInaccessible(
AccessType.READABLE, accountsWithContacts);
// 反序列化后清理不可信来源数据
List<Account> accounts = (List<Account>)JSON.deserializeStrict(jsonInput, List<Account>.class);
SObjectAccessDecision sd = Security.stripInaccessible(AccessType.UPDATABLE, accounts);
update sd.getRecords();
Describe 安全方法——最细粒度控制:使用 Schema.DescribeSObjectResult 和 Schema.DescribeFieldResult 方法检查当前用户的读写创建删除权限:
// 检查字段级更新权限
if (Schema.sObjectType.Contact.fields.Email.isUpdateable()) { /* 更新 */ }
// 检查字段级创建权限
if (Schema.sObjectType.Contact.fields.Email.isCreateable()) { /* 创建 */ }
// 检查字段级读取权限
if (Schema.sObjectType.Contact.fields.Email.isAccessible()) { /* 查询 */ }
// 检查对象级删除权限
if (Schema.sObjectType.Contact.isDeletable()) { /* 删除 */ }
类安全与 Apex 托管共享
类安全:基于用户配置文件或权限集指定哪些用户可以执行顶级类或入口点中的方法。只能对 Apex 类(而非触发器)设置安全。适用于 Aura 控制器、@AuraEnabled 方法、Visualforce 控制器、Apex REST 服务、异步 Apex。用户可通过有权限的类间接访问无权访问的类,但不能直接调用。
共享 = 授予对记录执行操作的权限。Salesforce 提供三种共享类型:
- 托管共享:基于记录所有权、角色层次结构和共享规则——由平台自动管理
- 用户托管共享(手动共享):记录所有者或 Full Access 用户将记录共享给其他用户/组——所有者变更时自动移除
- Apex 托管共享:通过 Apex 或 SOAP API 程序化实现共享——只有 Modify All Data 权限的用户才能添加或更改。记录所有者变更时保持有效
共享原因(rowCause):自定义对象上的 Reason 字段指定共享类型。托管共享包括 ImplicitChild/ImplicitParent/Owner/Team/Rule/TerritoryRule。手动共享为 Manual/TerritoryManual。Apex 托管共享由开发者定义。
| 访问级别 | API 名称 | 说明 |
|---|---|---|
| Private | None | 仅所有者和角色上级——仅 AccountShare |
| Read Only | Read | 只能查看 |
| Read/Write | Edit | 可查看和编辑 |
| Full Access | All | 可查看、编辑、转移、共享和删除——仅托管共享(内部值不可授予) |
Apex 托管共享:实现
Share 对象。每个支持共享的对象都有对应的 Share 对象:AccountShare、ContactShare、MyCustomObject__Share。主-详细信息关系中的详细对象没有关联 Share 对象——由主对象的共享和关系设置决定。
每个 Share 对象属性:ObjectAccessLevel(必须高于组织的默认访问级别)、ParentID(不可更新)、RowCause(共享原因,不可更新)、UserOrGroupId(用户或公共组/共享组/区域组 ID,不可更新)。
// 创建 Apex 托管共享——为自定义对象 Job__c 创建
Job__Share jobShr = new Job__Share();
jobShr.ParentId = jobId; // 共享的记录
jobShr.UserOrGroupId = userId; // 共享给谁
jobShr.AccessLevel = 'Edit'; // 访问级别
jobShr.RowCause = Schema.Job__Share.RowCause.Review_Reason__c; // Apex 共享原因
insert jobShr;
Apex 托管共享的关键规则:需要 Modify All Data 权限或共享对象的 Modify All。Apex 共享原因必须在对象上定义。同一个用户/组与同一记录的共享只能有一个共享记录(rowCause 必须唯一)。共享记录所有者变更时不会被删除(与手动共享不同)。
安全提示:XSS、CSRF 与注入防护
XSS(跨站脚本)防护:对输出到 HTML 的所有用户输入使用 JSENCODE()、HTMLENCODE() 或 URLENCODE() 函数。在 Visualforce 页面中使用 escape="true" 属性。
CSRF(跨站请求伪造)防护:Visualforce 表单自动包含反 CSRF token。对于自定义 JavaScript 调用,手动添加 {!$Site.csrfToken}。
SOQL/SOSL 注入防护:永远不要直接将用户输入拼接到查询字符串中——使用 String.escapeSingleQuotes() 转义单引号,或使用绑定变量(:variable)将用户输入与查询逻辑分离。静态查询配合绑定变量是最安全的方式。
SOQL 注入防护与自定义设置
SOQL 注入示例与防护:
// 危险——用户输入直接拼接到查询
String query = 'SELECT Id FROM Account WHERE Name LIKE \'%' + userInput + '%\'';
// 安全——使用 escapeSingleQuotes
String safeInput = String.escapeSingleQuotes(userInput);
String safeQuery = 'SELECT Id FROM Account WHERE Name LIKE \'%' + safeInput + '%\'';
// 最安全——使用静态查询加绑定变量(完全避免拼接)
List<Account> results = [SELECT Id FROM Account WHERE Name LIKE :('%' + userInput + '%')];
自定义设置(Custom Settings):类似自定义对象,用于存储应用配置数据。所有数据暴露在应用缓存中,无需重复查询数据库即可高效访问。公式字段、验证规则、Flow、Apex 和 API 都可以使用这些数据。List 类型(数据在组织级别定义)和 Hierarchy 类型(基于用户或配置文件)。
Apex 安全是一个多层次体系——从共享规则(记录级)到对象/字段权限,再到注入防护和类访问控制。理解和正确应用这些机制是构建安全 Salesforce 应用的基础。