Problem with GCP Secret Manager and Spring Boot app - spring-boot

Spring Boot (2.5.9) has problem to access password from the GCP Secret Manager using spring-cloud-gcp-starter-secretmanager ver 2.0.8 throwing error
AnnotationConfigApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'defaultFeignClientConfiguration': Injection of autowired dependencies failed; nested exception is org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [com.google.protobuf.ByteString$LiteralByteString] to type [java.lang.String]
for a passsword declared in the application.properties as
webservices.security.basic.user.password=${sm://my-password}
when I will replace it with regular string or even env variable it will work fine.
Failing part of the code looks like:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import feign.Retryer;
import feign.auth.BasicAuthRequestInterceptor;
import feign.codec.ErrorDecoder;
/**
* Default feign client configuration. Includes retry policies, basic auth user name and password, and HTTP status decoder.
* #author Greg Meyer
* #since 6.0
*/
public class DefaultFeignClientConfiguration
{
#Value("${webservices.retry.backoff.multiplier:3}")
protected double backoffMultiplier;
#Value("${webservices.retry.backoff.initialBackoffInterval:100}")
protected long initialBackoffInterval;
#Value("${webservices.retry.backoff.maxInterval:20000}")
protected long maxInterval;
#Value("${webservices.security.basic.user.name:}")
protected String user;
#Value("${webservices.security.basic.user.password:}")
protected String pass;
/**
* Creates an instance of the a the default HTTP status translator.
* #return An instance of the a the default HTTP status translator
*/
#Bean
public ErrorDecoder feignClientErrorDecoder()
{
return new DefaultErrorDecoder();
}
/**
* Creates an instance of BasicAuth interceptor configured with a username and password. This bean is only created if the
* "webservices.security.basic.user.name" property is set.
* #return An instance of BasicAuth interceptor configured with a username and password
*/
#Bean
#ConditionalOnProperty(name="webservices.security.basic.user.name", matchIfMissing=false)
public BasicAuthRequestInterceptor basicAuthRequestInterceptor()
{
return new BasicAuthRequestInterceptor(user, pass);
}
/**
* Creates an instance of a back off policy used in conjuntion with the retry policy.
* #return An instance of a back off policy
*/
#Bean
public LoadBalancedRetryFactory backOffPolciyFactory()
{
return new LoadBalancedRetryFactory()
{
#Override
public BackOffPolicy createBackOffPolicy(String service)
{
final ExponentialBackOffPolicy backoffPolicy = new ExponentialBackOffPolicy();
backoffPolicy.setMultiplier(backoffMultiplier);
backoffPolicy.setInitialInterval(initialBackoffInterval);
backoffPolicy.setMaxInterval(maxInterval);
return backoffPolicy;
}
};
}
/**
* Creates a default http retry policy.
* #return A default http retry policy.
*/
#Bean
public Retryer retryer()
{
/*
* Default retryer config
*/
return new Retryer.Default(200, 1000, 5);
}
}
Any thoughts?

The problem is that most likely, Feign autoconfiguration happens early on, before GcpSecretManagerEnvironmentPostProcessor had a chance to run and introduce ByteString converters.
Basically, the solution that works is to implement a Custom Converter which implements the Generic Conversion Service and register the Converter in the main method before calling SpringApplication.run.
public static void main(String[] args)
{
((DefaultConversionService)DefaultConversionService.getSharedInstance()).addConverter(new CustomConverter());
SpringApplication.run(STAApplication.class, args);
}
and custom converter:
#Component
public class CustomConverter implements GenericConverter {
#Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(ByteString.class, String.class));
}
#Override
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
if (sourceType.getType() == String.class) {
return source;
}
try {
source = ((ByteString) source).toString("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return source;
}
}

Related

Why JwtAccessTokenConverter - Unable to create an RSA verifier from verifierKey (ignoreable if using MAC) when start up a microservice server?

Description:
When the Bet microservice server starts, it never manages to create an RSA verifier because its value is null. I'm developing a microservices-based architecture using Oauth2, taking as a guide the microservices architecture developed by Jhipster:
UAA server. It expose public key to verify JWT signature.
Eureka server.
Cloud Config Server.
Bet microservice server. On start up try to retrieve the public key
to verify signed JWT.
Euraka configuration for each server:
Eureka server:
eureka:
client:
fetch-registry: false
register-with-eureka: false
instance-info-replication-interval-seconds: 10
registry-fetch-interval-seconds: 10
service-url:
defaultZone: http://admin:${spring.security.user.password:admin}#${eureka.instance.hostname}:${server.port}/eureka/
instance:
hostname: localhost
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 10
UAA server:
eureka:
client:
service-url:
defaultZone: http://admin:eureka#localhost:8761/eureka/
instance-info-replication-interval-seconds: 10
registry-fetch-interval-seconds: 10
instance:
appname: uaa
instanceId: uaa:${spring.application.instance-id:${random.value}}
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 10
Microservice Bet:
eureka:
client:
service-url:
defaultZone: http://admin:eureka#localhost:8761/eureka/
instance-info-replication-interval-seconds: 10
registry-fetch-interval-seconds: 10
instance:
appname: bets
instanceId: bets:${spring.application.instance-id:${random.value}}
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 10
When trying to consume a service published by Bet microservice: http://localhost:8083/api/markets, it returns the following response:
{
"error": "invalid_token",
"error_description": "public key expired"
}
status: 401 Unauthorize
Tracing the code I discovered that in class org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter the attribute this.verifier is null inside the afterPropertiesSet () method throwing an Exception (logger.warn ("Unable to create an RSA verifier from verifierKey (ignoreable if using MAC) "))
because I couldn't start the new RsaVerifier object (this.verifierKey)
I thought it was due to the delay time of registering the microservice with the eureka server, so I waited more than 5 min and checked via Postman client that the public key was available and even so the Bet microservice could not create RSA verifier.
So,
Why the Bet microservice at startup failed to create RSA verifier?
Could it be that the public key could not be received when starting
the microservice that is not yet registered with eureka server?
Finally i found the issue.
I must call the tryCreateSignatureVerifier() method when the OAuth2JwtAccessTokenConverter class initialize(in constructor) and to try to retrieve the public key and create RSA verifier
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
public class OAuth2JwtAccessTokenConverter extends JwtAccessTokenConverter {
private final AppProperties properties;
private final OAuth2SignatureVerifierClient signatureVerifierClient;
/**
* When did we last fetch the public key?
*/
private long lastKeyFetchTimestamp;
public OAuth2JwtAccessTokenConverter(OAuth2SignatureVerifierClient signatureVerifierClient, AppProperties properties) {
this.properties = properties;
this.signatureVerifierClient = signatureVerifierClient;
//NOTE, it is very important to try fetch for the public key to verify the JWT signature
tryCreateSignatureVerifier();
}
/**
* Fetch a new public key from the AuthorizationServer.
*
* #return true, if we could fetch it; false, if we could not.
*/
private boolean tryCreateSignatureVerifier() {
long t = System.currentTimeMillis();
if (t - lastKeyFetchTimestamp < properties.getSignatureVerification().getPublicKeyRefreshRateLimit()) {
return false;
}
try {
SignatureVerifier verifier = signatureVerifierClient.getSignatureVerifier();
if (verifier != null) {
setVerifier(verifier);
lastKeyFetchTimestamp = t;
log.debug("Public key retrieved from OAuth2 server to create SignatureVerifier");
return true;
}
} catch (Throwable ex) {
log.error("Could not get public key from OAuth2 server to create SignatureVerifier", ex);
}
return false;
}
/**
* Try to decode the token with the current public key.
* If it fails, contact the OAuth2 server to get a new public key, then try again.
* We might not have fetched it in the first place or it might have changed.
*
* #param token the JWT token to decode.
* #return the resulting claims.
* #throws InvalidTokenException if we cannot decode the token.
*/
#Override
protected Map<String, Object> decode(String token) {
try {
//check if our public key and thus SignatureVerifier have expired
long ltt = properties.getSignatureVerification().getTtl();
if (ltt > 0 && System.currentTimeMillis() - lastKeyFetchTimestamp > ltt) {
throw new InvalidTokenException("public key expired");
}
return super.decode(token);
} catch (InvalidTokenException e) {
if (tryCreateSignatureVerifier()) {
super.decode(token);
}
throw e;
}
}
/**
* Extract JWT claims and set it to OAuth2Authentication decoded details.
* Here is how to get details:
* #param claims OAuth2JWTToken claims.
* #return {#link OAuth2Authentication}.
*/
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
OAuth2Authentication authentication = super.extractAuthentication(claims);
authentication.setDetails(claims);
return authentication;
}
Abstracts how to create a SignatureVerifier to verify JWT tokens with a public key
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
/**
* Abstracts how to create a {#link SignatureVerifier} to verify JWT tokens with a public key.
* Implementations will have to contact the OAuth2 authorization server to fetch the public key
* and use it to build a {#link SignatureVerifier} in a server specific way.
*
* #see com.example.bets.config.oauth2.OAuth2UaaSignatureVerifierClient
*/
public interface OAuth2SignatureVerifierClient {
/**
* Returns the {#link SignatureVerifier} used to verify JWT tokens.
* Fetches the public key from the Authorization server to create
* this verifier.
*
* #return the new verifier used to verify JWT signatures.
* Will be null if we cannot contact the token endpoint.
* #throws Exception if we could not create a {#link SignatureVerifier} or contact the token endpoint.
*/
SignatureVerifier getSignatureVerifier() throws Exception;
}
Implementation of OAuth2SignatureVerifierClient
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
/**
* Client fetching the public key from UAA to create a {#link SignatureVerifier}.
*/
#Slf4j
#Component
public class OAuth2UaaSignatureVerifierClient implements OAuth2SignatureVerifierClient {
private final AppProperties properties;
private final RestTemplate restTemplate;
public OAuth2UaaSignatureVerifierClient(DiscoveryClient discoveryClient,
AppProperties properties,
#Qualifier("loadBalancedRestTemplate") RestTemplate restTemplate) {
this.properties = properties;
this.restTemplate = restTemplate;
// Load available UAA servers
discoveryClient.getServices();
}
/**
* Fetches the public key from the UAA.
*
* #return the public key used to verify JWT tokens; or {#code null}.
*/
#Override
public SignatureVerifier getSignatureVerifier() {
try {
HttpEntity<Void> request = new HttpEntity<Void>(new HttpHeaders());
String key = (String) restTemplate
.exchange(getPublicKeyEndpoint(), HttpMethod.GET,request, Map.class)
.getBody()
.get("value");
return new RsaVerifier(key);
} catch (IllegalStateException ex) {
log.warn("could not contact UAA to get public key");
return null;
}
}
/**
* Returns the configured endpoint URI to retrieve the public key.
*
* #return the configured endpoint URI to retrieve the public key.
*/
private String getPublicKeyEndpoint() {
String tokenEndpointUrl = properties.getSignatureVerification().getPublicKeyEndpointUri();
if (tokenEndpointUrl == null) {
throw new InvalidClientException("no token endpoint configured in application properties");
}
return tokenEndpointUrl;
}
}
Finally Class SecurityConfiguration extends ResourceServerConfigurerAdapter
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends ResourceServerConfigurerAdapter {
private final AppProperties properties;
public SecurityConfiguration(AppProperties properties) {
this.properties = properties;
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/prometheus").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN);
}
/**
* Apply the token converter (and enhancer) for token store.
*
* #return the {#link JwtTokenStore} managing the tokens.
*/
#Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter){
return new JwtTokenStore(jwtAccessTokenConverter);
}
/**
* This bean generates an token enhancer, which manages the exchange between JWT access tokens and Authentication
* in both directions.
*
* #return an access token converter configured with the fetched public key of the authorization server.
*/
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(OAuth2SignatureVerifierClient signatureVerifierClient) {
return new OAuth2JwtAccessTokenConverter(properties, signatureVerifierClient);
}
}
Maven Dependecies
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR4</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Error creating bean with name 'scopedTarget.oauth2ClientContext': Scope 'request' is not active for the current thread for feign client

