使用 JWT 保护 Salesforce 集成:Apex 与连接应用的开发者指南

背景与应用场景

在现代企业级应用架构中,系统间的安全通信与身份验证是至关重要的一环。随着云计算和微服务架构的普及,不同服务之间如何安全、高效地交换信息成为了开发者面临的常见挑战。JSON Web Token (JWT) 应运而生,它是一种开放标准 (RFC 7519),定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息。

JWT 的核心优势在于其无状态性(statelessness)和自包含性(self-contained)。这意味着令牌本身包含了验证所需的所有信息,接收方无需访问数据库即可验证其真实性。这极大地简化了身份验证流程,减少了服务器负载,并提升了可扩展性。

在 Salesforce 平台上,JWT 在多种集成场景中发挥着关键作用:

  • 服务器到服务器集成(Server-to-Server Integrations): 当外部系统需要调用 Salesforce API,或者 Salesforce 需要调用外部系统的 API 时,JWT 提供了一种无需交互式用户登录的身份验证机制。例如,夜间批量数据同步、自动化流程触发等场景。
  • OAuth 2.0 JWT 持有者流(OAuth 2.0 JWT Bearer Flow): 这是 Salesforce 连接应用(Connected App) 的一种常用授权流,允许外部应用通过预先配置的数字证书,使用 JWT 断言(assertion)来直接获取 Salesforce 访问令牌(Access Token),而无需用户手动授权。这在构建自定义 SSO(Single Sign-On)解决方案或集成第三方应用时非常有用。
  • 从 Apex 调用外部服务: 当 Apex 代码需要安全地调用外部 REST API,且该外部 API 支持 JWT 验证时,Salesforce 提供了内置的类库,允许开发者在 Apex 中生成、签名 JWT,并将其作为授权凭证发送给外部服务。
  • 单点登录(Single Sign-On, SSO): 虽然 JWT 本身不是一个完整的 SSO 协议,但它是构建许多 SSO 解决方案的基石,特别是在实现基于 SAML 2.0 或 OAuth 2.0 的自定义 SSO 场景中。

本文将深入探讨 JWT 的工作原理,并重点介绍如何在 Salesforce 平台上,特别是通过 Apex 代码,有效地生成和使用 JWT,从而实现安全、高效的系统集成。


原理说明

JWT,全称 JSON Web Token,通常由三部分组成,它们之间用点号(.)分隔:Header(头部)Payload(有效载荷)Signature(签名)。它的结构形如 header.payload.signature

1. Header(头部)

Header 是一个 JSON 对象,通常包含两部分信息:

  • alg:签名算法,例如 HS256(HMAC SHA256)或 RS256(RSA SHA256)。Salesforce 在与连接应用配合使用时,通常推荐使用 RS256。
  • typ:令牌类型,通常为 "JWT"。

例如:

{
  "alg": "RS256",
  "typ": "JWT"
}

这个 JSON 对象会被进行 Base64url 编码,形成 JWT 的第一部分。

2. Payload(有效载荷)

Payload 也是一个 JSON 对象,包含了一组声明(claims),这些声明是关于实体(通常是用户)和附加数据的陈述。声明分为三类:

  • 注册声明(Registered Claims): 一组预定义的声明,它们不是强制性的,但推荐使用,以提供有用的可互操作的声明。例如:
    • iss (issuer):签发者,表示 JWT 的签发方。在 Salesforce Connected App 的 JWT Bearer Flow 中,这通常是连接应用的消费者密钥(Consumer Key)。
    • sub (subject):主题,表示 JWT 所面向的用户或主体。在 Salesforce JWT Bearer Flow 中,这通常是目标 Salesforce 用户的用户名。
    • aud (audience):受众,表示 JWT 的接收方。在 Salesforce JWT Bearer Flow 中,这通常是 Salesforce 的 OAuth 令牌端点(https://login.salesforce.com/services/oauth2/tokenhttps://test.salesforce.com/services/oauth2/token)。
    • exp (expiration time):过期时间,一个 Unix 时间戳,表示 JWT 的过期时间。
    • nbf (not before):生效时间,一个 Unix 时间戳,表示 JWT 在此时间之前不可用。
    • iat (issued at):签发时间,一个 Unix 时间戳,表示 JWT 的签发时间。
    • jti (JWT ID):JWT 的唯一标识符。
  • 公共声明(Public Claims): 可以在 JWT 生产者和消费者之间自定义的声明,但为了避免冲突,它们应该在 IANA JSON Web Token Registry 中注册,或者定义为包含碰撞抵抗命名空间的 URI。
  • 私有声明(Private Claims): 可以在生产者和消费者之间创建的自定义声明,既不是注册声明也不是公共声明。使用时应确保不会与其他声明名称冲突。

例如:

{
  "iss": "3MVG9Wt...", // Connected App Consumer Key
  "sub": "user@example.com", // Salesforce Username
  "aud": "https://login.salesforce.com/services/oauth2/token",
  "exp": 1678886400, // Unix timestamp for expiration
  "iat": 1678882800  // Unix timestamp for issued at
}

这个 JSON 对象同样会被进行 Base64url 编码,形成 JWT 的第二部分。

3. Signature(签名)

Signature 是 JWT 的核心安全机制。它通过使用 Header 中指定的算法(例如 RS256)和一个密钥,对编码后的 Header 和 Payload 进行签名。签名的目的是验证 JWT 的发送者,并确保令牌在传输过程中未被篡改。

签名是这样生成的:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your_secret_key
)

