salesforce零基础学习(一百四十五)Apex API 66以后 运行为 User Mode

背景

此前我们的经验中,apex代码一直运行在system mode中,但是当summer'26 release(即apex api version: 67)以后,apex将运行在 user mode,所以以后创建apex class的时候,也需要严格的检查当前系统设置默认生成的api version。否则程序中会出现到意想不到的结果。

此前我们的博客解释过 System Mode以及User Mode,这里一句话进行总结:

User Mode(用户模式): 按当前运行用户的权限执行,会检查对象权限(CRUD)、字段安全(FLS)以及共享规则(sharing);

System Mode(系统模式):不检查当前用户的对象权限以及FLS,相当于用户拥有所有对象和字段的访问权。记录级共享是否生效,要看apex class声明的sharing关键字。

那么 api version 66及以前和以后都有哪些明显的区别呢,我们按照以下的demo可以更好的了解:

测试举例, 由AI生成

**1.**创建一个自定义表,包含两个字段,其中一个有FLS,一个没有FLS

  1. AccessModeDemoController,主要实现三个功能:
  • 展示当前用户信息权限
  • 通过前端按钮进行不同的路由,执行 system mode和 user mode
  • 对查询所有的场景进行测试
复制代码
/**
 * Apex 用户模式 / 系统模式对比演示(LWC 控制器)。
 *
 * 外层类声明为 with sharing;需要绕过记录共享的场景放在内部 without sharing 类 Elevated 中,
 * 以便同时演示:访问模式(CRUD/FLS)与 sharing 关键字(记录级)是两套独立的机制。
 */