I am calling another microservice once my current microservice is up and ready using feign client in my current microservice built using Jhipster.
So my Feign Interface is
package com.persistent.integration.client;
import java.util.List;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import com.persistent.integration.service.dto.DataPipelineDTO;
#AuthorizedFeignClient(name = "Integrationconfiguration")
public interface DataPipelinesResourceFeign {
#RequestMapping(value = "/api/data-pipelines", method = RequestMethod.GET)
List<DataPipelineDTO> getAllDataPipelines(#RequestParam(value = "pageable") Pageable pageable );
}
}
And I have implemented ApplicationRunner where I have called feign client method.
#Component
public class ApplicationInitializer implements ApplicationRunner {
#Autowired
private DataPipelinesResourceFeign dataPipelinesResourceFeign;
#Autowired
private ActiveMQListener activeMqListener;
#Override
public void run(ApplicationArguments args) throws Exception {
// TODO Auto-generated method stub
Pageable pageable = PageRequest.of(0, 20);
try {
List <DataPipelineDTO> allStartedDataPipeLines = dataPipelinesResourceFeign.getAllDataPipelines(pageable); //.stream().filter(p->p.getState().equals(State.STARTED)).collect(Collectors.toList());
allStartedDataPipeLines.forEach(datapipe ->
{
try {
activeMqListener.consume(datapipe);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
But after running this, it gives below exception at dataPipelinesResourceFeign.getAllDataPipelines :
com.netflix.hystrix.exception.HystrixRuntimeException: DataPipelinesResourceFeign#getAllDataPipelines(Pageable) failed and no fallback available.
at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:819)
at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:804)
at rx.internal.operators.OperatorOnErrorResumeNextViaFunction$4.onError(OperatorOnErrorResumeNextViaFunction.java:140)
at rx.internal.operators.OnSubscribeDoOnEach$DoOnEachSubscriber.onError(OnSubscribeDoOnEach.java:87)
at rx.internal.operators.OnSubscribeDoOnEach$DoOnEachSubscriber.onError(OnSubscribeDoOnEach.java:87)
at com.netflix.hystrix.AbstractCommand$DeprecatedOnFallbackHookApplication$1.onError(AbstractCommand.java:1472)
Caused by: org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'scopedTarget.oauth2ClientContext':
Scope 'request' is not active for the current thread; consider
defining a scoped proxy for this bean if you intend to refer to it
from a singleton; nested exception is java.lang.IllegalStateException:
No thread-bound request found: Are you referring to request attributes
outside of an actual web request, or processing a request outside of
the originally receiving thread? If you are actually operating within
a web request and still receive this message, your code is probably
running outside of DispatcherServlet/DispatcherPortlet: In this case,
use RequestContextListener or RequestContextFilter to expose the
current request. at
org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(Abstrac>tBeanFactory.java:362)
at
org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractB>eanFactory.java:199)
at
org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTarge>tSource.java:35)
at
org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.>java:193)
at com.sun.proxy.$Proxy147.getAccessToken(Unknown Source) at
com.persistent.integration.security.oauth2.AuthorizationHeaderUtil.getAuthoriza>tionHeaderFromOAuth2Context(AuthorizationHeaderUtil.java:28)
at
com.persistent.integration.client.TokenRelayRequestInterceptor.apply(TokenRelay>RequestInterceptor.java:23)
at
feign.SynchronousMethodHandler.targetRequest(SynchronousMethodHandler.java:158)
at
feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:88)
at
feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:76)
at
feign.hystrix.HystrixInvocationHandler$1.run(HystrixInvocationHandler.java:108)
at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:302)
at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:298)
at
rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:46)
... 68 more Caused by: java.lang.IllegalStateException: No
thread-bound request found: Are you referring to request attributes
outside of an actual web request, or processing a request outside of
the originally receiving thread? If you are actually operating within
a web request and still receive this message, your code is probably
running outside of DispatcherServlet/DispatcherPortlet: In this case,
use RequestContextListener or RequestContextFilter to expose the
current request. at
org.springframework.web.context.request.RequestContextHolder.currentRequestAttr>ibutes(RequestContextHolder.java:131)
at
org.springframework.web.context.request.AbstractRequestAttributesScope.get(Abst>ractRequestAttributesScope.java:42)
at
org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(Abstrac>tBeanFactory.java:350)
many suggestions on internet were to add listerner RequestContextListener. But problem persisted even if I added listener in webConfigurer.java in onStartup method.
{
servletContext.addListener(RequestContextListener.class);
}
But of no use.
Any leads would be appreciated.
I found a workaround for this. I don't know why TokenRelayRequestIntercepton isn't working but you can use your own RequestInterceptor based on Spring's SecurityContext.
First, define a RequestInterceptor :
public class MyRequestInterceptor implements RequestInterceptor {
public static final String AUTHORIZATION = "Authorization";
public static final String BEARER = "Bearer";
public MyRequestInterceptor() {
super();
}
#Override
public void apply(RequestTemplate template) {
// demander un token à keycloak et le joindre à la request
Optional<String> header = getAuthorizationHeader();
if (header.isPresent()) {
template.header(AUTHORIZATION, header.get());
}
}
public static Optional<String> getAuthorizationHeader() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getDetails() != null && authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails oAuth2AuthenticationDetails =
(OAuth2AuthenticationDetails) authentication.getDetails();
return Optional.of(String.format("%s %s", oAuth2AuthenticationDetails.getTokenType(),
oAuth2AuthenticationDetails.getTokenValue()));
} else {
return Optional.empty();
}
}
}
and then, declare a config class for your feign client using your RequestInterceptor, it should contains something like this :
#Bean(name = "myRequestInterceptor")
public RequestInterceptor getMyRequestInterceptor() throws IOException {
return new MyRequestInterceptor();
}
Your Feign client shoud look like this:
#FeignClient(name = "SERVICE_NAME", configuration = MyFeignConfiguration.class)
public interface MyRestClient {
I had the same issue with Feign Client running on startup using ApplicationRunner and I came up with following solution.
I defined my FeignClientsConfiguration with OAuth2FeignRequestInterceptor, which accepts predefined bean DefaultOAuth2ClientContext and OAuth2 configuration OAuth2ProtectedResourceDetails:
#Configuration
public class MyConfig extends FeignClientsConfiguration {
#Bean
public RequestInterceptor oauth2FeignRequestInterceptor( DefaultOAuth2ClientContext oAuth2ClientContext, MyOauth2Properties properties) {
return new OAuth2FeignRequestInterceptor(oAuth2ClientContext, resourceDetails(properties));
}
#Bean
public DefaultOAuth2ClientContext oAuth2ClientContext() {
return new DefaultOAuth2ClientContext();
}
private OAuth2ProtectedResourceDetails resourceDetails(MyOauth2Properties oauth2Properties) {
ResourceOwnerPasswordResourceDetails resourceDetails = new ResourceOwnerPasswordResourceDetails();
resourceDetails.setAccessTokenUri(oauth2Properties.getAccessTokenUri());
resourceDetails.setUsername(oauth2Properties.getUsername());
resourceDetails.setPassword(oauth2Properties.getPassword());
resourceDetails.setClientId(oauth2Properties.getClientId());
return resourceDetails;
}
}
Your feign client will look something like this:
#FeignClient(url = "http://localhost:8080/api/v1")
public interface FeignClient {
}
After all this, calling FeignClient from ApplicationRunner.run() works fine.
Spring Boot 2.2.6

SOAP Proxy with Spring Integration

I'm trying to wrap my head around spring integration and would preferably like to stay away from XML-based configuration.
What I would like to do is the following:
Accept a SOAP request on a certain endpoint
Forward this request to a downstream SOAP service, without any modification (for now I just want the basics to work)
Return the result to the client
This seems like it should be pretty simple, but I don't have the faintest idea of where to start. Is this even possible with SI? As I understand it, and please correct me if I'm wrong, SI is mainly used for async data flows.
I did check out the integration sample repository, which includes example inbound WS requests, but these are all configured in XML, which, as I said, I'd preferably like to stay far away from.
Any pointers would be much appreciated; I've been reading through documentation for the past two days and I'm none the wiser!
Here is an example using the SimpleWebServiceInboundGateway. In this example we also set the "ExtractPayload" to false so that it sends the RAW soap message. But agree with above, possibly the HTTPInboundRequest is better for your use case. I also didn't find many examples using DSL for the SoapInboundGateway so wanted to share and hope it helps someone else.
#Configuration
#EnableIntegration
public class SoapGatewayConfiguration {
/**
* URL mappings used by WS endpoints
*/
public static final String[] WS_URL_MAPPINGS = {"/services/*", "*.wsdl", "*.xsd"};
public static final String GATEWAY_INBOUND_CHANNEL_NAME = "wsGatewayInboundChannel";
public static final String GATEWAY_OUTBOUND_CHANNEL_NAME = "wsGatewayOutboundChannel";
/**
* Register the servlet mapper, note that it uses MessageDispatcher
*/
#Bean
public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(applicationContext);
servlet.setTransformWsdlLocations(true);
servlet.setTransformSchemaLocations(true);
servlet.setPublishEvents(true);
ServletRegistrationBean servletDef = new ServletRegistrationBean(servlet, WS_URL_MAPPINGS);
servletDef.setLoadOnStartup(1);
return servletDef;
}
/**
* Create a new Direct channels to handle the messages
*/
#Bean
public MessageChannel wsGatewayInboundChannel() {
return MessageChannels.direct(GATEWAY_INBOUND_CHANNEL_NAME).get();
}
#Bean
public MessageChannel wsGatewayOutboundChannel() {
return MessageChannels.direct(GATEWAY_OUTBOUND_CHANNEL_NAME).get();
}
/**
* Startup the WebServiceInboundGateway Endpoint, this will handle the incoming SOAP requests
* and place them onto the request channel
*/
#Bean
public SimpleWebServiceInboundGateway webServiceInboundGateway(
#Value("${spring.ws.request.timeout:1000}") long requestTimeout,
#Value("${spring.ws.reply.timeout:1000}") long replyTimeout,
#Value("${spring.ws.should.track:true}") boolean shouldTrack
) {
SimpleWebServiceInboundGateway wsg = new SimpleWebServiceInboundGateway();
wsg.setRequestChannel(wsGatewayInboundChannel());
wsg.setReplyChannel(wsGatewayOutboundChannel());
wsg.setExtractPayload(false); // Send the full RAW SOAPMessage and not just payload
wsg.setLoggingEnabled(true);
wsg.setShouldTrack(shouldTrack);
wsg.setReplyTimeout(replyTimeout); // Do not believe this prop supported currently
wsg.setRequestTimeout(requestTimeout); // Do not believe this prop is supported currently
wsg.setCountsEnabled(true);
return wsg;
}
/**
* You must enable debug logging on org.springframework.ws.server.endpoint.interceptor.PayloadLoggingInterceptor
* to see the logs from this interceptor
*/
#Bean
public EndpointInterceptor soapMessageLoggingInterceptor() {
SoapEnvelopeLoggingInterceptor li = new SoapEnvelopeLoggingInterceptor();
li.setLogRequest(true);
li.setLogResponse(true);
li.setLogFault(true);
return li;
}
/**
* Validate the incoming web service against the schema
*/
#Bean
public EndpointInterceptor payloadValidatingInterceptor(XsdSchema xsdSchema
, #Value("${spring.ws.soap.validate.request:true}") boolean soapValidateRequest
, #Value("${spring.ws.soap.validate.reply:true}") boolean soapValidateResponse
, #Value("${spring.ws.soap.validate.addErrorDetail:true}") boolean soapAddValidationErrorDetail
) {
PayloadValidatingInterceptor interceptor = new PayloadValidatingInterceptor();
interceptor.setXsdSchema(xsdSchema);
interceptor.setValidateRequest(soapValidateRequest);
interceptor.setValidateResponse(soapValidateResponse);
interceptor.setAddValidationErrorDetail(soapAddValidationErrorDetail);
return interceptor;
}
/**
* Map the allowable service Uri's.
*/
#Bean
public EndpointMapping uriEndpointMapping(
PayloadValidatingInterceptor payloadValidatingInterceptor
, SimpleWebServiceInboundGateway webServiceInboundGateway
, SoapEnvelopeLoggingInterceptor loggingInterceptor) {
UriEndpointMapping mapping = new UriEndpointMapping();
mapping.setUsePath(true);
mapping.setDefaultEndpoint(webServiceInboundGateway);
mapping.setInterceptors(new EndpointInterceptor[]{loggingInterceptor, payloadValidatingInterceptor});
return mapping;
}
/**
* Expose the wsdl at http://localhost:8080/services/myService.wsdl
**/
#Bean
public Wsdl11Definition myService() {
SimpleWsdl11Definition wsdl11Definition = new SimpleWsdl11Definition();
wsdl11Definition.setWsdl(new ClassPathResource("META-INF/myService.wsdl"));
return wsdl11Definition;
}
/**
* Expose the xsd at http://localhost:8080/services/mySchema.xsd
**/
#Bean
public XsdSchema mySchema() {
return new SimpleXsdSchema(new ClassPathResource("META-INF/mySchema.xsd"));
}
#Bean
public IntegrationFlow itemLookupFlow() {
return IntegrationFlows.from("wsGatewayInboundChannel")
.log(LoggingHandler.Level.INFO)
.handle(myBeanName, "execute")
.log(LoggingHandler.Level.TRACE, "afterExecute")
.get();
}
}
If your application is just a proxy over other SOAP service, you should consider to use just plain HTTP Inbound Gateway and HTTP Outbound Gateway.
You receive an XML from client and send it into the downstream service. Receive from there an XML again and just push it back to the response for the client.
For this purpose I can suggest HTTP proxy solution via Java DSL:
#Bean
public IntegrationFlow httpProxyFlow() {
return IntegrationFlows
.from(Http.inboundGateway("/service"))
.handle(Http.outboundGateway("/service/internal")
.expectedResponseType(String.class))
.get();
}
The problem with the SimpleWebServiceInboundGateway and SimpleWebServiceOutboundGateway pair that they extract a request and parse a respose to (un)wrap to/from the SOAP envelop. This looks like an overhead for your plain proxy use-case.
I got it working thanks to Artem's answer, with a small tweak. Not sure as to why the channels are required, but at least it's now working.
package com.example.integration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.http.Http;
import org.springframework.integration.http.config.EnableIntegrationGraphController;
import org.springframework.messaging.MessageChannel;
#SpringBootApplication
#EnableIntegration
#EnableIntegrationGraphController(allowedOrigins = "*")
public class IntegrationApplication {
public static void main(String[] args) {
SpringApplication.run(IntegrationApplication.class, args);
}
#Bean
public DirectChannel directChannel() {
return new DirectChannel();
}
#Bean
public IntegrationFlow httpProxyFlow(MessageChannel directChannel) {
return IntegrationFlows
.from(Http.inboundGateway("/thing").requestChannel(directChannel).replyChannel(directChannel))
.enrichHeaders(h -> h.header("Content-Type", "application/soap+xml; charset=utf-8"))
.handle(Http.outboundGateway("http://www.webservicex.net/geoipservice.asmx").expectedResponseType(String.class))
.channel(directChannel)
.get();
}
}

