Spring cloud gateway with discoveryclient and static routes - netflix-eureka

I'm currently replacing an api gateway using Netflix Zuul with spring cloud gateway. The setup uses discovery client (Eureka) for most of the routes, but we also have a solr instance running which requires manually defined routes (as solr doesn't support eureka)
Using a static route to solr running on localhost works fine using the following config:
routes:
- id: solr
predicates:
- Path=/solr/**
uri: http://localhost:10983
filters:
- RewriteLocationResponseHeader=AS_IN_REQUEST, Location,
However, I would like to use a load-balanced uri for this route as we have multiple solr instances. Looking at the documentation I've found that the way to implement this is to define a Bean returning a ServiceInstanceListSupplier. I've imlemented the following function:
#Bean
ServiceInstanceListSupplier serviceInstanceListSupplier() {
List<String> servers = Arrays.asList(microserviceGatewayConfig.getServers().split(","));
return new SolrServiceInstanceListSupplier("solrhosts", servers);
}
However, this seems to override the ServiceInstances defined from Eureka, meaning only the manual services are used...
Do anyone know if it is possble to combine manually defined serviceinstances with those generated from eureka?

The approach with creating a Bean returning a ServiceInstanceListSupplier doesn't seem to work in any way... However, I've found a way to achieve the same in application.yml, by adding the following config:
spring:
cloud:
discovery:
client:
simple:
instances:
solr-cluster:
- instanceId: cluster1
serviceId: solr-cluster
host: soa03i-t.usrv.ubergenkom.no
port: 10983
- instanceId: cluster2
serviceId: solr-cluster
host: soa04i-t.usrv.ubergenkom.no
port: 10983
This can be combined with autogenerated routes from service discovery (e.g. Eureka)

Related

Springboot 2.7 with oidc bearer token always redirects to login page

I have a client Springboot app which needs to access an oidc-protected REST service, so no UI component or UI login. I have the following yaml in the client:
spring:
security:
oauth2:
client:
registration:
my-service:
client-id: client-id
client-secret: client-secret
authorization-grant-type: client_credentials
provider:
my-service:
token-uri: https://mytokenhost.mydomain/token
which points to a Keycloak server on which I have configured an oidc client with a service account and enabled it. I use a WebClient to connect to the REST service which is configured like:
#Bean
public WebClient webClient(final OAuth2AuthorizedClientManager authorizedClientManager) {
final ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("my-service");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
I also needed to manually define an OAuth2AuthorizedClientManager for the WebClient to work:
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
final ClientRegistrationRepository clientRegistrationRepository,
final OAuth2AuthorizedClientService authorizedClientService) {
final OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder().refreshToken().clientCredentials().build();
final AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
And then use the WebClient to make a call to the REST service:
final String s = webClient
.method(HttpMethod.GET)
.uri("http://localhost:8080/my-rest-service/service?param1=value")
.attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId(
"catalogue-services"))
.retrieve()
.bodyToMono(String.class)
.block();
Via IntelliJ I can see I get a token back, however no matter what I do Springboot redirects to the login page of the REST service. The REST service has yaml:
spring:
security:
oauth2:
client:
registration:
my-service:
client-id: client-id
client-secret: my-secret
authorization-grant-type: client_credentials
provider:
my-service:
token-uri: https://mytokenhost.mydomain/token/openid-connect/token
and a security configuration in the REST service:
#Bean
public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception {
http.authorizeRequests(
a ->
a.antMatchers("/", "/error", "/someUrl")
.permitAll()
.anyRequest()
.authenticated())
.oauth2Login();
return http.build();
}
What is happening is that after successfully authenticating against KeyCloak, the REST service security filter is flagging the authentication as an anonymous login, presumably because the role is ROLE_ANONYMOUS and/or principal is anonymousUser. I can see the service account user name come back, the roles are included as well but maybe not being picked up. I have a realm role which is exposed in the token realm_access.roles and a client role which is exposed in resource_access.my-service.roles. When I debug the decision voting in the AffirmativeBase class I get:
AnonymousAuthenticationToken [Principal=anonymousUser,
Credentials=[PROTECTED], Authenticated=true,
Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null],
Granted Authorities=[ROLE_ANONYMOUS]]
I haven't used the Keycloak adapter since they look to be deprecated and the documentation still uses the WebSecurityConfigurerAdapter which is also deprecated in the Spring Security model.
It's likely I'm missing something really simple, but if anyone has done machine-to-machine oidc with Springboot and Keycloak and knows any tricks, any help would be appreciated.
If by "REST service" you mean a spring #RestControtroller (or #Controller with #ResponseBody), then it is an OAuth2 resource-server, not a client (like you configured in your "REST service" yaml file).
You can have a look at those tutorials which provide with OAuth2 concepts you need and sample configurations for resource-servers.

"DedupeResponseHeader" not working with Greenwich.SR3

DedupeResponseHeader is not working for me in Spring Cloud Greenwich.SR3, I have added CORS configuration in application.yml, and downstream application is also sending Access-Control-Allow-Origin in response header, which in ending up with:
The 'Access-Control-Allow-Origin' header contains multiple values 'http://localhost:4200, http://localhost:4200', but only one is allowed.
I have used DedupeResponseHeader but that is not working for me still seeing same error in browser console. Following is the config for CORS and DedupeResponseHeader:
spring:
cloud:
gateway:
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin, RETAIN_UNIQUE
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
allowedOrigins: "http://localhost:4200"
allowedMethods: "*"
allowedHeaders: "*"
Tried in filters also, but also didn't work
spring:
cloud:
gateway:
routes:
- id: dedupe_response_header_route
uri: http://localhost:4200
predicates:
- Method=OPTIONS
- Method=GET
filters:
- DedupeResponseHeader=Access-Control-Allow-Origin
Couldn't figure out the reason why its not working, double checked the spring cloud version. I appreciate, if someone could help to understand why DedupeResponseHeader not working.
You can use the latest version of the spring cloud i.e. 2020.0.2 --- it is working perfectly there.

Spring Cloud Vault With k2 v2 - How to Avoid 403 at Startup?

Problem
Does anyone know how to configure bootstrap.yml to tell Spring Cloud Vault to go to the correct path for k2 v2 and not try other paths first?
Details
I can successfully connect to my Vault, running k2 v2, but Spring Cloud will always try to connect to paths in the vault that don't exist, throwing a 403 on startup.
Status 403 Forbidden [secret/application]: permission denied; nested exception is org.springframework.web.client.HttpClientErrorException$Forbidden: 403 Forbidden
The above path, secret/application, doesn't exist because k2 v2 puts data in the path. For example: secret/data/application.
This isn't a show-stopper because Spring Cloud Vault does check other paths, including the correct one that has the data item in the path, but the fact a meaningless 403 is thrown during startup is like a splinter in my mind.
Ultimately, it does try the correct k2 v2 path
2019-03-18 12:22:46.611 INFO 77685 --- [ restartedMain] b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource {name='vault', propertySources=[LeaseAwareVaultPropertySource {name='secret/data/my-app'}
My configuration
spring.cloud.vault:
kv:
enabled: true
backend: secret
profile-separator: '/'
default-context: my-app
application-name: my-app
host: localhost
port: 8200
scheme: http
authentication: TOKEN
token: my-crazy-long-token-string
Thanks for your help!
Add the following lines in your bootstrap.yml, this disables the generic backend
spring.cloud.vault:
generic:
enabled: false
for more information https://cloud.spring.io/spring-cloud-vault/reference/html/#vault.config.backends.generic
In addition to the accepted answer it's important to turn off (or just remove) fail-fast option:
spring.cloud.vault:
fail-fast: false
spring.cloud.vault.generic.enabled is deprecated in spring-cloud 3.0.0, but the 403 error is still there. To disable the warning (by telling spring to use the exact context), this is what I used:
spring:
config:
import: vault://
application:
name: my-application
cloud:
vault:
host: localhost
scheme: http
authentication: TOKEN
token: my-crazy-long-token-string
kv:
default-context: my-application
Other configs were set to default (such as port = 8200, backend = secret, etc.)

As 'host' is deprecated for manifest.yml - how to configure a standard scapp.io route?

CF CLI now warns with a deprecation message:
Deprecation warning: Route component attributes 'domain', 'domains', 'host', 'hosts' and 'no-hostname' are deprecated. Found: host.
My manifest.yml looks like that currently:
applications:
- host: myexample-test
which results in a final route like: myexample-test.scapp.io
how to define this exact same route with the new manifest routes config?
These examples are taken from the cloudfoundry docs but I am not sure whether swisscomdev is adopting anything behind the scenes?
routes:
- route: example.com
- route: www.example.com/foo
- route: tcp-example.com:1234
UPDATE
Just tried it with suggested solution and this manifest:
applications:
routes:
- route: myexample-test.scapp.io
name: MyExample
buildpack: nodejs_buildpack
instances: 1
memory: 64M
which resulted in the following error message:
yaml: unmarshal errors:
line 2: cannot unmarshal !!map into []manifest.Application
Swisscom Application cloud does not do something special behind the scenes, so you can apply what's written in the CF CLI docs.
If we're doing something other than vanilla CF, we will mention this in our docs.
I quickly checked it, the following does the trick for your route:
routes:
- route: myexample-test.scapp.io
In your example, note that applications must be an array of maps, so make sure the first element key contains a -, otherwise it's treated as a map.
Full example:
applications:
- name: MyExample
routes:
- route: myexample-test.scapp.io
buildpack: nodejs_buildpack
instances: 1
memory: 64M

Spring Cloud app - zuul timeout after deploying on Tomcat

I'm trying to set up a couple of services using Spring Cloud and everything seems to work fine up until the moment I deploy the Eureka client services to Tomcat. When I call a service through my gateway app, I get the following error:
o.s.c.n.z.filters.post.SendErrorFilter : Error during filtering
com.netflix.zuul.exception.ZuulException: Forwarding error
...
Caused by: com.netflix.hystrix.exception.HystrixRuntimeException: hello timed-out and no fallback available.
...
Caused by: java.util.concurrent.TimeoutException: null
It works perfectly from eclipse however. It even works when I run the discovery and gateway services from Tomcat, and run the Eureka client service from eclipse. But as soon as I run the same service on tomcat, I get the error.
I'm using Brixton.M5, Java 8 and Tomcat 8.
Again, the code seems to work, the problem is it doesn't work after being deployed to Tomcat.
I have one Tomcat instance for the Discovery and Gateway services, and a second Tomcat instance for the Eureka client services.
Here's some code and config..
DiscoveryServerApp
#SpringBootApplication
#EnableEurekaServer
public class DiscoveryServerApp extends SpringBootServletInitializer
{
public static void main(String[] args)
{
SpringApplication.run(DiscoveryServerApp.class, args);
}
}
DiscoveryServer - application.yml
# Configure this Discovery Server
eureka:
instance:
hostname: discovery
client: # Not a client, don't register with yourself
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://localhost:1111/discovery/eureka/
server:
port: 1111 # HTTP (Tomcat) port
context-path: /discovery
DiscoveryServer - bootstrap.yml
spring:
application:
name: discovery
jmx:
default-domain: com.example.cloud.discovery
GatewayApplication
#SpringCloudApplication
#EnableZuulProxy
public class GatewayApplication extends SpringBootServletInitializer
{
public static void main(String[] args)
{
SpringApplication.run(GatewayApplication.class, args);
}
}
GatewayApplication - application.yml
# Discovery Server Access
eureka:
client:
serviceUrl:
defaultZone: http://localhost:1111/discovery/eureka/
instance:
instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}}
# HTTP Server
server:
port: 4444 # HTTP (Tomcat) port
context-path: /api
GatewayApplication - bootstrap.yml
# Spring properties
spring:
application:
name: gateway-service # Identify this application
jmx:
default-domain: com.example.cloud.gateway
encrypt:
failOnError: false
DummyApplication
#SpringCloudApplication
#RestController
public class DummyApplication extends SpringBootServletInitializer
{
public static void main(String[] args)
{
SpringApplication.run(DummyApplication.class, args);
}
#RequestMapping( path = "/hello-resource", method = RequestMethod.GET )
public String hello()
{
return "hello";
}
}
DummyApplication - application.yml
# Discovery Server Access
eureka:
client:
serviceUrl:
defaultZone: http://localhost:1111/discovery/eureka/
instance:
instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}} # Unique id for multiple instances
# HTTP Server
server:
port: 3333 # HTTP (Tomcat) port
context-path: /hello-context
DummyApplication - bootstrap.yml
# Spring properties
spring:
application:
name: hello-service # Service registers under this name
jmx:
default-domain: com.example.cloud.hello
encrypt:
failOnError: false
I figured it out by accident... turns out that the value of server.port needs to match the port of the Tomcat instance where it's deployed. It seems obvious now, but I thought Spring would somehow magically figure that out from the container it's running in. I guess it would be a good idea to read that configuration from an external location to handle different environments without having to make 'code changes'.
So anyways, the answer is: make sure your server.port in application.yml matches the port on the target container.
Thanks to everyone who took the time to help me with this!
You should point your bowser to port 4444 (gateway), not 1111 (eureka).
Ok, #SpringCloudApplication wraps #EnableDiscoveryClient which causes DummyApplication to register itself with Eureka on startup. You can confirm this is happening through the Eureka dashboard.
Assuming DummyApplication registers with Eureka as service name "hello-service", then Zuul / Ribbon will create a route for that service name. Thus, your "/hello-resource" endpoint should be proxied through Zuul at:
http://localhost:4444/api/hello-service/hello-resource/