public with sharing class AccessModeDemoController {

    private static final String NOT_RETURNED = '— 未返回 —';
    private static final String DEMO_FIELDS = 'Id, Name, Public_Note__c, Secret_Note__c, Owner.Name';
    private static final List<String> DEMO_PATHS = new List<String>{ 'Name', 'Public_Note__c', 'Secret_Note__c', 'Owner.Name' };
    private static final List<String> DEMO_PUBLIC_PATHS = new List<String>{ 'Name', 'Public_Note__c', 'Owner.Name' };

    // ---------------------------------------------------------------- DTO

    public class ScenarioResult {
        @AuraEnabled public String key;
        @AuraEnabled public String category;
        @AuraEnabled public String title;
        @AuraEnabled public String mode;
        @AuraEnabled public String sharing;
        @AuraEnabled public String codeSnippet;
        @AuraEnabled public String explanation;
        @AuraEnabled public Boolean success = false;
        @AuraEnabled public Integer rowCount;
        @AuraEnabled public List<Map<String, String>> columns = new List<Map<String, String>>();
        @AuraEnabled public List<Map<String, Object>> rows = new List<Map<String, Object>>();
        @AuraEnabled public String errorType;
        @AuraEnabled public String errorMessage;
        @AuraEnabled public List<String> problemFields = new List<String>();
    }

    public class UserContext {
        @AuraEnabled public String userId;
        @AuraEnabled public String userName;
        @AuraEnabled public String loginName;
        @AuraEnabled public String profileName;
        @AuraEnabled public Boolean modifyAllData;
        @AuraEnabled public Boolean canCreateDemo;
        @AuraEnabled public Boolean canReadSecret;
        @AuraEnabled public Integer totalDemoRecords;
        @AuraEnabled public Integer ownDemoRecords;
    }

    // ---------------------------------------------------------------- 内部 without sharing 类

    private without sharing class Elevated {
        List<Access_Mode_Demo__c> querySystemMode() {
            return [SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name
                    FROM Access_Mode_Demo__c WITH SYSTEM_MODE ORDER BY CreatedDate];
        }
        Integer countAll() {
            return Database.countQuery('SELECT COUNT() FROM Access_Mode_Demo__c', AccessLevel.SYSTEM_MODE);
        }
        void insertAsSystem(Access_Mode_Demo__c rec) {
            insert as system rec;
        }
    }

    // ---------------------------------------------------------------- 当前用户上下文

    @AuraEnabled
    public static UserContext getUserContext() {
        Id uid = UserInfo.getUserId();
        UserContext ctx = new UserContext();
        ctx.userId = uid;
        ctx.userName = UserInfo.getName();
        ctx.loginName = UserInfo.getUserName();
        User me = [SELECT Profile.Name FROM User WHERE Id = :uid WITH SYSTEM_MODE];
        ctx.profileName = me.Profile?.Name;
        ctx.modifyAllData = [SELECT COUNT() FROM PermissionSetAssignment
                             WHERE AssigneeId = :uid AND PermissionSet.PermissionsModifyAllData = true
                             WITH SYSTEM_MODE] > 0;
        ctx.canCreateDemo = Schema.SObjectType.Access_Mode_Demo__c.isCreateable();
        ctx.canReadSecret = Schema.SObjectType.Access_Mode_Demo__c.fields.Secret_Note__c.isAccessible();
        ctx.totalDemoRecords = new Elevated().countAll();
        ctx.ownDemoRecords = [SELECT COUNT() FROM Access_Mode_Demo__c WHERE OwnerId = :uid WITH SYSTEM_MODE];
        return ctx;
    }

    // ---------------------------------------------------------------- 查询对比场景

    @AuraEnabled
    public static List<ScenarioResult> runAllScenarios() {
        List<ScenarioResult> results = new List<ScenarioResult>();

        // A. 未声明访问模式 · 类 API 66.0
        ScenarioResult a = newResult('A', '默认模式', '未声明访问模式(类 API 66.0)', '默认(系统)', 'without sharing',
            '// AccessModeDefaultV66.cls  (apiVersion 66.0)\n[SELECT ' + DEMO_FIELDS + ' FROM Access_Mode_Demo__c]',
            'API 66.0 及以前默认系统模式:不检查当前用户的对象权限和 FLS;without sharing 也不按共享过滤。');
        try {
            fill(a, AccessModeDefaultV66.queryWithoutModeClause(), DEMO_PATHS);
        } catch (Exception e) { fail(a, e); }
        results.add(a);

        // B. 未声明访问模式 · 类 API 68.0
        ScenarioResult b = newResult('B', '默认模式', '未声明访问模式(类 API 68.0)', '默认(用户)', 'without sharing',
            '// AccessModeDefaultV68.cls  (apiVersion 68.0)\n[SELECT ' + DEMO_FIELDS + ' FROM Access_Mode_Demo__c]',
            'API 67.0 及以后默认用户模式:同样的代码,仅因类的 API 版本不同,就会检查 CRUD、FLS 和共享。');
        try {
            fill(b, AccessModeDefaultV68.queryWithoutModeClause(), DEMO_PATHS);
        } catch (Exception e) { fail(b, e); }
        results.add(b);

        // C. WITH USER_MODE(包含受限字段)
        ScenarioResult c = newResult('C', '显式声明', 'WITH USER_MODE(包含 Secret_Note__c)', '用户模式', 'with sharing',
            '[SELECT ' + DEMO_FIELDS + ' FROM Access_Mode_Demo__c WITH USER_MODE]',
            '用户模式检查 FLS:用户没有 Secret_Note__c 读取权限时整条查询抛 QueryException,可用 getInaccessibleFields() 拿到全部无权限字段。');
        try {
            fill(c, [SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name
                     FROM Access_Mode_Demo__c WITH USER_MODE ORDER BY CreatedDate], DEMO_PATHS);
        } catch (Exception e) { fail(c, e); }
        results.add(c);

        // D. WITH USER_MODE(仅有权限字段)
        ScenarioResult d = newResult('D', '显式声明', 'WITH USER_MODE(仅查询有权限字段)', '用户模式', 'with sharing',
            '[SELECT Id, Name, Public_Note__c, Owner.Name FROM Access_Mode_Demo__c WITH USER_MODE]',
            '用户模式始终遵守共享规则:OWD 为 Private 时,普通用户只能看到自己拥有或被共享的记录。');
        try {
            fill(d, [SELECT Id, Name, Public_Note__c, Owner.Name
                     FROM Access_Mode_Demo__c WITH USER_MODE ORDER BY CreatedDate], DEMO_PUBLIC_PATHS);
        } catch (Exception e) { fail(d, e); }
        results.add(d);

        // E. WITH SYSTEM_MODE + without sharing
        ScenarioResult e1 = newResult('E', '显式声明', 'WITH SYSTEM_MODE + without sharing', '系统模式', 'without sharing',
            'private without sharing class Elevated {\n  [SELECT ' + DEMO_FIELDS + ' FROM Access_Mode_Demo__c WITH SYSTEM_MODE]\n}',
            '系统模式不检查 CRUD/FLS;类为 without sharing,因此也不按共享过滤 —— 能看到全部记录和 Secret_Note__c。');
        try {
            fill(e1, new Elevated().querySystemMode(), DEMO_PATHS);
        } catch (Exception e) { fail(e1, e); }
        results.add(e1);

        // F. WITH SYSTEM_MODE + with sharing
        ScenarioResult f = newResult('F', '显式声明', 'WITH SYSTEM_MODE + with sharing', '系统模式', 'with sharing',
            'public with sharing class AccessModeDemoController {\n  [SELECT ' + DEMO_FIELDS + ' FROM Access_Mode_Demo__c WITH SYSTEM_MODE]\n}',
            '系统模式下记录可见性由类的 sharing 关键字决定:with sharing 仍按共享过滤行,但 FLS 不检查,Secret_Note__c 照样返回。');
        try {
            fill(f, [SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name
                     FROM Access_Mode_Demo__c WITH SYSTEM_MODE ORDER BY CreatedDate], DEMO_PATHS);
        } catch (Exception e) { fail(f, e); }
        results.add(f);

        // G. Security.stripInaccessible
        ScenarioResult g = newResult('G', '优雅降级', 'Security.stripInaccessible(READABLE)', '系统模式查询 + 剥离', 'with sharing',
            'List<Access_Mode_Demo__c> recs = [SELECT ... FROM Access_Mode_Demo__c WITH SYSTEM_MODE];\n'
                + 'SObjectAccessDecision dec = Security.stripInaccessible(AccessType.READABLE, recs);\n'
                + 'dec.getRecords(); dec.getRemovedFields();',
            '不抛异常,而是把当前用户无权读取的字段从结果里移除;getRemovedFields() 返回被移除的字段。');
        try {
            List<Access_Mode_Demo__c> raw = [SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name
                                             FROM Access_Mode_Demo__c WITH SYSTEM_MODE ORDER BY CreatedDate];
            SObjectAccessDecision dec = Security.stripInaccessible(AccessType.READABLE, raw);
            fill(g, dec.getRecords(), DEMO_PATHS);
            for (String obj : dec.getRemovedFields().keySet()) {
                for (String fld : dec.getRemovedFields().get(obj)) {
                    g.problemFields.add(obj + '.' + fld);
                }
            }
        } catch (Exception e) { fail(g, e); }
        results.add(g);

        return results;
    }

    // ---------------------------------------------------------------- DML 对比场景

    @AuraEnabled
    public static ScenarioResult createRecord(String name, String publicNote, String secretNote, String mode) {
        Boolean asUser = (mode == 'USER');
        ScenarioResult r = newResult(asUser ? 'DML_USER' : 'DML_SYSTEM', 'DML',
            asUser ? 'insert as user' : 'insert as system',
            asUser ? '用户模式' : '系统模式', asUser ? 'with sharing' : 'without sharing',
            asUser ? 'insert as user rec;' : 'insert as system rec;  // 在 without sharing 内部类中执行',
            asUser
                ? '用户模式 DML 检查对象“新建”权限与字段编辑 FLS。本 demo 实测:缺少对象新建权限时抛 System.TypeException(DML operation INSERT not allowed);官方文档说明 FLS 错误可通过 DmlException.getDmlFieldNames() 获取。'
                : '系统模式 DML 不检查当前用户的 CRUD/FLS,即使用户没有“新建”权限也能写入。');
        Access_Mode_Demo__c rec = new Access_Mode_Demo__c(
            Name = String.isBlank(name) ? 'Demo ' + System.now().format('HH:mm:ss') : name,
            Public_Note__c = publicNote,
            Secret_Note__c = secretNote);
        try {
            if (asUser) {
                insert as user rec;
            } else {
                new Elevated().insertAsSystem(rec);
            }
            r.success = true;
            r.rowCount = 1;
            Access_Mode_Demo__c saved = [SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name
                                         FROM Access_Mode_Demo__c WHERE Id = :rec.Id WITH SYSTEM_MODE];
            fill(r, new List<SObject>{ saved }, DEMO_PATHS);
        } catch (Exception e) {
            fail(r, e);
        }
        return r;
    }

    // ---------------------------------------------------------------- helpers

    private static ScenarioResult newResult(String key, String grp, String title, String mode, String sharing,
                                            String code, String explanation) {
        ScenarioResult r = new ScenarioResult();
        r.key = key;
        r.category = grp;
        r.title = title;
        r.mode = mode;
        r.sharing = sharing;
        r.codeSnippet = code;
        r.explanation = explanation;
        return r;
    }

    private static void fill(ScenarioResult r, List<SObject> recs, List<String> paths) {
        r.success = true;
        r.rowCount = recs.size();
        for (String p : paths) {
            r.columns.add(new Map<String, String>{ 'label' => p, 'fieldName' => p.replace('.', '_') });
        }
        for (SObject rec : recs) {
            Map<String, Object> row = new Map<String, Object>{ 'Id' => rec.Id };
            for (String p : paths) {
                row.put(p.replace('.', '_'), readPath(rec, p));
            }
            r.rows.add(row);
        }
    }

    private static void fail(ScenarioResult r, Exception e) {
        r.success = false;
        r.errorType = e.getTypeName();
        r.errorMessage = e.getMessage();
        if (e instanceof QueryException) {
            Map<String, Set<String>> inaccessible = ((QueryException) e).getInaccessibleFields();
            if (inaccessible != null) {
                for (String obj : inaccessible.keySet()) {
                    for (String fld : inaccessible.get(obj)) {
                        r.problemFields.add(obj + '.' + fld);
                    }
                }
            }
        } else if (e instanceof DmlException) {
            DmlException de = (DmlException) e;
            for (Integer i = 0; i < de.getNumDml(); i++) {
                List<String> names = de.getDmlFieldNames(i);
                if (names != null) {
                    r.problemFields.addAll(names);
                }
            }
        }
    }

    private static Object readPath(SObject rec, String path) {
        Map<String, Object> populated = rec.getPopulatedFieldsAsMap();
        if (!path.contains('.')) {
            if (!populated.containsKey(path)) {
                return NOT_RETURNED;
            }
            Object v = populated.get(path);
            return v == null ? '(空值)' : String.valueOf(v);
        }
        String rel = path.substringBefore('.');
        if (!populated.containsKey(rel) || populated.get(rel) == null) {
            return NOT_RETURNED;
        }
        return readPath(rec.getSObject(rel), path.substringAfter('.'));
    }
}
  1. AccessModeDefaultV66: 设置api version为 66