Autowiring a spring managed bean in the resteasy providers

I have a developing a webservice where in i need to validate a particular httpheader sent in the request against the database.
I want to use RestEasy provider for doing the same as the same functionality needs to be applied to all the service.
#Provider
#ServerInterceptor
public class TestValidationInterceptor implements PreProcessInterceptor {
#Autowired
DetailsDelegate detailsDelegate;
#Override
public ServerResponse preProcess(HttpRequest request, ResourceMethod method) throws Failure, WebApplicationException {
//code to get data using detailsDelegate.
return null;
}
}
public interface DetailsDelegate {
String BEAN_ID = "DetailsDelegate";
/**
* #param appName
* #return
* #throws BaseException
*/
ServiceInfo getServiceDetails(String appName) throws BaseException;
}
#Service("detailsDelegate")
public class DetailsDelegateImpl implements DetailsDelegate {
#Override
public ServiceInfo getServiceDetails(String appName) throws BaseException {
return null;
}
}
The detailsDelegate instance is not getting autowired and is null.
Can someone explain why I am facing this issue.
It's best to let spring chose it's bean names so change
#Service("detailsDelegate")
to
#Service
The autowire the interface :
#Auowired
DetailsDelegate
Finally make sure the package in which DetailsDelegate is defined is scanned in your config:
#ComponentScan("com.mypackage")
See http://docs.spring.io/spring-javaconfig/docs/1.0.0.M4/reference/html/ch06s02.html for examples