或者对于 RSA 签名(如 RS256),使用私钥对 base64UrlEncode(header) + "." + base64UrlEncode(payload) 的哈希值进行加密。

在 Salesforce 中,当使用 Auth.JWS 类进行签名时,我们通常会引用 Salesforce 中上传的数字证书(Digital Certificate)的名称。这个数字证书包含了一个私钥(Private Key),用于对 JWT 进行签名。对应的公钥(Public Key)则被外部系统(或 Salesforce 连接应用)用来验证签名。


示例代码:在 Apex 中生成并使用 JWT 调用外部服务

本示例将展示如何在 Salesforce Apex 中生成一个 JWT,并使用该 JWT 作为授权凭证来调用一个外部 REST API。这个过程涉及到使用 Salesforce 提供的 Auth.JWT 类来构建 JWT 的头部和载荷,以及 Auth.JWS 类来使用预配置的数字证书对 JWT 进行签名。

前提条件:

  • 您需要在 Salesforce 中创建一个数字证书(Certificates & Key Management)。导航至“设置(Setup)” -> “身份(Identity)” -> “证书和密钥管理(Certificates & Key Management)”,点击“创建自签名证书(Create Self-Signed Certificate)”,给它一个易于识别的标签和唯一名称(例如:MyJwtSigningCert)。请确保记录下此证书的唯一名称,这将在 Apex 代码中使用。
  • 您需要将该证书的公钥(Public Key)提供给您希望调用的外部服务。外部服务将使用此公钥来验证由 Salesforce 生成的 JWT 签名。
  • 外部服务需要有一个支持 JWT Bearer 认证的 API 端点。
/**
 * @description JWT 示例服务类,演示如何在 Apex 中生成 JWT 并调用外部服务。
 *              该服务类使用了 Salesforce 的 Auth.JWT 和 Auth.JWS 类。
 *              官方文档参考:
 *              Auth.JWT: https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_class_Auth_JWT.htm
 *              Auth.JWS: https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_class_Auth_JWS.htm
 */
public class JwtIntegrationService {

    // 定义外部服务的端点 URL 和证书名称
    private static final String EXTERNAL_API_ENDPOINT = 'https://api.example.com/protected-resource';
    private static final String CERTIFICATE_NAME = 'MyJwtSigningCert'; // 在 Salesforce 中创建的证书的唯一名称
    private static final String JWT_AUDIENCE = 'https://api.example.com/oauth2/token'; // JWT 的接收方(外部服务的令牌端点或资源服务器)
    private static final String JWT_ISSUER = 'SalesforceOrgIdOrConnectedAppConsumerKey'; // JWT 签发者,可以是您的 Salesforce 组织ID 或 Connected App 的 Consumer Key
    private static final String JWT_SUBJECT = 'salesforce-integration-user@example.com'; // JWT 所代表的主体,通常是外部服务用于识别的用户名或ID