复制代码
public without sharing class AccessModeDefaultV66 {
    public static List<Access_Mode_Demo__c> queryWithoutModeClause() {
        return [SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name
                FROM Access_Mode_Demo__c ORDER BY CreatedDate];
    }
}
  1. AccessModeDefaultV68: 代码和V66相同,唯一区别是设置api version 为68
复制代码
public without sharing class AccessModeDefaultV68 {
    public static List<Access_Mode_Demo__c> queryWithoutModeClause() {
        return [SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name
                FROM Access_Mode_Demo__c ORDER BY CreatedDate];
    }
}
  1. AccessModeDemo.html
复制代码
<template>
    <div class="slds-p-around_small">
        <!-- 当前用户 -->
        <lightning-card title="Apex 用户模式 vs 系统模式 演示" icon-name="standard:user_role">
            <div class="slds-p-horizontal_medium">
                <template lwc:if={ctx}>
                    <div class="slds-grid slds-wrap slds-gutters_x-small">
                        <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3">
                            <p class="label">当前运行用户</p>
                            <p class="value">{ctx.userName}</p>
                            <p class="sub">{ctx.loginName}</p>
                        </div>
                        <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3">
                            <p class="label">Profile</p>
                            <p class="value">{ctx.profileName}</p>
                        </div>
                        <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3">
                            <p class="label">演示记录(总数 / 本人拥有)</p>
                            <p class="value">{ctx.totalDemoRecords} / {ctx.ownDemoRecords}</p>
                        </div>
                    </div>
                    <div class="slds-m-top_small badges">
                        <span class={modifyAllClass}>Modify All Data:{modifyAllText}</span>
                        <span class={canCreateClass}>Access_Mode_Demo__c 新建:{canCreateText}</span>
                        <span class={canReadSecretClass}>Secret_Note__c 读取 FLS:{canReadSecretText}</span>
                    </div>
                </template>
                <template lwc:if={error}>
                    <div class="slds-text-color_error slds-m-top_small">{error}</div>
                </template>
            </div>
        </lightning-card>

        <!-- DML 对比 -->
        <lightning-card title="① DML 对比:insert as user / insert as system" icon-name="standard:record_create">
            <div class="slds-p-horizontal_medium">
                <div class="slds-grid slds-wrap slds-gutters_x-small">
                    <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3">
                        <lightning-input label="Name" value={newName} data-field="newName" onchange={handleInput}></lightning-input>
                    </div>
                    <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3">
                        <lightning-input label="Public Note" value={newPublic} data-field="newPublic" onchange={handleInput}></lightning-input>
                    </div>
                    <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-3">
                        <lightning-input label="Secret Note(受限字段)" value={newSecret} data-field="newSecret" onchange={handleInput}></lightning-input>
                    </div>
                </div>
                <div class="slds-m-top_small">
                    <lightning-button label="用户模式新建(insert as user)" data-mode="USER" onclick={handleCreate} disabled={busy}></lightning-button>
                    <lightning-button class="slds-m-left_x-small" variant="brand" label="系统模式新建(insert as system)" data-mode="SYSTEM" onclick={handleCreate} disabled={busy}></lightning-button>
                </div>
                <template lwc:if={dmlResult}>
                    <div class="slds-m-top_small">
                        <c-access-mode-demo-scenario scenario={dmlResult}></c-access-mode-demo-scenario>
                    </div>
                </template>
            </div>
        </lightning-card>

        <!-- 查询对比 -->
        <lightning-card title="② 查询对比场景" icon-name="standard:sossession">
            <lightning-button slot="actions" variant="brand" label="运行全部对比场景" onclick={handleRunAll} disabled={busy}></lightning-button>
            <div class="slds-p-horizontal_medium">
                <template lwc:if={busy}>
                    <lightning-spinner alternative-text="运行中" size="small"></lightning-spinner>
                </template>
                <template lwc:if={hasResults}>
                    <p class="slds-text-body_small slds-m-bottom_x-small">运行时间:{lastRunAt}</p>
                    <table class="slds-table slds-table_cell-buffer slds-table_bordered summary">
                        <thead>
                            <tr class="slds-line-height_reset">
                                <th>#</th>
                                <th>场景</th>
                                <th>访问模式</th>
                                <th>sharing</th>
                                <th>结果</th>
                                <th>问题字段 / 被剥离字段</th>
                            </tr>
                        </thead>
                        <tbody>
                            <template for:each={summaryRows} for:item="s">
                                <tr key={s.key}>
                                    <td>{s.key}</td>
                                    <td>{s.title}</td>
                                    <td>{s.mode}</td>
                                    <td>{s.sharing}</td>
                                    <td><span class={s.statusClass}>{s.statusText}</span></td>
                                    <td class="mono">{s.problemText}</td>
                                </tr>
                            </template>
                        </tbody>
                    </table>
                    <template for:each={results} for:item="r">
                        <div key={r.key} class="slds-m-top_medium">
                            <c-access-mode-demo-scenario scenario={r}></c-access-mode-demo-scenario>
                        </div>
                    </template>
                </template>
                <template lwc:else>
                    <p class="slds-text-color_weak">点击右上角“运行全部对比场景”。</p>
                </template>
            </div>
        </lightning-card>
    </div>
