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