    /**
     * @description 生成 JWT 并调用外部 API 的主方法。
     * @return String 外部 API 的响应主体。
     */
    public static String callExternalApiWithJwt() {
        String responseBody = '';
        try {
            // 1. 创建 Auth.JWT 实例,用于构建 JWT 的 Header 和 Payload
            //    Auth.JWT 类负责 JWT 的结构化,包括其头部(Header)和有效载荷(Payload)中的声明(Claims)。
            Auth.JWT jwt = new Auth.JWT();

            // 设置 JWT 的声明 (Claims)
            // iss (Issuer): JWT 的签发者,标识谁创建了这个 JWT。
            //             这里设置为一个自定义的标识符,或者在Connected App场景中是Consumer Key。
            //             接收方会用此值来识别令牌的来源。
            jwt.setIssuer(JWT_ISSUER);

            // sub (Subject): JWT 的主题,通常是代表的用户或系统标识符。
            //             外部服务会使用此值来识别请求方。
            jwt.setSubject(JWT_SUBJECT);

            // aud (Audience): JWT 的受众,即期望接收和验证此 JWT 的服务或应用程序。
            //             确保这个值与外部服务的期望值精确匹配,这是验证的关键一步。
            jwt.setAudience(JWT_AUDIENCE);

            // exp (Expiration Time): JWT 的过期时间,以 Unix 时间戳表示(自 1970-01-01 00:00:00 UTC 以来的秒数)。
            //             设置一个合理的短时过期时间以增强安全性,例如 5 分钟。
            //             JWT 的生命周期不宜过长,避免泄露风险。
            //             Salesforce 的 getTime() 返回毫秒,需要除以 1000 转换为秒。
            Long expirationTimeSeconds = Datetime.now().addMinutes(5).getTime() / 1000;
            jwt.setExpiration(expirationTimeSeconds);

            // iat (Issued At): JWT 的签发时间,以 Unix 时间戳表示。
            //             有助于接收方判断 JWT 的新鲜度。
            Long issuedAtTimeSeconds = Datetime.now().getTime() / 1000;
            jwt.setIssuedAt(issuedAtTimeSeconds);

            // 2. 创建 Auth.JWS 实例,用于使用证书对 JWT 进行签名
            //    Auth.JWS 类负责将 Auth.JWT 对象转换为签名后的 JWT 字符串。
            //    它使用 Salesforce 中存储的指定名称的证书的私钥进行签名。
            Auth.JWS jws = new Auth.JWS(jwt, CERTIFICATE_NAME);

            // 3. 签名 JWT,生成最终的签名令牌字符串
            //    getSignedToken() 方法执行签名操作,返回一个 Base64url 编码的 JWT 字符串。
            String signedJwtToken = jws.getSignedToken();
            System.debug('Generated Signed JWT Token: ' + signedJwtToken);

            // 4. 构建 HTTP 请求,将 JWT 作为 Bearer Token 发送
            HttpRequest request = new HttpRequest();
            request.setMethod('GET'); // 或 'POST', 'PUT' 等,取决于外部 API 的要求
            request.setEndpoint(EXTERNAL_API_ENDPOINT);
            // 将签名后的 JWT 放入 Authorization 头部,格式为 "Bearer "
            request.setHeader('Authorization', 'Bearer ' + signedJwtToken);
            request.setTimeout(120000); // 设置请求超时时间(毫秒),120000ms = 120秒

            // 5. 发送 HTTP 请求
            Http http = new Http();
            HttpResponse response = http.send(request);

            // 6. 处理响应
            if (response.getStatusCode() == 200) {
                responseBody = response.getBody();
                System.debug('External API Response: ' + responseBody);
            } else {
                // 处理非 200 状态码的错误
                System.error('Error calling external API: ' + response.getStatusCode() + ' - ' + response.getStatus());
                System.error('Response Body: ' + response.getBody());
                // 抛出 CalloutException 便于上层捕获和处理
                throw new CalloutException('Failed to call external API: ' + response.getStatusCode() + ' - ' + response.getStatus());
            }

        } catch (Exception e) {
            // 捕获在 JWT 生成、签名或 HTTP Callout 过程中可能发生的任何异常
            System.error('An error occurred during JWT generation or API call: ' + e.getMessage());
            System.error('Stack Trace: ' + e.getStackTraceString());
            // 可以在此处抛出自定义异常或进行其他错误处理,例如记录到自定义对象或发送通知
            throw new CalloutException('JWT integration failed: ' + e.getMessage());
        }
        return responseBody;
    }
}