</template>

AccessModeDemo.js

复制代码
import { LightningElement } from 'lwc';
import getUserContext from '@salesforce/apex/AccessModeDemoController.getUserContext';
import runAllScenarios from '@salesforce/apex/AccessModeDemoController.runAllScenarios';
import createRecord from '@salesforce/apex/AccessModeDemoController.createRecord';

const yesNo = (v) => (v ? '有' : '无');
const badge = (v) => `slds-badge ${v ? 'slds-theme_success' : 'slds-theme_error'}`;

export default class AccessModeDemo extends LightningElement {
    ctx;
    error;
    busy = false;
    results = [];
    dmlResult;
    lastRunAt;
    newName = '';
    newPublic = '公开备注';
    newSecret = '机密备注';

    connectedCallback() {
        this.loadContext();
    }

    async loadContext() {
        try {
            this.ctx = await getUserContext();
        } catch (e) {
            this.error = this.reduceError(e);
        }
    }

    get modifyAllText() { return yesNo(this.ctx?.modifyAllData); }
    get modifyAllClass() { return badge(this.ctx?.modifyAllData); }
    get canCreateText() { return yesNo(this.ctx?.canCreateDemo); }
    get canCreateClass() { return badge(this.ctx?.canCreateDemo); }
    get canReadSecretText() { return yesNo(this.ctx?.canReadSecret); }
    get canReadSecretClass() { return badge(this.ctx?.canReadSecret); }

