How to use spring.security.oauth2.client with SOAP calls, initially sent by org.springframework.ws.client.core.WebServiceTemplate?

huangapple go评论77阅读模式
英文:

How to use spring.security.oauth2.client with SOAP calls, initially sent by org.springframework.ws.client.core.WebServiceTemplate?

问题

我们有一个使用 org.springframework.ws.client.core.WebServiceTemplate 进行 SOAP 调用的 Spring Boot 微服务。

现在系统将使用 Keycloak 进行保护,因此所有请求都需要携带身份验证令牌。

如果是 REST API,我只需将预先存在的 RestTemplate 替换为 OAuth2RestTemplate。但如何对最初由 org.springframework.ws.client.core.WebServiceTemplate 进行的调用进行调整呢?

因此,我理解我应该手动添加带有值 'Bearer ....token there...'authentication 标头。我如何手动检索该部分以将其放入请求中?

英文:

We have a Spring Boot microservice that does the SOAP call to the external system using org.springframework.ws.client.core.WebServiceTemplate.

Now the system would be protected with Keycloak, so all the request need to beak the auth token.

If it was a REST API, I would just replace the pre-existed RestTemplate with OAuth2RestTemplate. But how to instrument the calls initially done by the org.springframework.ws.client.core.WebServiceTemplate ?

So, I understand, I should put the authentication header manually with value 'Bearer ....token there...'. How I can retrieve that part manually to put it into the request?

答案1

得分: 0

你可以使用 RequestContextHolder 类获取当前请求令牌,并将其添加到 SOAP 请求头中。

String token = ((ServletRequestAttributes)(RequestContextHolder.getRequestAttributes())).getRequest().getHeader("Authorization");

另外,我建议使用 Web 服务拦截器,而不是在每个 Web 服务请求调用中添加标头。

英文:

you can get current request token using RequestContextHolder class and add into soap request header.

String token = ((ServletRequestAttributes)(RequestContextHolder.getRequestAttributes())).getRequest().getHeader("Authorization");

Also I would suggest use Webservice interecptor instead of adding header in each web service request call.

答案2

得分: 0

以下是翻译好的部分:

问题的原因是:

  1. 现有的库代码基于 org.springframework.ws.client.core.WebServiceTemplate,所以很大并且很难用 WebClient 重写它,兼容 OAuth2 SpringSecurity 或使用已弃用的 OAuth2RestTemplate

  2. 我们之前与之通信的 Web 服务现在受到了 Gravitee 的保护,只接受带有 JWT 令牌的查询。所以,在这里唯一的变化是添加 Authentication 头部,带有 'Bearer ....token there...'

  3. 我们从微服务中的计划任务发起调用。因此,在发出请求之前,它应该从 Keycloak 获取令牌并能够根据时间进行更新。没有人像前端那样明确进行授权,所以 OAuth2 客户端应该使用 client-idclient-secret 连接,没有人工干预。

解决方案

  1. 首先,我们定义了 SOAP 调用的拦截器,它将令牌作为标头传递,通过一个 Supplier<String> 函数将其获取到任何地方:
public class JwtClientInterceptor implements ClientInterceptor {
    private final Supplier<String> jwtToken;

    public JwtClientInterceptor(Supplier<String> jwtToken) {
        this.jwtToken = jwtToken;
    }

    @Override
    public boolean handleRequest(MessageContext messageContext) {
        SoapMessage soapMessage = (SoapMessage) messageContext.getRequest();
        SoapHeader soapHeader = soapMessage.getSoapHeader();
        soapHeader.addHeaderElement(new QName("authorization"))
                .setText(String.format("Bearer %s", jwtToken.get()));
        return true;
    }

    // 其他方法的实现
}
  1. 然后将其传递给模板,以及其他预先存在的拦截器,以在配置类中调用:
protected WebServiceTemplate buildWebServiceTemplate(Jaxb2Marshaller marshaller,
                                                     HttpComponentsMessageSender messageSender, String uri, Supplier<String> jwtToken) {
    WebServiceTemplate template = new WebServiceTemplate();
    template.setMarshaller(marshaller);
    template.setUnmarshaller(marshaller);
    template.setMessageSender(messageSender);
    template.setDefaultUri(uri);
    ClientInterceptor[] clientInterceptors = ArrayUtils.addAll(template.getInterceptors(), new Logger(), new JwtClientInterceptor(jwtToken));

    template.setInterceptors(clientInterceptors);
    return template;
}
  1. 然后添加 Spring Security OAuth2 客户端库:
compile 'org.springframework.security:spring-security-oauth2-client:5.2.1.RELEASE'
  1. 我们创建一个 OAuth2AuthorizedClientService bean,它使用标准的 ClientRegistrationRepository(该存储库通过在 @Configuration 类上使用 @EnableWebSecurity 注解来初始化,但请仔细检查):
