开发安全站点:经过身份验证的用户和来宾用户

实施可通过外部和未经身份验证访问的 Experience Cloud 站点时 来宾用户,请牢记这些安全注意事项。外部用户具有登录权限 您的 Experience Cloud 站点,但他们无法访问您的内部 Salesforce 组织。来宾用户是 互联网上任何可以访问您的可公开访问的页面和组件的人 Experience Cloud 站点。

  • 限制声明性访问
    授予查看对象的权限允许外部用户使用标准控制器查看该对象。启用了 Lightning 功能的 Experience Builder 站点和 Salesforce 选项卡 + Visualforce 站点中提供了标准控制器。这些控制器仅根据平台声明性权限授予访问权限。
  • 确定安全模型 对于每个用例,确定是实现自定义访问控制模型还是依赖声明性平台访问控制模型
    。建议尽可能使用平台声明性访问控制模型。但是,有时您的要求需要自定义访问控制模型。
  • 限制对 Apex 类的访问 仅允许来宾和外部用户访问他们必须调用的那些类
  • 安全性
    如果来宾或外部用户必须运行流,请覆盖流权限以仅授予对特定外部用户配置文件、权限集或站点来宾用户配置文件的访问权限,而不是允许用户运行所有流。尽可能避免在系统上下文中运行流,并限制对子流的访问。否则,请确保对这些流和子流实施过程访问控制。
  • SOQL 注入:
    清理传递到动态 SOQL 查询的用户控制数据。

限制声明性访问

授予查看对象的权限允许外部用户使用 标准控制器。Experience Builder 站点中提供了标准控制器,并且 启用了 Lightning 功能的 Salesforce 选项卡 + Visualforce 站点。这些控制器 仅基于平台声明性权限授予访问权限。

授予声明性访问权限,以仅创建、查看、修改或删除其 允许外部用户通过控制器进行访问,而无需中介。Salesforce的 平台包括可用于创建、读取、更新或删除的标准控制器 数据。标准 UI 控制器强制执行 平台的共享规则,在创建、读取、更新和删除 (CRUD) 权限中,以及 字段级安全性 (FLS)。如果向外部用户授予查看或更新 对象,他们能够执行操作。不要向任何对象授予过多的权限 如果您不希望行使这些权限。

未经身份验证的访客用户准则

请考虑以下有关记录 ID 加密和提供不同级别的准则 在选择声明性或自定义访问权限之前,对未经身份验证的来宾用户进行访问 控制模型。

  • 加密来宾用户
    的记录 ID 出于安全原因,除非您希望记录是公开的,否则不允许来宾用户按记录 ID 查找记录。当前宾用户创建记录并希望稍后访问该记录时,请创建一个加密字符串,该字符串使用记录 ID、记录创建时间戳和当前时间戳的组合。加密的字符串充当记录的唯一标识符,只有记录创建者才拥有。稍后,处理请求的 Apex 代码要求来宾用户提交加密字符串。该 Apex 代码解密字符串以获取记录 ID 和其他记录标识符,并检索或更新请求的记录。
  • 授予来宾用户读取记录的访问权限 当允许来宾用户访问读取记录
    数据时,将向公众公开数据。查看我们的指南,并设计实现,以允许来宾用户进行必要的访问,而不会影响你的数据。
  • 授予来宾用户创建记录的访问权限 因此,来宾用户可以创建对象记录
    ,请配置来宾用户配置文件以包括对所需对象的创建访问权限。
  • 授予来宾用户访问更新记录的权限 若要允许来宾用户更新记录
    ,请在系统上下文中执行操作,而不进行共享。在允许用户更新记录之前,最好验证以前提供给用户的加密令牌。若要确保记录正确无误,请验证有关记录的信息,例如其创建者。

加密来宾用户的记录 ID

出于安全原因,不允许来宾用户按记录 ID 查找记录,除非你 希望记录是公开的。当前宾用户创建记录并希望稍后访问该记录时, 创建一个加密字符串,该字符串使用记录 ID、记录创建时间戳、 和当前时间戳。加密字符串充当记录的唯一标识符 只有记录创建者有。稍后,处理请求的 Apex 代码需要 要提交加密字符串的来宾用户。该 Apex 代码解密字符串以获取 记录 ID 和其他记录标识符,并检索或更新请求的 记录。

提示

用户加密解密 AppExchange 包 提供类,该类使用 Apex 库进行加密和 解密,为您存储相关数据,并提供两个模板流。使用托管 package 来实现自定义的记录 ID 加密,或创建类似的 你自己的。UserCryptoHelperSystem.Crypto

授予来宾用户对读取记录的访问权限

当您允许来宾用户访问读取记录数据时,会将您的数据公开给 公共。查看我们的指南,并设计您的实施,以允许对 来宾用户,而不会损害您的数据。

警告

Summer ’20 版本添加了新的设置并更新了访客用户记录访问指南。截至 21 年冬季,将执行 20 年夏季更新中引入的准则。之后 Winter ’21 release,请使用本文档中描述的方法之一,因为之前的方法 方法不再起作用。

每次来宾用户请求读取记录数据时,响应都会在以下模式下运行 确定共享规则是否适用于请求。因为模式具有不同的安全性 顾及以下问题,请考虑数据的敏感性,以确定最适合您的方法 业务需求。您还可以使用单独的流或多个 Apex 类来运行某些请求 在一种模式下,其他请求在另一种模式下。

如何处理敏感信息

在将数据返回给未经身份验证的人之前,从记录中删除所有敏感信息 用户。出于安全原因,请勿使用记录 ID 等可猜测信息进行检索 包含敏感信息的记录。

与共享

与共享一起运行的记录请求无法访问记录,除非共享规则提供 来宾用户对它们的访问权限。如果出现以下情况,请考虑共享只读访问规则 是真的:

  • 您希望公开记录并可供任何人访问。
  • 可以使用共享规则选择目标记录,而不会公开其他记录 记录。

无共享

警告

在不共享的情况下实现请求时,请设计请求和 仔细响应数据,以确保不会无意中暴露组织的敏感数据 数据。