    get hasResults() {
        return this.results && this.results.length > 0;
    }

    get summaryRows() {
        return this.results.map((r) => ({
            key: r.key,
            title: r.title,
            mode: r.mode,
            sharing: r.sharing,
            statusText: r.success ? `成功 · ${r.rowCount} 行` : `失败 · ${r.errorType}`,
            statusClass: badge(r.success),
            problemText: (r.problemFields || []).join(', ')
        }));
    }

    handleInput(event) {
        this[event.target.dataset.field] = event.target.value;
    }

    async handleCreate(event) {
        const mode = event.target.dataset.mode;
        this.busy = true;
        this.error = undefined;
        try {
            this.dmlResult = await createRecord({
                name: this.newName,
                publicNote: this.newPublic,
                secretNote: this.newSecret,
                mode
            });
            await this.loadContext();
        } catch (e) {
            this.error = this.reduceError(e);
        } finally {
            this.busy = false;
        }
    }

    async handleRunAll() {
        this.busy = true;
        this.error = undefined;
        try {
            this.results = await runAllScenarios();
            this.lastRunAt = new Date().toLocaleString();
        } catch (e) {
            this.error = this.reduceError(e);
        } finally {
            this.busy = false;
        }
    }

    reduceError(e) {
        return e?.body?.message || e?.message || JSON.stringify(e);
    }
}
  1. 创建普通用户:没有 Access_Mode_Demo的创建权限,没有Secret_Note__c的读取权限。