@Bean
public OAuth2AuthorizedClientService oAuth2AuthorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {
    return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
}
  1. 然后创建一个 OAuth2AuthorizedClientManager:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {
    // 具体的配置,请参考原文
}
  1. 提供一个供应函数的方法,每次都可以从安全模块(存储库和管理器)中检索 JWT 令牌。这里应该是自动更新的,所以我们只需调用它来检索:
public Supplier<String> getJwtToken() {
    return () -> {
        OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient("keycloak", "we_havePout_realm_there_from_the_properties");
        return authorizedClient.getAccessToken().getTokenValue();
    };
}
  1. 将此 Supplier 传递给 @Bean,定义 WebServiceTemplate
@Bean
public Client client(@Qualifier("Sender1") HttpComponentsMessageSender bnfoMessageSender,
                     @Qualifier("Sender2") HttpComponentsMessageSender uhMessageSender) {
    WebServiceTemplate sender1 = buildWebServiceTemplate(buildSender1Marshaller(), sender1MessageSender, properties.getUriSender1(), getJwtToken());
    WebServiceTemplate sender2 = buildWebServiceTemplate(buildSender2Marshaller(), sender2MessageSender, properties.getUriSender2(), getJwtToken());
    return buildClient(buildRetryTemplate(), sender1, sender2);
}
  1. 我们将 Spring Security 客户端值添加到 application.yaml 中以进行配置。请参考原文中的具体配置信息。

希望这些翻译对你有所帮助。

英文:

The problem was caused by

  1. Existing library code, based on org.springframework.ws.client.core.WebServiceTemplate, so large and huge for rewriting it using WebClient, compatible with OAuth2 SpringSecurity or use depricated OAuth2RestTemplate

  2. The webservice we previously communicated with, turns into protected with Gravitee and accepts queries with JWT tokens only. So, the only change here is to add the Authentication header with &#39;Bearer ....token there...&#39;

  3. We initiate the call from the scheduled jo in the microservice. So, it should be getting token from the Keycloak before the request and be able to update it with time. No one does the explicit authorization like in the frontend, so the OAuth2 client should use client-id and client-secret to connect with no human involved