在系统模式下运行而不共享的记录请求执行操作 系统级访问并绕过共享规则。如果你不小心它的行为 执行以及如何执行它们,请求可以在运行时公开或修改记录数据 没有共享。在不共享的情况下运行的查询会将所有选定的记录公开给 公共。如果以下任一情况为真,请考虑不共享的系统模式:

  • 来宾用户需要的不仅仅是对记录的只读访问权限。
  • 您不希望记录是公开的。
  • 如果不公开其他记录,则无法选择记录。
  • 目标记录是父子关系和对子项的访问的一部分 记录受父记录的写入访问权限的限制。因为共享规则不能 向来宾用户授予写入访问权限,在此方案中运行请求而不共享。

用于记录选择的加密记录 ID

如果来宾用户创建了一条记录,并且以后必须访问该记录,请使用 记录创建时间戳,并将加密后的字符串返回给客户端。为客人提供 用户具有包含加密字符串的 URL,以便他们可以避免键入长字符串。 当他们请求对记录的读取访问权限时,请从 URL 中检索加密的字符串。自 选择记录,使用解密的记录 ID。

闪电组件

直接与对象字段链接的 Lightning 组件会自动执行 对象创建、读取、更新和删除 (CRUD) 权限和字段级安全性 (FLS) 检查以确定组件是否为用户显示。对于您不这样做的记录 与来宾用户共享时,CRUD 和 FLS 检查将失败,并且组件不会显示。自 使用 Lightning 组件显示这些记录,将其值设置为变量,以及 在 Apex 代码中将这些变量与对象的字段单独关联。由于此方法适用于自动 CRUD 和 FLS 检查,因此请实现 Lightning 组件遵循以下准则:

  • 在查询中,仅包含所需的记录字段。
  • 不要将敏感字段传递给客户端代码。
  • 仅将客户端所需的字段传递给客户端。发送到客户端的所有数据 是公开的。

代码示例

  • 共享示例代码:向来宾用户授予读取记录的访问权限
  • 不共享的示例代码:授予来宾用户在同一事务中创建和读取记录的权限
  • 不共享的示例代码:授予来宾用户创建记录并在以后读取记录的权限

流样

  • 共享示例流:为来宾用户提供读取记录的访问权限
  • 不共享的示例流:授予来宾用户在一个流中创建和读取记录的权限

另见

  • Salesforce 帮助:保护访客用户的共享设置和记录 访问
  • Salesforce 帮助:共享规则
  • Salesforce 安全指南:共享规则

授予来宾用户创建记录的权限

因此,来宾用户可以创建对象记录,配置来宾用户配置文件以包括 为所需对象创建访问权限。要授予对对象的创建访问权限,必须授予对对象的读取访问权限。如果读取 不需要对该对象进行访问权限,我们建议删除该对象的所有权限,并且 在不共享控制器中运行创建逻辑。

提示

执行数据 对来宾创建的数据进行验证,以确保它不会影响自动化 过程。

记录 ID 和来宾用户

创建记录后,不要在对客户端的响应中包含记录 ID。创建 供以后访问的唯一记录标识符,在创建记录时对记录 ID 进行加密 时间戳,并将加密的字符串返回给客户端。

检索记录时,记录 ID 会自动包含在对象中。删除 对象的记录 ID,不要将其传递给客户端。

在 Apex 方法中创建和访问记录

来宾用户共享规则在事务完成后生效。如果 Apex 代码 共享创建并请求新创建的记录,而来宾用户依赖于 共享规则 若要访问记录,则读取请求失败,因为来宾共享规则 尚未生效。允许来宾用户创建记录并读取新创建的记录 记录,用关键字定义类。without sharing

Flow 中的记录创建和访问

在流程中,“创建记录”元素不会创建记录,直到面试执行 Screen、Local Action 或 Pause 元素。要在同一流中创建和读取相同的记录, 在记录创建和检索之间插入一个屏幕,或在 Apex 操作中读取记录。

样品

  • 示例流:授予来宾用户创建记录的访问权限
  • 不共享的示例流:授予来宾用户在一个流中创建和读取记录的权限
  • 不共享的示例代码:授予来宾用户在同一事务中创建和读取记录的权限

另见

  • Salesforce 帮助:配置访客用户配置文件
  • 加密来宾用户的记录 ID

授予来宾用户访问更新记录的权限

若要允许来宾用户更新记录,请在系统上下文中执行操作,而无需 共享。在允许用户更新记录之前,请先验证加密令牌 作为最佳实践提供给用户。为确保其记录正确无误,请验证 有关记录的信息,例如其创建者。

警告

Summer ’20 版本添加了新的设置并更新了访客用户记录访问指南。截至 21 年冬季,将执行 20 年夏季更新中引入的准则。之后 21 年冬季版本,必须使用无共享模式,因为无法使用共享规则 授予来宾用户对记录的更新访问权限。

Lightning 组件和访客用户

直接与对象字段链接的 Lightning 组件会自动执行 对象权限和字段级安全性 (FLS) 检查以确定组件是否 为用户显示。如果启用了“保护来宾用户记录访问”设置,则无法授予来宾用户更新记录的访问权限。在这种情况下, 需要更新权限的对象权限检查失败,组件不会 显示。要使用 Lightning 组件处理访客用户输入,请使用变量来设置 闪电组件。然后将这些变量与记录的字段分别关联 在 Apex 代码中。因为此方法适用于自动对象权限和 FLS 检查,请按照以下准则实施您的 Lightning 组件:

  • 使用服务器端代码检索记录并验证它是否是所需的记录 在执行更新之前进行更新。
  • 不要将记录字段传递给客户端,除非您希望将其显示给用户。 您发送给客户端的任何数据都是公开的。
  • 不要将记录 ID 传递给客户端。不接受来自客户端的记录 ID。自 为记录创建唯一标识符,将记录 ID 加密为字符串。
  • 使用服务器端逻辑将更新限制为仅对所需字段进行更新。请勿使用 用于确定服务器端行为的客户端代码。
  • 使用服务器端逻辑验证来自客户端的数据,然后代码执行 更新。