如何执行:

您可以在“匿名执行(Anonymous Apex)”窗口中调用此方法进行测试:

System.debug(JwtIntegrationService.callExternalApiWithJwt());

注意事项

在 Salesforce 平台上实现 JWT 集成时,需要考虑多方面的因素,以确保其安全性、可靠性和合规性。

1. 权限与访问控制

  • 证书访问: Apex 代码需要访问在 Salesforce 中存储的数字证书。这通常由运行 Apex 代码的用户或集成用户配置文件(Profile)和权限集(Permission Set)隐式管理。确保证书已正确配置,并且 Apex 在正确的上下文中执行。如果遇到“System.SecurityException: Unable to access certificate”错误,请检查证书是否存在且名称正确。
  • 远程站点设置(Remote Site Settings): 如果 Apex 代码要调用外部 API,必须在 Salesforce 中配置远程站点设置(Remote Site Settings)。导航至“设置(Setup)” -> “安全” -> “远程站点设置”,添加外部 API 的 URL。这是 Salesforce 平台出于安全考虑,对所有出站 HTTP/HTTPS 请求的强制要求。如果缺少此设置,将导致 “CalloutException: Unauthorized endpoint” 错误。
  • Connected App 权限: 如果您是通过 OAuth 2.0 JWT Bearer Flow 使用连接应用来访问 Salesforce,确保连接应用已正确配置:
    • OAuth 策略(OAuth Policies): 授予对特定用户配置文件的访问权限,或者将“允许的用户(Permitted Users)”设置为“Admin approved users are pre-authorized”并指定相应的配置文件/权限集。
    • IP 范围(IP Ranges): 如果您的组织有严格的 IP 限制,确保连接应用的 IP 范围包含所有可能发起请求的 IP 地址。
    • API 启用: 确保连接应用拥有必要的 API 权限(如“访问和管理您的数据(Access and manage your data (api))”)。

2. API 限制与性能

  • Apex CPU 时间限制: JWT 的生成和签名过程,特别是涉及到非对称加密(如 RS256)时,可能会消耗一定的 CPU 时间。虽然 Salesforce 的 Auth.JWS 类进行了优化,但在高并发或循环中大量生成 JWT 时,仍需留意 Apex 的 CPU 时间限制(同步事务最长 10 秒,异步事务最长 60 秒)。应避免在循环中重复生成或签名 JWT。
  • Callout 限制: Salesforce 对出站 HTTP 调用(Callout)有严格的限制,包括:
    • 每个同步 Apex 事务最多可以发出 100 个 Callout。
    • 每个事务的所有 Callout 的总时间最长 120 秒。
    • 响应主体大小限制(通常为 6MB)。
    合理设计集成方案,考虑批量处理和异步调用(如使用 `Queueable` 或 `Batch Apex`),以避免触及这些限制。

3. 安全性最佳实践

  • 私钥安全: Salesforce 证书和密钥管理功能提供了安全的私钥存储。永远不要在 Apex 代码、自定义设置(Custom Settings)或自定义元数据类型(Custom Metadata Types)中以明文形式存储私钥字符串。始终通过引用证书的唯一名称来使用私钥,让 Salesforce 平台负责私钥的安全管理。
  • JWT 过期时间(exp): 将 JWT 的过期时间设置得尽可能短,以减少令牌被盗用后造成的损害。通常建议在几分钟(例如 5-10 分钟)内过期。如果外部服务需要更长的会话,可以考虑实现令牌刷新机制,而不是延长单个 JWT 的有效期。
  • 验证所有声明: 外部服务在接收 JWT 时,必须严格验证所有的注册声明,特别是 iss (签发者)、aud (受众)、exp (过期时间) 和 sub (主题)。这可以防止未经授权的令牌被接受,并确保令牌的有效性和目标正确性。
  • 证书管理: 定期检查并更新在 Salesforce 中使用的数字证书。证书是有有效期的,过期后将导致签名失败。建立证书轮换策略,并在证书到期前进行更新。在证书到期前创建新证书,并将公钥同步给外部系统,然后更新 Apex 代码中引用的证书名称。
  • 日志与监控: 实施充分的日志记录,以便在 JWT 生成、签名或调用外部服务失败时进行故障排除。监控外部服务的响应和错误,以便及时发现和解决问题。可以使用 `System.debug` 进行调试日志,或者将更重要的错误记录到自定义对象或通过邮件通知。