测试结果展示:

一. DML 场景

  1. 点击用户模式新建,因为走 User Mode,所以报错:DML operation INSERT not allowed on Access_Mode_Demo__c。

后台的DML主要执行insert as user rec;//rec为这条Access_Mode_Demo的记录

  1. 添加permission set,允许 Access_Mode_Demo__c新建权限,点击用户模式新建,因为走 User Mode,所以报错:System.DmlException:Operation failed due to fields being inaccessible on Sobject Access_Mode_Demo__c, check errors on Exception or Result!

  2. 点击系统模式新建,因为走 System Mode,所以创建成功。

后台的DML主要执行insert as system rec;//rec为这条Access_Mode_Demo的记录

二. 查询场景

查询场景主要包含了以下的组合。通过下图的排列组合可以看到,当在api version是 68.0时,由于走 User Mode, 当SOQL中对某个字段没有查询权限时,会报错。System.QueryException,

|------------------------------------------------|------------------|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------|------------------------------------|
| 场景 | 访问模式 | sharing | SOQL | 结果 | 问题字段 |
| 未声明访问模式(类 API 66.0) | 系统模式 | Without Sharing | SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name FROM Access_Mode_Demo__c ORDER BY CreatedDate | 成功 | |
| 未声明访问模式(类 API 68.0) | 用户模式 | Without Sharing | SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name FROM Access_Mode_Demo__c ORDER BY CreatedDate | 失败 · System.QueryException | Access_Mode_Demo__c.Secret_Note__c |
| WITH USER_MODE(包含 Secret_Note__c;类 API 68.0) | 用户模式 | With Sharing | SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name FROM Access_Mode_Demo__c WITH USER_MODE ORDER BY CreatedDate | 失败 · System.QueryException | Access_Mode_Demo__c.Secret_Note__c |
| WITH USER_MODE(仅查询有权限字段;类 API 68.0) | 用户模式 | With Sharing | SELECT Id, Name, Public_Note__c, Owner.Name FROM Access_Mode_Demo__c WITH USER_MODE ORDER BY CreatedDate | 成功 | |
| WITH SYSTEM_MODE + without sharing(类 API 68.0) | 系统模式 | Without Sharing | SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name FROM Access_Mode_Demo__c WITH SYSTEM_MODE ORDER BY CreatedDate | 成功 | |
| WITH SYSTEM_MODE + with sharing(类 API 68.0) | 系统模式 | With Sharing | SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name FROM Access_Mode_Demo__c WITH SYSTEM_MODE ORDER BY CreatedDate | 成功 | |
| Security.stripInaccessible(类 API 68.0) | 系统模式查询 + 无权限字段移除 | With Sharing | List<Access_Mode_Demo__c> raw = SELECT Id, Name, Public_Note__c, Secret_Note__c, Owner.Name FROM Access_Mode_Demo__c WITH SYSTEM_MODE ORDER BY CreatedDate; SObjectAccessDecision dec = Security.stripInaccessible(AccessType.READABLE, raw); | 成功 | 结果集中,返回的结果将不会包含Secret_Note__c |