ClassCastException: javax.naming.Reference cannot be cast to javax.jms.ConnectionFactory

I wrote a Java program to connect to Websphere MQ to publish messages. I created a JNDI namespace, connection factory, destinations, and queue manager in Websphere MQ Explore. When I am running my program it is showing ClassCastException for type casting from string to ConnectionFactory.
Here is my code. Can anyone help resolve this problem.
JNDIUtil.java
package com.tradefinance.jms.util;
//JMS classes
import javax.jms.JMSException;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
//JNDI classes
import javax.naming.InitialContext;
import javax.naming.Context;
import javax.naming.NamingException;
//Standard Java classes
import java.util.Hashtable;
import java.util.Properties;
/**
*
* A wrapper class for JNDI calls
*
*/
public class JNDIUtil
{
private Context context;
public JNDIUtil(String icf, String url) throws JMSException, NamingException
{
Hashtable environment = new Hashtable();
environment.put(Context.INITIAL_CONTEXT_FACTORY, icf );
environment.put(Context.PROVIDER_URL, url);
context= new InitialContext( environment );
}
/**
* #param ObjName Object Name to be retrieved
* #return Retrieved Object
* #throws NamingException
*/
private Object getObjectByName(String ObjName) throws NamingException
{
return context.lookup(ObjName);
}
/**
* #param factoryName Factory Name
* #return ConnectionFactory object
* #throws NamingException
*/
public ConnectionFactory getConnectionFactory(String factoryName) throws NamingException
{
return (ConnectionFactory) getObjectByName(factoryName);
}
/**
* #param destinationName Destination Name
* #return ConnectionFactory object
* #throws NamingException
*/
public Destination getDestination(String destinationName) throws NamingException
{
return (Destination) getObjectByName(destinationName);
}
}
NewPublisher.java
package com.tradefinance.jms.topics;
//JMS classes
import javax.jms.JMSException;
import javax.jms.ConnectionFactory;
import javax.jms.Connection;
import javax.jms.Session;
import javax.jms.Destination;
import javax.jms.MessageProducer;
import javax.jms.TextMessage;
//JNDI classes
import javax.naming.NamingException;
import com.tradefinance.jms.util.JNDIUtil;
/**
* A class to demonstrate how to a publish to a topic.
*/
public class NewsPublisher
{
public static String icf = "com.sun.jndi.fscontext.RefFSContextFactory";
public static String url = "file:/C:/JNDI-Directory/";
public static void main(String[] vars) throws JMSException, NamingException
{
ConnectionFactory factory = null;
Connection connection = null;
Session session = null;
Destination destination= null; // a destination can be a topic or a queue
MessageProducer producer= null;
try
{
JNDIUtil jndiUtil= new JNDIUtil(icf,url);
factory= jndiUtil.getConnectionFactory("TestQM1ConnectionFactory");
connection = factory.createConnection();
connection.start();
// Indicate a non-transactional session
boolean transacted = false;
session = connection.createSession( transacted, Session.AUTO_ACKNOWLEDGE);
destination = jndiUtil.getDestination("NewsTopic");
producer = session.createProducer(destination);
TextMessage message = session.createTextMessage("No News is Good News!");
producer.send(message);
System.out.println("NewsPublisher: Message Publication Completed");
}
finally
{
// Always release resources
if ( producer!= null )
producer.close();
if ( session!= null )
session.close();
if ( connection!= null )
connection.close();
}
}
}
Getting the error on these lines:
return (ConnectionFactory) getObjectByName(factoryName);
in JNDIUtil.java
factory= jndiUtil.getConnectionFactory("TestQM1ConnectionFactory");
in NewPublisher.java
You are missing some JARs of MQ Client to get this working.
I had the same error, and after some further investigation, I ended up with this list of Jars in order to get this working:
com.ibm.mq.jmqi.jar
com.ibm.mqjms.jar
dhbcore.jar
fscontext.jar
providerutil.jar
com.ibm.mq.headers.jar
What you received back from the jndi context was a reference. This is a recipe to build the connection factory and I suspect that the class responsible for this cannot be found because the MQ jars required are not in the classpath. The error message is not intuitive.
Failing that, I find a good way to debug jndi lookup issues is to acquire the context and execute a list() on it, printing out the details of each object returned, just so you're clear on what exactly resides in the directory.
Add the Below jars to the classpath :
sibc.jms
sibc.jndi
In my case, we use TIBCO JMS adding the following dependency is resolved casting issue
ClassCastException: javax.naming.Reference cannot be cast to
javax.jms.ConnectionFactory
implementation 'com.tibco.tibjms:tibjms:5.1.4'

Resources