这是我的第一篇文章,如果我错过任何标准的 SOF 实践,抱歉。如果我需要提供任何额外信息或者我提供的信息太多,请告诉我。
我一直在关注 Springboot 3 迁移文档以及此处的博客文章:https://davidagood.com/oauth-client-credentials-auto-refresh-spring/
从 Springboot 2.6 升级到 3.2 后,我发现 RestTemplate 现已被弃用,我切换到 WebClient,希望让我的 Springboot 应用程序按照我想要的方式运行。
就上下文而言,我使用的是 Springboot 3.2 Spring Security 6 Java 17 。我的应用程序是一个代理,它有一堆 API 端点,可以调用 Salesforce API。在从 RestTemplate 升级到 WebClient 之前,请求工作正常,但是,现在升级后,每个 API 调用都会收到多个身份验证调用,而不是存储会话并重用一个访问令牌,仅在其过期时刷新它。当我记录这些请求时,看起来它们没有进行身份验证,因此没有存储身份验证?我从我的请求中看到的“AnonymousRole”中收集到了这一点。
我认为 Salesforce 端的资源服务器不是问题,因为当我向同一 URL 发出邮递员请求时,我会返回正确的访问令牌:
请求
curl -X POST -H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=secret-placeholder&client_secret=secret-placeholder" \ "https://my.salesforce.com/services/oauth2/token"
回复
{"access_token":"{TOKEN_VALUE}","signature":"{SIGNATURE_VALUE}","scope":"api id","instance_url":"https://my.salesforce.com","id":"https://test.salesforce.com/id/{ID_VALUE}","token_type":"Bearer","issued_at":"{EPOCH_VALUE}"}
我相信博客“依赖 HTTP 会话来维护跨多个请求的上下文”中解决了这个“AnonymousUser”问题的可能原因。因此,我按照建议使用了“AuthorizedClientServiceOAuth2AuthorizedClientManager”来解决这个问题,但是不幸的是我仍然遇到同样的问题!我环顾四周,每个资源似乎都指向我所遵循的相同步骤,所以我在这一点上真的迷失了方向,我的高级工程师也无法弄清楚。
希望能得到一些帮助。非常感谢!
这是使用“.permitAll()”时的一些日志:
2024-01-29T15:59:11.615-08:00 INFO 53077 --- [crm-proxy] [oundedElastic-1] [ ] c.s.c.c.s.c.SalesforceWebClientFactory : Authorization successful for clientRegistrationId=salesforce, tokenUri=https://my.salesforce.com/services/oauth2/token
2024-01-29T15:59:12.610-08:00 DEBUG 53077 --- [crm-proxy] [nio-8080-exec-1] [65b83bcea50539ad048191ae13ee2d26-a244a0c27369d291] c.s.c.c.s.client.SalesforceClient : took 2096ms to successfully GET to 'https://my.salesforce.com/services/data/v52.0/query?
2024-01-29T15:59:12.615-08:00 DEBUG 53077 --- [crm-proxy] [nio-8080-exec-1] [65b83bcea50539ad048191ae13ee2d26-a244a0c27369d291] c.s.c.c.s.client.SalesforceClient : Security Context: SecurityContextImpl [Authentication=AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]]]
这是旧的2.6代码:
@Component
public class SalesforceOAuth2RestTemplateFactory {
public RestOperations generateOAuth2RestTemplate(SalesforceProperties salesforceProperties) {
ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
resource.setAccessTokenUri(salesforceProperties.getAccessTokenUri());
resource.setClientId(salesforceProperties.getClientId());
resource.setAuthenticationScheme(AuthenticationScheme.header);
resource.setClientAuthenticationScheme(AuthenticationScheme.form);
resource.setClientSecret(salesforceProperties.getClientSecret());
resource.setGrantType("password");
resource.setUsername(salesforceProperties.getUsername());
resource.setPassword(salesforceProperties.getPassword());
OAuth2RestTemplate oAuth2RestTemplate =
new OAuth2RestTemplate(
resource, new DefaultOAuth2ClientContext(new DefaultAccessTokenRequest()));
oAuth2RestTemplate.setRetryBadAccessTokens(true);
oAuth2RestTemplate.setErrorHandler(new SalesforceOAuth2ErrorHandler(resource));
return oAuth2RestTemplate;
}
这是我当前的3.2代码:
(活动资料:本地) (环境名称:dev01)
我已在此在线剪贴板链接中粘贴了以下类,因为如果我尝试将代码粘贴到此处,我的帖子会被标记为垃圾邮件,抱歉:SalesforceWebClientFactory.java、SalesforceClient.java、UserController.java、application.yaml、build。 gradle.kts:
我注意到您来自 Salesforce API 的访问令牌响应不提供
expires_in
字段,仅提供 issued_at
字段。
这是预期行为,因为根据规范推荐使用
expires_in
:https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
深入研究 Spring Security 代码,发现如果缺少
expires_in
字段,则过期时间将默认为 issued_at
时间之后的 1 秒。因此,导致您看到访问令牌不断刷新的问题,即使它不一定无效。
要解决此问题,您需要通过重写
getTokenResponse()
来添加自定义令牌处理,实现如下所示:
public class CustomClientCredentialsTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
private final DefaultClientCredentialsTokenResponseClient delegate =
new DefaultClientCredentialsTokenResponseClient();
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) throws OAuth2AuthenticationException {
var response = delegate.getTokenResponse(clientCredentialsGrantRequest);
// expiration time of 1 hour (3600 seconds)
long defaultExpiresInSeconds = 3600L;
Instant now = Instant.now();
// here is where you update the expiration
Instant expiresAt = now.plusSeconds(defaultExpiresInSeconds);
// rebuild the OAuth2AccessToken with the new expiration time
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
response.getAccessToken().getTokenValue(), now, expiresAt, response.getAccessToken().getScopes());
return OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
.tokenType(accessToken.getTokenType())
.expiresIn(defaultExpiresInSeconds)
.scopes(accessToken.getScopes())
.additionalParameters(response.getAdditionalParameters())
.build();
}
}
然后确保将此处理程序添加到现有的
authorizedClientManager
中,如下所示:
public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService) {
…
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder().clientCredentials(clientCredentialsGrantBuilder ->
clientCredentialsGrantBuilder.accessTokenResponseClient(
// here is where you reference your custom token handling
new
CustomClientCredentialsTokenResponseClient()))
.build();
}
这应该可以解决问题。您拥有的其他所有内容看起来都很好,并且此方法可以与
WebClient
或 RestClient
实现一起使用,因为两者都使用相同的 authorizedClientManager
。让我知道这是否有效,希望有帮助。 :)