总结:

AI时代,Salesforce对于安全性看得越来越重要,所以最近的release增强了很多的安全的更新。 如果你的项目中的代码对于权限的安全性要求很高,你可以考虑user mode或者使用 system mode 搭配stripInaccessible方法实现基于用户级别的权限访问和操作,把数据操作控制在最小范围。后续创建apex前,也思考一下你的项目架构中适用于system mode还是 user mode运行,不同的场景使用不同的api version(66及以前 / 66以后)。

参考文档:

https://developer.salesforce.com/docs/platform/apex-dev/references/apex-dev/apex-classes-enforce-usermode.html

https://developer.salesforce.com/docs/platform/apex-dev/references/apex-dev/apex-classes-with-security-stripinaccessible.html

相关推荐
zero.zhang1 年前
salesforce零基础学习(一百四十四)External Client App浅谈
salesforce学习·integration
zero.zhang2 年前
salesforce零基础学习(一百四十三)零碎知识点小总结(十一)
salesforce学习·admin学习
zero.zhang2 年前
salesforce零基础学习(一百四十二)在Formula字段中如何通过Datetime字段显示Local Time(适配DST)
salesforce学习
zero.zhang2 年前
salesforce零基础学习(一百四十一)刷新dev sandbox需要强制group
salesforce学习
zero.zhang2 年前
Salesforce AI Specialist篇之 Prompt Builder
salesforce学习·self learning·ai 学习
zero.zhang2 年前
Salesforce AI Specialist篇之 Einstein Trust Layer
salesforce学习·ai 学习
小小码虫2 年前
如何在页面调用utility bar并传递参数至lwc组件
javascript·ecmascript·salesforce·lwc
zero.zhang2 年前
Salesforce Sales Cloud 零基础学习(五) My Labels的使用
salesforce学习·admin学习·sales cloud
zero.zhang2 年前
salesforce零基础学习(一百四十)Record Type在实施过程中的考虑
salesforce学习·self learning·admin学习