4. 错误处理

  • 异常处理: 使用 `try-catch` 块来捕获在 JWT 生成、签名或 HTTP Callout 过程中可能发生的异常(例如 `CalloutException`、`System.UnexpectedException`)。提供有意义的错误消息和日志记录,这对于集成调试至关重要。
  • HTTP 响应码: 外部服务可能会返回不同的 HTTP 状态码(例如 401 Unauthorized, 403 Forbidden, 400 Bad Request, 500 Internal Server Error)。根据不同的状态码执行相应的错误处理逻辑,而不是仅仅检查 200 OK。例如,对于 401 错误,可能需要检查 JWT 的有效性或证书配置。
  • 重试机制: 对于偶发的网络或服务问题,可以考虑实现指数退避(Exponential Backoff)的重试机制,但要注意不要过度重试,以免加重外部服务负担或触及 Salesforce Callout 限制。重试机制应在异步上下文中实现。

总结与最佳实践

JSON Web Token (JWT) 为 Salesforce 的安全集成提供了一个强大且灵活的框架。无论是作为连接应用实现 SSO,还是从 Apex 代码安全地调用外部服务,JWT 都通过其无状态、自包含和可签名的特性,简化了身份验证和授权的复杂性。正确地理解和实施 JWT 对于构建健壮的企业级集成至关重要。

最佳实践:

  1. 优先使用 Salesforce 平台特性: 充分利用 Salesforce 提供的 Auth.JWTAuth.JWS 类来生成和签名 JWT,以及其内置的证书和密钥管理功能。这确保了与 Salesforce 安全模型的最佳兼容性和效率。
  2. 严格控制密钥安全: 永远不要硬编码或以不安全的方式存储私钥。将私钥安全地存储在 Salesforce 证书中,并通过证书名称引用。此举可以最大限度地降低私钥泄露的风险。
  3. 短生命周期 JWT: 保持 JWT 的过期时间(exp)尽可能短(例如 5-10 分钟),以最大限度地降低令牌被盗用后的风险。如果需要长时间会话,应使用刷新令牌(Refresh Token)机制,而不是延长访问令牌的有效期。
  4. 全面的声明验证: 确保 JWT 的接收方(无论是 Salesforce 还是外部服务)对所有关键声明(iss, sub, aud, exp)进行严格验证,以防止伪造或过期的令牌被接受。任何验证失败都应导致请求被拒绝。
  5. 清晰的集成设计: 在规划集成时,明确 JWT 的角色。对于 Salesforce Connected App,理解 OAuth 2.0 JWT Bearer Flow 的工作原理。对于 Apex Callout,明确哪个是 JWT 的签发者(Issuer)和受众(Audience)。清晰的设计有助于避免配置错误和安全漏洞。
  6. 错误处理与监控: 实现健壮的错误处理机制和详细的日志记录,以便在集成出现问题时能够快速诊断和解决。考虑使用平台事件(Platform Events)或自定义日志对象来捕获和分析集成错误。
  7. 证书生命周期管理: 建立证书的定期审查和更新流程,避免因证书过期导致集成中断。在证书到期前至少提前一个月进行更新和替换计划。
  8. 环境分离: 在开发、测试和生产环境中,使用不同的证书和配置,避免混淆和安全风险。使用自定义元数据类型(Custom Metadata Types)来存储环境特定的配置,例如证书名称、API 端点等。
  9. 考虑使用命名凭证(Named Credentials): 对于从 Salesforce 到外部服务的出站调用,如果外部服务支持 OAuth 2.0 或 API Key 认证,命名凭证(Named Credentials) 是一个更推荐的选择。命名凭证抽象了认证细节,并自动处理授权头部,提供更高的安全性(例如自动处理 OAuth 令牌刷新)和简化的开发体验。然而,如果外部服务明确要求自定义 JWT,那么 Auth.JWTAuth.JWS 仍然是首选。

通过遵循这些指南,您可以在 Salesforce 平台上成功地部署和管理基于 JWT 的安全集成,从而提升系统的互操作性和安全性。


评论

此博客中的热门博文

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

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

Salesforce Data Loader 全方位指南:数据迁移与管理的最佳实践