The Solution

  1. At the beginning, we define the Interceptor to the SOAP calls, that will pass the token as a header, via a Supplier<String> function taking it wherever it can be taken:

     public class JwtClientInterceptor implements ClientInterceptor {
    
     private final Supplier&lt;String&gt; jwtToken;
    
     public JwtClientInterceptor(Supplier&lt;String&gt; jwtToken) {
         this.jwtToken = jwtToken;
     }
    
     @Override
     public boolean handleRequest(MessageContext messageContext) {
         SoapMessage soapMessage = (SoapMessage) messageContext. getRequest();
         SoapHeader soapHeader = soapMessage.getSoapHeader();
         soapHeader.addHeaderElement(new QName(&quot;authorization&quot;))
                 .setText(String. format(&quot;Bearer %s&quot;, jwtToken.get()));
         return true;
     }
    
     @Override
     public boolean handleResponse(MessageContext messageContext) throws WebServiceClientException {
         return true;
     }
    
     @Override
     public boolean handleFault(MessageContext messageContext) throws WebServiceClientException {
         return true;
     }
    
     @Override
     public void afterCompletion(MessageContext messageContext, Exception ex) throws WebServiceClientException {
    
     }
    

    }

  2. Then pass it to the template in addition to other pre-existed interceptor to be called in config class:

     protected WebServiceTemplate     buildWebServiceTemplate(Jaxb2Marshaller marshaller,
                                                              HttpComponentsMessageSender messageSender, String uri,     Supplier&lt;String&gt; jwtToken) {
         WebServiceTemplate template = new WebServiceTemplate();
         template.setMarshaller(marshaller);
         template.setUnmarshaller(marshaller);
         template.setMessageSender(messageSender);
         template.setDefaultUri(uri);
         ClientInterceptor[] clientInterceptors =     ArrayUtils.addAll(template.getInterceptors(), new Logger(), new     JwtClientInterceptor(jwtToken));
    
         template.setInterceptors(clientInterceptors);
         return template;
     }
    
  3. Then add the Spring Security Oath2 Client library

     compile &#39;org.springframework.security:spring-security-oauth2-client:5.2.1.RELEASE&#39;
    
  4. We create OAuth2AuthorizedClientService bean, that uses a standard ClientRegistrationRepository (the repository is initiated through usage of @EnableWebSecurity annotation on the @Configuration class, but please double check about that)

     @Bean
     public OAuth2AuthorizedClientService     oAuth2AuthorizedClientService(ClientRegistrationRepository     clientRegistrationRepository) {
             return  new     InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
        }
    
  5. Then create a OAuth2AuthorizedClientManager

         @Bean
         public OAuth2AuthorizedClientManager authorizedClientManager(
                 ClientRegistrationRepository clientRegistrationRepository,
                 OAuth2AuthorizedClientRepository authorizedClientRepository) {
    
             Authentication authentication = new Authentication()     {
                 @Override
                 public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
                     GrantedAuthority grantedAuthority = new GrantedAuthority() {
                         @Override
                         public String getAuthority() {
                             return &quot;take_a_needed_value_from_property&quot;;
                         }
                     };
                     return Arrays.asList(grantedAuthority);
                 }
    
                 @Override
                 public Object getCredentials() {
                     return null;
                 }
    
                 @Override
                 public Object getDetails() {
                     return null;
                 }
    
                 @Override
                 public Object getPrincipal() {
                     return new Principal() {
                         @Override
                         public String getName() {
                             return &quot;our_client_id_from_properties&quot;;
                     }
                 };
             }
    
             @Override
             public boolean isAuthenticated() {
                 return true;
             }
    
             @Override
             public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
    
             }
    
             @Override
             public String getName() {
                 return &quot;take_a_needed_name_from_properties&quot;;
             }
         };
     //we need to emulate Principal there, as other classes relies on it. In fact, Principal isn&#39;t needed for the app which is a client and just do the call, as nothing is authorized in the app against this Principal itself
    
             OAuth2AuthorizationContext oAuth2AuthorizationContext     =     OAuth2AuthorizationContext.withClientRegistration(clientRegistrationRepository.findByRegistrationId(&quot;keycloak&quot;)).
                     principal(authentication).
                     build();
                 oAuth2AuthorizationContext.getPrincipal().setAuthenticated(true);
             oAuth2AuthorizationContext.getAuthorizedClient();
    
             OAuth2AuthorizedClientProvider authorizedClientProvider =     OAuth2AuthorizedClientProviderBuilder.builder().
             //refreshToken().
             clientCredentials(). //- we use this one according to our set up
             //authorizationCode().
                     build();
    
             OAuth2AuthorizedClientService oAuth2AuthorizedClientService =     oAuth2AuthorizedClientService(clientRegistrationRepository); //use the bean from before step here
    
             AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
                     new AuthorizedClientServiceOAuth2AuthorizedClientManager(
                             clientRegistrationRepository,     oAuth2AuthorizedClientService);
             OAuth2AuthorizedClient oAuth2AuthorizedClient =     authorizedClientProvider.authorize(oAuth2AuthorizationContext);
                 authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
           oAuth2AuthorizedClientService.saveAuthorizedClient(oAuth2AuthorizedClient, 
       oAuth2AuthorizationContext.getPrincipal());
       //this step is needed, as without explicit authorize call, the 
       //oAuth2AuthorizedClient isn&#39;t initialized in the service
    
             return authorizedClientManager;
         }
    
  6. Provide a method for supplied function that can be called each time to retrieve the JWT token from the security stuff (repository and manager). Here it should be auto-updated, so we just call for retrieving it

      public Supplier&lt;String&gt; getJwtToken() {
             return () -&gt; {
                 OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient(&quot;keycloak&quot;, &quot;we_havePout_realm_there_from_the_properties&quot;);
                 return     authorizedClient.getAccessToken().getTokenValue();
             };
         }
    
  7. Pass this Consumer to the @Bean, defining the WebServiceTemplate's

      @Bean
         public Client client(@Qualifier(&quot;Sender1&quot;) HttpComponentsMessageSender bnfoMessageSender,
                              @Qualifier(&quot;Sender2&quot;)     HttpComponentsMessageSender uhMessageSender) {
             WebServiceTemplate sender1= buildWebServiceTemplate(buildSender1Marshaller(),     sender1MessageSender, properties.getUriSender1(),getJwtToken());
             WebServiceTemplate sender2 = buildWebServiceTemplate(buildSender2Marshaller(), sender2MessageSender, properties.getUriSender2(),getJwtToken());
             return buildClient(buildRetryTemplate(), sender1, sender2);
         }
    
  8. We add Spring Security Client values to application.yaml in order to configure it.

     spring:
      security:
         oauth2:
           client:
             provider:
               keycloak:
                 issuer-uri: https://host/keycloak/auth/realms/ourrealm
             registration:
               keycloak:
                 client-id: client_id
                 client-secret: client-secret-here
                 authorization-grant-type: client_credentials     //need to add explicitly, otherwise would try other grant-type by default and never get the token!
                 client-authentication-method: post //need to have this explicitly, otherwise use basic that doesn&#39;t fit best the keycloak set up
                 scope: openid //if your don&#39;t have it, it checks all available scopes on url like https://host/keycloak/auth/realms/ourrealm/ .well-known/openid-configuration keycloak and then sends them as value of parameter named &#39;scope&#39; in the query for retrieving the token; that works wrong on our keycloak, so to replace this auto-picked value, we place the explicit scopes list here
    

huangapple
  • 本文由 发表于 2023年2月8日 22:09:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/75386966.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定