声明式访问控制模型示例

这些代码和流示例使用声明性访问控制模型来提供 未经身份验证的来宾用户访问读取记录。

  • 共享示例流:授予来宾用户读取记录
    的访问权限 在此示例流中,来宾用户输入日期范围,然后查看该范围内的事件。来宾用户具有对具有共享规则的记录的读取访问权限,因此来宾用户配置文件确定流可以访问哪些字段。
  • 共享示例代码:授予来宾用户读取记录
    的访问权限 在此代码示例集合中,来宾用户输入日期范围,然后查看该范围内的事件。来宾用户通过共享规则对记录具有读取访问权限。

共享示例流:为来宾用户提供读取记录的访问权限

在此示例流中,来宾用户输入日期范围,然后查看该日期范围内的事件 范围。来宾用户具有对具有共享规则的记录的读取访问权限,因此来宾用户 配置文件确定流可以访问哪些字段。

Important

在向来宾用户授予读取访问权限之前,请参阅授予 来宾用户访问读取记录。

流配置

由于来宾用户可以通过共享规则访问记录,因此请将“如何”设置为“如何” 将“流”设置运行到“用户”或“系统上下文”(Based on Flow 是如何启动的。

输入日期范围 (1)

流程中的第一个元素是显示开始日期和结束日期输入字段的屏幕。 该元素将输入日期保存在变量和 中。Start_DateEnd_Date

获取活动 (2)

下一个元素是“获取记录”查询,该查询选择符合以下条件的事件:

  • 事件大于变量。StartDateTimeStart_Date
  • 事件小于变量。EndDateTimeEnd_Date
  • 事件的值为 。isPrivateFalse
  • 事件的值为 。isArchivedFalse

该元素将所选事件保存在变量中。GetEvents

循环唱片 (3)

Loop 元素循环遍历变量中的每个事件。GetEvents

在循环中,Assignment 元素将每个事件的 、 、 和 附加到 一个字符串。StartDateTimeEndDateTimeSubjectLocation

展会活动 (4)

最后一个元素是显示包含所有事件的字符串的屏幕。

共享示例代码:向来宾用户授予读取记录的访问权限

在此代码示例集合中,来宾用户输入日期范围,然后查看 该范围内的事件。来宾用户通过共享对记录具有读取访问权限 规则。

重要

在向来宾用户授予读取访问权限之前,请参阅向来宾用户授予读取记录的访问权限。

Aura 组件:DisplayEvents.cmp

此示例 Aura 组件显示两个组件,用户在其中输入开始日期和结束日期 以查看事件。组件 显示每个事件的 、 、 和 。lightning:inputlightning:cardStartDateTimeEndDateTimeSubjectLocation

<aura:component controller="GuestUserEventsAuraController">

    <aura:attribute name="events" type="Event[]"/>
    <aura:attribute name="StartDate" type="String" default=""/>
    <aura:attribute name="EndDate" type="String" default=""/>

    <lightning:input type="datetime" name="StartDate" value="{!v.StartDate}" aura:id="StartDate" label="Start after: " required="true"/>
    <lightning:input type="datetime" name="EndDate" value="{!v.EndDate}" aura:id="EndDate" label="End before: " required="true"/>
    <lightning:button name="Submit" variant="brand" label="Find events" title="Find events" onclick="{!c.handleSearch}"/>
    
    <lightning:card title="Events">
        <p class="slds-p-horizontal--small">
            <aura:iteration items="{!v.events}" var="event">
                {!event.Subject} ({!event.Location}) starts at {!event.StartDateTime} and ends at {!event.EndDateTime} <br/>
            </aura:iteration>
        </p>
    </lightning:card>
</aura:component>

组件控制器:DisplayEventsController.js

此示例 JavaScript 控制器处理 Aura 组件的事件并调用 帮助程序文件中的方法。

({
    handleSearch : function(component, event, helper) {
        helper.doSearch(component, event, helper);
    }
})

JavaScript 帮助程序:DisplayEventsHelper.js

此 JavaScript 帮助程序创建一个异步请求来查找两者中的事件 用户提交的时间戳,并定义请求时要执行的操作 完成。

({
        doSearch : function(component, event, helper) {
            var start_date = component.find("StartDate").get("v.value");
            var end_date = component.find("EndDate").get("v.value");
            var action = component.get("c.searchEvents");
            action.setParams({
                "start_date": start_date,
                "end_date": end_date
            });
            action.setCallback(this, function(response){
                component.set("v.events", response.getReturnValue());
            });
            $A.enqueueAction(action);
        }
})

Apex 控制器:GuestUserEventsAuraController.cls

此示例 Apex 控制器接收从 JavaScript 查找记录的调用 助手。它选择符合以下条件的事件:

  • 事件更大 比参数。StartDateTimeStart_Date
  • 事件小于 参数。EndDateTimeEnd_Date
  • 事件的值为 。isPrivateFalse
  • 事件的值为 。isArchivedFalse

查询为每个事件返回以下字段:

  • StartDateTime
  • EndDateTime
  • Location
  • Subject
  • Id

由于来宾用户不需要记录 ID,因此循环会将所有其他字段复制到新的 Event 对象。然后,我们 将新对象添加到新列表,并将该列表返回给客户端。for

来宾用户有权访问具有共享规则的记录,因此我们定义了类 替换为关键字。with sharing

警告

Internet 上的任何系统或个人都可以调用方法。保护 方法。确保查询选择 仅所需的记录和必填字段。@AuraEnabled

public with sharing class GuestUserEventsAuraController {
	
    @AuraEnabled
    public static List<Event> searchEvents(Datetime start_date, Datetime end_date){
        List<Event> results = [SELECT Event.Subject,
                                  Event.StartDateTime,
                                  Event.EndDateTime,
                                  Event.Location
                FROM Event 
                WHERE Event.EndDateTime<:end_date AND 
                      Event.StartDateTime>:start_date AND 
                      Event.isPrivate=False AND 
                      Event.isArchived=False];

        List<Event> filtered_events = new List<Event>();
        for (Event event : results) {
            Event new_event = new Event(Subject = event.Subject, 
                                                              StartDateTime = event.StartDateTime, 
                                                              EndDateTime = event.EndDateTime,
                                                              Location = event.Location);
            filtered_events.add(new_event);
        }
        return filtered_events;
    }
}

自定义访问控制模型示例

这些代码和流示例使用自定义访问控制模型来提供 未经身份验证的来宾用户访问以创建记录。

  • 不共享的示例代码:授予来宾用户创建记录并在以后
    读取记录的权限 这些代码示例支持两个单独的交互。在第一次交互中,来宾用户创建一个案例。为了允许将来访问,Apex 方法将记录 ID 替换为加密字符串。当前宾用户稍后想要读取案例时,他们会输入加密的字符串。Apex 方法解密字符串并使用它来检索大小写。
  • 示例流:授予来宾用户创建记录的访问权限 在此示例流中,来宾用户输入反馈,流将其存储在自定义对象记录
    中。来宾用户在创建后无权读取记录。
  • 不共享的示例代码:授予来宾用户在同一事务
    中创建和读取记录的权限 在此代码示例集合中,来宾用户输入详细信息以报告支持问题,Apex 代码创建案例。Apex 方法检索新记录,Aura 组件在创建后向来宾用户显示部分记录。Apex 代码在不共享的情况下运行,因为我们不依赖对象权限和平台共享来允许来宾用户访问记录。
  • 不共享的示例流:授予来宾用户在一个流中创建和读取记录的权限 在此示例流中,来宾用户输入详细信息以报告支持问题,然后流
    创建案例。来宾用户创建记录后,默认活动用户将成为记录的所有者,并且来宾用户无法直接访问该记录。然后,流检索新案例以获取案例和字段,并将这些字段显示给来宾用户。由于来宾用户在创建记录后不拥有该记录,并且流必须检索该记录,因此该流在不共享的情况下运行。CaseNumberStatus
  • 不共享的示例代码:授予来宾用户创建记录并在以后
    更新记录的权限 这些代码示例支持两个单独的交互。在第一次交互中,来宾用户创建一个案例。出于安全原因,Apex 方法将记录 ID 替换为加密字符串。当来宾用户想要稍后关闭案例时,他们会输入该加密字符串。Apex 方法解密字符串以获取记录 ID,使用记录 ID 选择案例,并更新案例的状态。

不共享的示例代码:授予来宾用户创建记录和读取记录的权限 后

这些代码示例支持两个单独的交互。在第一次互动中,客人 用户创建案例。为了允许将来访问,Apex 方法将记录 ID 替换为 加密的字符串。当来宾用户稍后想要阅读案例时,他们会输入 加密字符串。Apex 方法解密字符串并使用它来检索大小写。

Aura 组件:CreateCase.cmp

此示例 Aura 组件显示来宾用户可以输入的多个组件 有关新案例或现有案例中的令牌的详细信息。创建记录后,组件将显示新的 案例的加密令牌或与令牌匹配的案例的状态。lightning:card

出于演示目的,此示例使用来宾用户可以在其中输入的字段 他们案件的令牌。若要实现此方案,请为来宾用户提供链接 包含令牌,并从 URL 中检索令牌。

<aura:component controller="GuestUserCreateForLater">
    <aura:attribute name="caseID" type="String"/>
    <aura:attribute name="case_status" type="String"/>
    <aura:attribute name="subject" type="String"/>
    <aura:attribute name="description" type="String"/>
    <aura:attribute name="email" type="String"/>

     Enter details to create a new case
    <lightning:input type="email" name="email" required="true" value="{!v.email}" aura:id="email" label="Where should we send email updates?"/>
    <lightning:input name="subject" label="Subject" required="true" value="{!v.subject}" aura:id="subject"/>
    <lightning:textarea name="description" required="true" label="Description" value="{!v.description}" aura:id="description"/>
    <lightning:button name="submit" variant="brand" label="Create case" title="Create case" onclick="{!c.submitCase}"/>
        
    <aura:if isTrue="{!v.caseID}">
        <lightning:card title="Case">
            <p class="slds-p-horizontal--small">
                New case created:
                <p>{!v.caseID}</p>
            </p>
        </lightning:card>
    </aura:if>

    Or enter an existing case token to view the status of the case
    <lightning:textarea name="existing_case" required="false" label="Existing case token" aura:id="existing_case"/>
	<lightning:button name="submit" variant="brand" label="Lookup case" title="Lookup case" onclick="{!c.lookupCase}"/>
    <aura:if isTrue="{!v.case_status}">
        <lightning:card title="Case">
            <p class="slds-p-horizontal--small">
                Case status: 
                <p>{!v.case_status}</p>
            </p>
        </lightning:card>
    </aura:if>
</aura:component>

组件控制器:CreateCaseController.js

此示例 JavaScript 控制器处理 Aura 组件的事件并调用 帮助程序文件中的方法。

({
    submitCase : function(component, event, helper) {
        helper.makeCase(component, event, helper);
    }
    lookupCase : function(component,event,helper){
        helper.getCase(component,event,helper);
    }
})

JavaScript 帮助程序:DisplayCaseHelper.js

此 JavaScript 帮助程序有两种方法:makeCase()该方法创建一个 异步请求,以使用提交的数据创建案例。当 请求完成时,回调会将新案例的唯一令牌存储在 使用的变量中的字段 由 Aura 组件提供。makeCase()caseIDgetCase()该方法使用令牌 由来宾用户输入,以异步检索匹配的案例 令牌。该方法的回调捕获来自 Apex 的响应 方法并将值存储在变量中。getCase()case_status

({
    makeCase : function(component, event, helper) {
        var subject = component.find("subject").get("v.value");
        var description = component.find("description").get("v.value");
        var email = component.find("email").get("v.value");

        var action = component.get("c.CreateCase");
        action.setParams({
            "subject": subject,
            "description": description,
            "email": email
        });
        action.setCallback(this, function(response){
            component.set("v.caseID", response.getReturnValue());
        });
        $A.enqueueAction(action);
    },
    getCase : function(component,event,helper){
        var case_token = component.find("existing_case").get("v.value");
        var action = component.get("c.GetCase");
        action.setParams({
            "token":case_token
        });
        action.setCallback(this, function(response){
            component.set("v.case_status", response.getReturnValue());
        });
        $A.enqueueAction(action);
    }
})

Apex 控制器:GuestUserCreateForLater.cls

此示例 Apex 控制器接收用于创建和检索案例的调用。它使用用户加密解密 AppExchange 包来加密和解密数据。CreateCase()Apex 方法创建一个 大小写与来宾用户的输入。创建记录后,它会生成一个 来自记录 ID、记录字段和当前记录的加密字符串 时间戳。Apex 方法返回加密的字符串。CreateCase()CreatedDateGetCase()该方法解密 提供的字符串,验证结果,并传递解密的记录 ID 并为帮助程序方法创建时间戳以检索原始记录。 响应是记录的状态。GetCase()

使用关键字定义类,因为我们不依赖于对象权限和平台共享 创建和访问记录。without sharing

警告

Internet 上的任何系统或个人都可以调用方法。确保查询可以 仅检索新创建的记录,并仅选择必填字段。@AuraEnabled

public class without sharing GuestUserCreateForLater {
    
    @AuraEnabled
    public static String CreateCase(String subject, 
                                         String description, 
                                         String email){
		Case new_case = new Case(Subject=subject, 
                                 Description=description,
                                 SuppliedEmail=email);
        insert new_case;
        
        List<Case> results = getCase(new_case.Id);


        String encryptedID = ued.UserCryptoHelper.doEncrypt(results[0].Id+'|'+ results[0].CreatedDate.getTime() +'|'+System.DateTime.now().getTime());
        return encryptedID;
    }
    
    public static final Long validTimestampMinutes = 10;

    @AuraEnabled
    public static String GetCase(String token){
        String status = 'Case not found';
        String decrypted_token = '';
        try {
            decrypted_token = ued.UserCryptoHelper.doDecrypt(token);
        } catch(Exception e) {
            return status;
        }
        
        String[] decrypted_parts = decrypted_token.split('\\|');
        String decryptedRecordId = decrypted_parts[0];
        String created_timestamp = decrypted_parts[1];
        String original_request_timestamp = decrypted_parts[2];

        
        
        if( isTimestampValid(System.Long.valueOf(original_request_timestamp)) ){
        	List<Case> caseList = getCase(decryptedRecordId, created_timestamp);
        	if(caseList.size() == 1){
    	        status = caseList[0].Status;
	    	}else{
                status = 'Case not found';
        	}
    	}
        return status;

    }

    private static List<Case> getCase(String caseID, Datetime created_date)
    {
        List<Case> results = [SELECT Case.CaseNumber, Case.CreatedDate, Case.Status  
        FROM Case 
        WHERE Case.Id=:caseID AND Case.CreatedDate=:created_date];
        return results;
    }

    private static Boolean isTimestampValid(Long timestamp)
    {
        return ((System.now().getTime() - timestamp) / 60000) < validTimestampMinutes;
    }
}

注意

如果要加载高度敏感的信息,请考虑以下附加信息之一 提高安全性的措施。

  • 要求用户输入与以下数据相关的其他信息 他们试图阅读或修改,只有他们自己知道。
  • 要求用户登录才能读取或修改数据。

示例流:授予来宾用户创建记录的访问权限

在此示例流中,来宾用户输入反馈,流将其存储在自定义流中 对象记录。来宾用户在创建后无权读取记录。

重要

在向来宾用户授予记录创建访问权限之前,请阅读向来宾用户授予创建记录的访问权限。

自定义Feedback__c对象

此方案使用 Feedback__c 自定义对象来存储来自来宾用户的反馈。一个 Feedback__c自定义对象都包含以下字段,按字母顺序列出:Email__c必填。来宾用户的电子邮件地址。数据类型: 电子邮件Score__c必填。来宾用户输入的反馈分数。可能的值为 、 、 、 、 、 。012345Additional_comments__c来宾用户输入的任何其他反馈。数据类型:长文本区域

流配置

因为流不需要对任何记录的读取访问权限,并且我们不依赖于对象 权限,将“如何运行流”设置设置为“系统” 无共享上下文 – 访问所有数据

意见反馈表 (1)

流中的第一个元素是显示以下组件的屏幕:

  • 用户电子邮件地址的电子邮件组件。
  • 用户反馈分数的 Slider 组件,设置为允许从 0 到 5.
  • 用于任何其他注释的长文本区域组件。

创建记录 (2)

下一个元素是创建Feedback__c记录的 Create Records 元素。

片尾画面 (3)

最后一个屏幕元素显示文本,以感谢用户的反馈。

不共享的示例代码:授予来宾用户在其中创建和读取记录的权限 交易

在此代码示例集合中,来宾用户输入详细信息以报告支持 问题和 Apex 代码创建一个案例。Apex 方法检索新记录和 Aura 组件在创建后向来宾用户显示部分记录。Apex 代码运行 没有共享,因为我们不依赖对象权限和平台共享来 允许来宾用户访问记录。

Aura 组件:CreateCase.cmp

此示例 Aura 组件显示用户在其中输入详细信息的多个组件 关于案件。创建后,该组件将显示新案例的案例编号和 地位。lightning:card

<aura:component controller="GuestUserCreateCase">

    <aura:attribute name="caseNumber" type="String"/>
    <aura:attribute name="status" type="String"/>
    <aura:attribute name="subject" type="String" default=""/>
    <aura:attribute name="description" type="String" default=""/>
    <aura:attribute name="email" type="String" default=""/>
    <aura:attribute name="name" type="String" default=""/>
    <aura:attribute name="reason" type="String"/>    
    <aura:attribute name="type" type="String" default=""/>

    <lightning:select name="select" label="Reason" required="true" value="{!v.reason}"  aura:id="reason">
        <option value="installation">Installation</option>
        <option value="equipmentcomplexity">Equipment Complexity</option>
        <option value="performance">Performance</option>
        <option value="breakdown">Breakdown</option>
        <option value="equipmentdesign">Equipment Design</option>
        <option value="feedback">Feedback</option>
        <option value="other">Other</option>
    </lightning:select>
    
    <lightning:select name="type" label="Type" required="true" value="{!v.type}"  aura:id="type">
        <option value="mechanical">Mechanical</option>
        <option value="electrical">Electrical</option>
        <option value="electronic">Electronic</option>
        <option value="structural">Structural</option>
        <option value="other">Other</option>
    </lightning:select>
    
    <lightning:input type="email" name="email" required="true" value="{!v.email}" aura:id="email" label="Where should we send email updates?"/>
    <lightning:input name="name" label="Name" required="true" value="{!v.name}" aura:id="name"/>
    
    
    <lightning:input name="subject" label="Subject" required="true" value="{!v.subject}" aura:id="subject"/>
    <lightning:textarea name="description" required="true" label="Description" value="{!v.description}" aura:id="description"/>
    
    <lightning:button name="submit" variant="brand" label="Submit case" title="Submit case" onclick="{!c.submitCase}"/>
    
    <aura:if isTrue="{!v.caseNumber}">
        <lightning:card title="Case">
            <p class="slds-p-horizontal--small">
                {!v.caseNumber} has status {!v.status}.
            </p>
        </lightning:card>
    </aura:if>
</aura:component>

组件控制器:CreateCaseController.js

此示例 JavaScript 控制器处理 Aura 组件的事件并调用 帮助程序文件中的方法。

({
    submitCase : function(component, event, helper) {
        helper.makeCase(component, event, helper);
    }
})

JavaScript 帮助程序:DisplayCaseHelper.js

此 JavaScript 帮助程序创建一个异步请求,以使用 提交的数据。请求完成后,回调会存储案例编号和 Aura 组件使用的变量中的案例状态。

({
        makeCase : function(component, event, helper) {
            var subject = component.get("v.subject");
            var description = component.get("v.description");
            var email = component.get("v.email");
            var name = component.get("v.name");
            var reason = component.get("v.reason");
            var type = component.get("v.type");

            
            var action = component.get("c.CreateCase");
            action.setParams({
                "subject": subject,
                "description": description,
                "email": email,
                "name": name,
                "reason": reason,
                "caseType": type
            });
            action.setCallback(this, function(response){
                component.set("v.caseNumber", response.getReturnValue()[0]);
                component.set("v.status", response.getReturnValue()[1]);
            });
            $A.enqueueAction(action);
        }
})

Apex 控制器:GuestUserCreateCase.apxc

此示例 Apex 控制器创建记录,检索新记录,然后返回 从新记录到客户端的必填字段。因为对象权限 并且不使用平台共享,此控制器在不共享的情况下运行。

为了避免意外暴露记录数据,该方法仅返回 和 字段。CreateCaseCaseNumberStatus

警告

Internet 上的任何系统或个人都可以调用类。确保方法 仅返回新记录中的必填字段。@AuraEnabled

public without sharing class GuestUserCreateCase {
    
    @AuraEnabled
    public static List<String> CreateCase(String subject, 
                                         String description, 
                                         String email,
                                         String name,
                                         String reason,
                                         String caseType,
                                         String phone){
		Case new_case = new Case(Subject=subject, 
                                 Description=description,
                                 SuppliedEmail=email,
                                 SuppliedName=name,
                                 Reason=reason,
                                 Type=caseType,
                                 SuppliedPhone=phone);
        insert new_case;
                                             
        List<Case> results = getCase(new_case.Id);

        List<String> response = new List<String>();
        response.add(results[0].CaseNumber);
        response.add(results[0].Status);
        return response;

    }

    private static List<Case> getCase(String caseID)
    {
        List<Case> results = [SELECT Case.CaseNumber, Case.CreatedDate 
        FROM Case 
        WHERE Case.Id=:caseID];
        return results;
    }

}

不共享的示例流:为来宾用户提供在一个中创建和读取记录的权限 流

在此示例流中,来宾用户输入详细信息以报告支持问题和流 创建案例。来宾用户创建记录后,默认活动用户将成为所有者 的记录,来宾用户无法直接访问它。然后,该流检索 New case 获取案例的 AND 字段,并将这些字段显示给来宾用户。 由于来宾用户在创建后不拥有该记录,并且流必须检索 记录时,流在不共享的情况下运行。

CaseNumberStatus

重要

在向来宾用户授予创建和读取访问权限之前,请先阅读授予来宾用户读取记录的访问权限和授予来宾用户创建记录的访问权限。

流配置

由于流会创建记录,然后在不共享的情况下检索该记录,因此请将“如何运行流”设置设置为“不使用系统上下文” 共享 – 访问所有数据

案例表格 (1)

流中的第一个元素是显示以下输入组件的屏幕:

  • 公司名称的文本组件
  • 提交者姓名的 Name 组件
  • 提交者电子邮件地址的电子邮件组件
  • 提交者电话号码的电话组件
  • 一个 Picklist 组件,其中包含记录类型字段中的选项值Type_Options
  • 一个 Picklist 组件,其中包含记录类型字段中的选项值Reason_Options
  • 案例主题的文本组件
  • 用于描述案例的长文本区域组件

作业 (2)

第二个元素将输入组件中的数据分配给新的记录变量。Case

创建记录 (3)

下一个元素是“创建记录”元素,该元素使用记录变量创建案例记录。除了信息 由访客用户输入,设置元素配置以定义案例的来源字段 如。CaseWeb

获取记录 (4)

“获取记录”元素通过其字段检索新记录,该字段由“创建记录”元素自动定义。这 检索到的记录存储在新记录中 变量。IdCase

片尾画面 (5)

最后一个屏幕元素显示 Get 中的 Case 和字段 Record 元素的记录变量。CaseNumberStatusCase

不共享的示例代码:授予来宾用户创建记录和更新记录的权限 后

这些代码示例支持两个单独的交互。在第一次互动中,客人 用户创建案例。出于安全原因,Apex 方法将记录 ID 替换为 加密字符串。当来宾用户想要稍后关闭案例时,他们会输入该案例 加密字符串。Apex 方法解密字符串以获取记录 ID,使用记录 ID 以选择案例,并更新案例的状态。

Aura 组件:CreateCase.cmp

此示例 Aura 组件显示用于创建和关闭记录的组件。

若要创建案例,来宾用户使用组件输入案例详细信息。记录后 创建时,组件显示 新案例的加密令牌或与令牌匹配的案例的状态。lightning:card

出于演示目的,此示例显示一个组件,其中来宾用户 直接输入案例的令牌。若要实现此方案,请提供来宾用户 替换为包含令牌的链接,然后从 URL 中检索令牌。

<aura:component controller="GuestUserCreateForLater">
    <aura:attribute name="caseID" type="String"/>
    <aura:attribute name="case_status" type="String"/>
    <aura:attribute name="subject" type="String"/>
    <aura:attribute name="description" type="String"/>
    <aura:attribute name="email" type="String"/>

     Enter details to create a new case
    <lightning:input type="email" name="email" required="true" value="{!v.email}" aura:id="email" label="Where should we send email updates?"/>
    <lightning:input name="subject" label="Subject" required="true" value="{!v.subject}" aura:id="subject"/>
    <lightning:textarea name="description" required="true" label="Description" value="{!v.description}" aura:id="description"/>
    <lightning:button name="submit" variant="brand" label="Create case" title="Create case" onclick="{!c.submitCase}"/>
        
    <aura:if isTrue="{!v.caseID}">
        <lightning:card title="Case">
            <p class="slds-p-horizontal--small">
                New case created:
                <p>{!v.caseID}</p>
            </p>
        </lightning:card>
    </aura:if>

    Or enter an existing case token to close the case
    <lightning:textarea name="existing_case" required="false" label="Existing case token" aura:id="existing_case"/>
	<lightning:button name="submit" variant="brand" label="Close case" title="Close case" onclick="{!c.updateCase}"/>
    <aura:if isTrue="{!v.case_status}">
        <lightning:card title="Case">
            <p class="slds-p-horizontal--small">
                Case status: 
                <p>{!v.case_status}</p>
            </p>
        </lightning:card>
    </aura:if>
</aura:component>

组件控制器:CreateCaseController.js

此示例 JavaScript 控制器处理 Aura 组件的事件并调用 帮助程序文件中的方法。

({
    submitCase : function(component, event, helper) {
        helper.makeCase(component, event, helper);
    },
    updateCase : function(component,event,helper){
        helper.updateCase(component,event,helper);
    }
})

JavaScript 帮助程序:CaseHelper.js

此 JavaScript 帮助程序有两种方法:makeCase()该方法创建一个 异步请求,以使用提交的数据创建案例。当 请求完成时,回调会将新案例的唯一令牌存储在 使用的变量中的字段 由 Aura 组件提供。makeCase()caseIDupdateCase()该方法使用 来宾用户输入的令牌,以异步更新大小写 匹配令牌。该方法的回调捕获来自 Apex 方法并将值存储在变量中。updateCase()case_status

({
    makeCase : function(component, event, helper) {
        var subject = component.find("subject").get("v.value");
        var description = component.find("description").get("v.value");
        var email = component.find("email").get("v.value");

        var action = component.get("c.CreateCase");
        action.setParams({
            "subject": subject,
            "description": description,
            "email": email
        });
        action.setCallback(this, function(response){
            component.set("v.caseID", response.getReturnValue());
        });
        $A.enqueueAction(action);
    },
    updateCase : function(component,event,helper){
        var case_token = component.find("existing_case").get("v.value");
        var action = component.get("c.UpdateCase");
        action.setParams({
            "token":case_token
        });
        action.setCallback(this, function(response){
            component.set("v.case_status", response.getReturnValue());
        });
        $A.enqueueAction(action);
    }
})

Apex 控制器:GuestUserCreateForLater.cls

此示例 Apex 控制器接收用于创建和更新案例的调用。它使用用户加密解密 AppExchange 包来加密和解密数据。CreateCase()Apex 方法 使用来宾用户的输入创建案例。记录创建后,它 从记录 ID、记录字段和当前 时间戳。Apex 将新案例的 ID 替换为加密的 字符串。CreateCase()CreatedDateUpdateCase()该方法解密 提供的字符串,验证结果,并使用信息来 更新原始记录的状态。响应是 记录或错误消息(如果发生)。UpdateCase()

使用关键字定义类 因为它不直接访问记录。with sharing

警告

Internet 上的任何系统或个人都可以调用类。确保查询可以 仅更新正确的记录。@AuraEnabled

public with sharing class GuestUserCreateForLater {
    
    @AuraEnabled
    public static String CreateCase(String subject, 
                                         String description, 
                                         String email){
		Case new_case = new Case(Subject=subject, 
                                 Description=description,
                                 SuppliedEmail=email);
        insert new_case;
        
       List<Case> results = GuestUserCaseHelperWS.getCase(new_case.Id);


        String encryptedID = ued.UserCryptoHelper.doEncrypt(results[0].Id+'|'+ results[0].CreatedDate.getTime() +'|'+System.DateTime.now().getTime());
        return encryptedID;
    }
    
    public static final Long validTimestampMinutes = 10;

    @AuraEnabled
    public static String UpdateCase(String token){
        String status = 'Case not found';
        String decrypted_token = '';
        try {
            decrypted_token = ued.UserCryptoHelper.doDecrypt(token);
        } catch(Exception e) {
            return status;
        }
        String[] decrypted_parts = decrypted_token.split('\\|');
        String decryptedRecordId = decrypted_parts[0];
        String created_timestamp = decrypted_parts[1];
        String original_request_timestamp = decrypted_parts[2];


        if( isTimestampValid(System.Long.valueOf(original_request_timestamp))) {

        	List<Case> caseList = GuestUserCaseHelperWS.getCase(decryptedRecordId, created_timestamp);
            if(caseList.size() == 1){
    	       Case case_to_update = caseList[0];
                        case_to_update.Status = 'Closed';
                try {
                         GuestUserCaseHelperWS.updateCase(case_to_update);
                         status = 'Closed';
                } catch(DmlException e){
                         System.debug('An unexpected error has occurred: ' + e.getMessage());
                }
            }else{
                status = 'Case not found';
            }
        }
        return status;
    }

    private static Boolean isTimestampValid(Long timestamp)
    {
        return ((System.now().getTime() - timestamp) / 60000) < validTimestampMinutes;
    }
}

Apex 帮助程序类:GuestUserCaseHelperWS.apxc

此示例 Apex 帮助程序类定义按记录的 ID 和 更新记录。Apex 控制器调用此方法。

使用关键字定义类,以便它可以检索和更新记录,而无需共享。without sharing

public without sharing class GuestUserCaseHelperWS {

    public static List<Case> getCase(String caseID, Datetime created_date)
    {
        List<Case> results = [SELECT Case.CaseNumber, Case.CreatedDate, Case.Status  
        FROM Case 
        WHERE Case.Id=:caseIDAND Case.CreatedDate=:created_date];
        return results;
    }

    public static Case updateCase(Case case_to_update)
    {
        update case_to_update;
        return case_to_update;
    }
}

限制对 Apex 类的访问

仅允许来宾和外部用户访问他们必须访问的类 叫。

如果 Apex 类包含公开的方法,例如使用 、、 或 的方法,则来宾和外部用户可以调用这些方法 具有任意参数的方法。但是他们必须具有执行 Apex 类的权限。我们 建议将 Apex 类访问权限限制为具有特定权限集或配置文件的用户。 允许来宾和外部用户对 Apex 类的完全访问权限是不安全的。㥢 关于哪些用户必须调用哪些 Apex 类,为这些角色创建权限集,以及 为所需的权限集启用 Apex 类。@InvocableMethod@AuraEnabled@RestResourcewebservice

流量安全

如果来宾用户或外部用户必须运行流,请覆盖流权限以授予访问权限 仅适用于特定的外部用户配置文件、权限集或站点来宾用户配置文件,而不是 而不是允许用户运行所有流。尽可能避免在系统上下文中运行流,以及 限制对子流的访问。否则,请确保对 这些流和子流。

流是一项强大的功能,可以覆盖平台安全设置以访问 对象和 Apex 类。流可用于激活和停用权限集。还 屏幕流由浏览器使用用户控制的输入参数驱动。因此,我们 建议重写“运行流”权限以将访问权限分配给 基于来宾或外部用户配置文件或权限集的特定流。对于来宾用户, 在相应站点的来宾用户配置文件上配置流访问策略。

删除运行子流的权限也是一种很好的安全做法,即使用户运行 子流独立。从安全角度来看,最好创建两个单独的 流,并仅授予对用户直接运行的流的访问权限,而不授予对以 子流。仅向最高级别的父流授予流访问权限,而不向子流授予流访问权限。 同样的建议也适用于流调用的可调用的 Apex 方法。避免授予 用户对这些类的访问权限,因此调用这些方法仅限于 他们注定要被召唤。请注意,如果用户有权运行屏幕流,他们可以:

  • 随时使用他们选择的参数调用流。
  • 随时取消流。

这些注意事项也适用于子流或从其他流调用的流。具体而言,流用户可以:

  • 查看和修改屏幕流的输入(开始)变量。
  • 查看从屏幕子流返回到父流的输出变量。
  • 如果他们有权运行子流,请将输入变量修改为子流。

如果其中任何一项功能违反了您的安全策略,请不要使用子流。例如,如果 处理必须保密的账单信息或其他敏感信息 通过子流。将业务逻辑保留在主流中。

SOQL注射液

清理传递到动态 SOQL 查询的用户控制数据。当 Apex 代码将用户控制的数据插入到动态 SOQL 中时,会发生 SOQL 或 SOSL 注入 或 SOSL 查询,但未正确审查输入。有两种情况需要考虑:

  • 更改查询的整体结构
  • 更改查询参数的值

通过调用阻止调用方访问数据的 Apex 代码来控制这些方案 他们无权获得。考虑一下 法典:

@AuraEnabled
public static List<Account> getAccountName(string userId) {
  if (FeatureManagement.checkPermission('readAccount')) {
      string query='SELECT Name FROM Account WHERE Id=\''+ userId + '\'';
      return database.query(query);
  }
}

用户可能会获得对查询的控制权,并访问比他们更多的信息 有权。查询的返回值不会限制用户可以访问的信息。用户可以 提交字符串,例如 :

userId = '0035Y00003pPJiNQAW\' OR AnnualRevenue>100000.00 OR Name=\'a'
// 0035Y00003pPJiNQAW is any id to any object that is not an account

这 查询的返回值将列出年收入超过 100,000 美元的所有帐户, 这不是开发人员打算返回给调用方的内容。修复 SOQL 或 SOSL 注入需要适当的上下文编码。不适用于每个用户输入。而 根据字段在 SOQL 或 SOSL 查询中的显示位置清理字段。

String.escapeSingleQuotes

  • 对于 WHERE (SOQL)、ORDER BY (SOQL)、WITH (SOSL) 或 FIND (SOSL) 子句中的变量,请使用 绑定 变量:string query='SELECT Name FROM Account WHERE Id=:userId';
  • 对于字段和表名称,请调用字段的 。或强制执行 声明性策略,使用您自己的过程逻辑将字段或表名限制为 您的安全策略允许的内容。isAccessibledescribeResult
  • 对于带引号的字符串中的参数,请使用绑定变量。不要使用绑定变量或清理表名 和字段名称,或未出现在引号上下文中的任何参数。String.escapeSingleQuote
  • 对于其他基元类型,将用户输入转换为布尔值、整数、Id 或其他 基元(非字符串)类型。

请注意,WITH SECURITY_ENFORCED关键字不会清理 WHERE 子句,只会清理 WHERE 子句 SELECT 和 FROM 子句,因此它不是 SOQL 或 SOSL 注入攻击的清理程序。