Request-based Sticky Session configuration with Spring Cloud LoadBalancer - spring-cloud

I have the following configuration for request-based sticky session using Spring Cloud LoadBalancer
spring:
cloud:
discovery.client.simple.instances:
say-hello:
- instanceId: say-hello1
uri: http://localhost:8080
- instanceId: say-hello2
uri: http://localhost:8081
loadbalancer:
configurations: request-based-sticky-session
sticky-session:
add-service-instance-cookie: true
server.port:9090
the following call:
$ http :9090/hi 'Cookie:sc-lb-instance-id=say-hello1'
should go only to the say-hello1 instance based on the Request-based Sticky Session for LoadBalancer but instead is using round robin load balancing.
What do I miss here?
Here is the source code to try it: https://github.com/altfatterz/client-side-loadbalancing

There are 2 things to consider here:
In the sample, the cookie has to be passed on to the actual load-balanced request, for example like so:
#GetMapping("/hi")
public String hi(#RequestParam(value = "name", defaultValue = "Mary") String name) {
logger.info("Accessing /hi endpoint");
HttpHeaders headers = new HttpHeaders();
headers.set("Cookie", "sc-lb-instance-id=say-hello1");
HttpEntity entity = new HttpEntity(headers);
ResponseEntity<String> greeting = restTemplate.exchange("http://say-hello/greeting", HttpMethod.GET, entity, String.class, new HashMap<>());
return greeting + " " + name;
}
This feature is only supported for WebClient-backed load-balancing. It was not properly documented. I have documented it here.
I have also created a GitHub issue for adding the non-reactive implementation, however, the decision to implement it will be dependant on larger community interest.

Related

GRPC Repeated field does not transcode to an array as body parameter in REST API

I´m having little luck trying to send a PUT request with JSON containing an array of objects to my GRPC Server using REST. Using GRPC however it accepts an array just like expected. This is what I have defined in my proto file:
message UpdateRequest {
repeated Data data = 1;
int32 Id = 2;
}
message UpdateResponse {
}
message Data {
int32 id = 1;
string name = 2;
}
rpc Update(UpdateRequest) returns (UpdateResponse) {
option (google.api.http) = {
put: "/v1/data/{Id}"
body: "*"
};
}
This deploys successfully to GCP Endpoints but according to the GCP enpointsportal the request body is supposed to only contain a single object like:
{
"data": {
}
}
instead of an array of objects like expected:
{
"data": [
{},
{}
]
}
I´ve tried with replacing the "*" in the body with "data"
rpc Update(UpdateRequest) returns (UpdateResponse) {
option (google.api.http) = {
put: "/v1/data/{Id}"
body: "data"
};
}
This also compiles, but fails when trying to deploy to GCP endpoints with the following message:
kind: ERROR
message: "http: body field path 'data' must be a non-repeated message."
Any suggestions as to how I should go about solving this would be greatly appreciated.
Update:
Heres the contents of my .yaml file.
type: google.api.Service
config_version: 3
name: xxx.xxx-xxx.dev
title: xxxx
apis:
- name: x.x
- name: x.y
backend:
rules:
- selector: "*"
address: grpcs://xxx-xxx-app-xxxx-lz.a.run.app
This is a known issue, according to GCP support.
Here is the google issuetracker link: https://issuetracker.google.com/issues/178486575
There seems that this is a bug in GCP endpoints portal. I´m now successfully sending update requests with arrays containing object through CURL and my frontend application, although this does not work through endpoints.

Spring OAuth2 Keycloak Kubernetes internal/external access

I have Keycloak (10.0.3) server configured inside a Kubernetes Cluster.
The keycloak server has to handle authentification for external user (using an external url) and also handle oauth2 token for Spring microservices communications.
Then web application spring services uses oidc providers :
spring:
security:
oauth2:
client:
provider:
oidc:
issuer-uri: http://keycloak-cluster-http.keycloak-cluster.svc.cluster.local/auth/realms/myrealm
authorization-uri: http://keycloak-cluster-http.keycloak-cluster.svc.cluster.local/auth/realms/myrealm/protocol/openid-connect/auth
jwk-set-uri: http://keycloak-cluster-http.keycloak-cluster.svc.cluster.local/auth/realms/myrealm/protocol/openid-connect/certs
token-uri: http://keycloak-cluster-http.keycloak-cluster.svc.cluster.local/auth/realms/myrealm/protocol/openid-connect/token
user-name-attribute: preferred_username
The external URL of keycloak is https://keycloak.localhost, managed by ingress redirection handled by Traefik v2
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: keycloak-https
namespace: keycloak-cluster
annotations:
traefik.frontend.passHostHeader: "true"
spec:
entryPoints:
- websecure
routes:
- match: Host(`keycloak.localhost`)
kind: Rule
services:
- name: keycloak-cluster-http
port: 80
tls:
options:
name: mytlsoption
namespace: traefik
store:
name: default
I can access Keycloak using https://keycloak.localhost, no problem, it works.
The problem is that when I try to access my web application, it will always redirect to 'http://keycloak-cluster-http.keycloak-cluster.svc.cluster.local/auth/realms/myrealm', which is not resolved outside k8s.
If I change issuer-uri to http://keycloak.localhost then it doesn't work as keycloak.locahost is not resolved inside k8s.
I tried to set the KEYCLOAK_FRONTEND_URL to https://keycloak.localhost/auth, but no change.
Please, does someone has the same kind of settings and managed to make it working ?
Best regards
Managed to fix it using coredns and adding a rewrite rule... :
rewrite name keycloak.localhost keycloak-cluster-http.keycloak-cluster.svc.cluster.local
apiVersion: v1
data:
Corefile: |
.:53 {
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
rewrite name keycloak.localhost keycloak-cluster-http.keycloak-cluster.svc.cluster.local
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
The authorization_uri needs to be understood by the browser since that URI is processed in the front channel. The rest of the URIs are processed in the back channel.
Because of that, the authorization_uri should use the front channel way of addressing the authorization server:
authorization_uri: https://keycloak.localhost/auth/realms/myrealm/protocol/openid-connect/auth
EDIT Based on Joe Grandja's input below, it appears that it's also necessary to not specify the issuer-uri property. The issuer-uri property is a shortcut for specifying the other URIs, and since you are specifying those, you don't need it anyway.
Here A POC that helped me with the issue.
Similar configuration ,keycloak and spring gateway are in kubernetes
The external user uses keycloak external host with https protocol
https://external-https/auth/realms/myrealm/protocol/openid-connect/auth?...
The ingress break the https and moves it to http + change the host to internal-http
gateway uses internal-http to connect to keycloakon port 8080
In order for the issuer to be the same protocol as the external the configuration uses https in user-info-uri and authorization-uri but the rest are http
make sure that the keycloak pod is open for https connection (8443)
authorization-uri: https://internal-http:8443/auth/realms/myrealm/protocol/openid-connect/auth
user-info-uri: https://internal-http:8443/auth/realms/myrealm/protocol/openid-connect/userinfo
issuer-uri: http://internal-http:8080/auth/realms/myrealm
To fix the host part of the issuer
In the gateway code I updated the following based on https://github.com/spring-projects/spring-security/issues/8882#user-content-oauth2-client
#SneakyThrows
private WebClient webClient() {
SslContext sslContext = SslContextBuilder
.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
HttpClient httpClient = HttpClient.create()
.secure(t -> t.sslContext(sslContext))
.wiretap(true)
;
ReactorClientHttpConnector conn = new ReactorClientHttpConnector(httpClient);
return WebClient.builder()
.defaultHeader("HOST", "external-https")
.clientConnector(conn)
.build();
}
#Bean
WebClientReactiveAuthorizationCodeTokenResponseClient webClientReactiveAuthorizationCodeTokenResponseClient() {
final WebClientReactiveAuthorizationCodeTokenResponseClient webClientReactiveAuthorizationCodeTokenResponseClient = new WebClientReactiveAuthorizationCodeTokenResponseClient();
final WebClient webClient = webClient();
webClientReactiveAuthorizationCodeTokenResponseClient.setWebClient(webClient);
return webClientReactiveAuthorizationCodeTokenResponseClient;
}
#Bean
WebClientReactiveClientCredentialsTokenResponseClient webClientReactiveClientCredentialsTokenResponseClient() {
final WebClientReactiveClientCredentialsTokenResponseClient webClientReactiveClientCredentialsTokenResponseClient = new WebClientReactiveClientCredentialsTokenResponseClient();
final WebClient webClient = webClient();
webClientReactiveClientCredentialsTokenResponseClient.setWebClient(webClient);
return webClientReactiveClientCredentialsTokenResponseClient;
}
#Bean
WebClientReactiveRefreshTokenTokenResponseClient webClientReactiveRefreshTokenTokenResponseClient() {
final WebClientReactiveRefreshTokenTokenResponseClient webClientReactiveRefreshTokenTokenResponseClient = new WebClientReactiveRefreshTokenTokenResponseClient();
final WebClient webClient = webClient();
webClientReactiveRefreshTokenTokenResponseClient.setWebClient(webClient);
return webClientReactiveRefreshTokenTokenResponseClient;
}
#Bean
WebClientReactivePasswordTokenResponseClient webClientReactivePasswordTokenResponseClient() {
final var client = new WebClientReactivePasswordTokenResponseClient();
final WebClient webClient = webClient();
client.setWebClient(webClient);
return client;
}
#Bean
DefaultReactiveOAuth2UserService reactiveOAuth2UserService() {
final DefaultReactiveOAuth2UserService userService = new DefaultReactiveOAuth2UserService();
final WebClient webClient = webClient();
userService.setWebClient(webClient);
return userService;
}
Disabled the certificate validation - the connection is only between keycloak and gateway , both are in the kubernetes and otherwise would have used http connection, if not for this issue
The host part tells the keyclock what is the host to use for the issuer
Another issue encountered is that the location return when redirecting to authentication contains the internal url and not the external which the outside world doesn't know of
For that ,update the location that returns from the gateway
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
...
oauth2Login(oAuth2LoginSpec -> oAuth2LoginSpec
...
.addFilterAfter(new LoginLocationFilter("external-https"), SecurityWebFiltersOrder.LAST)
...
public class LoginLocationFilter implements WebFilter {
private final String externalUrl;
public LoginLocationFilter(String externalUrl) {
this.externalUrl = externalUrl;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
//before commit ,otherwise the headers will be read only
exchange.getResponse().beforeCommit(() -> {
fixLocation(exchange);
return Mono.empty();
});
return chain.filter(exchange);
}
...

Deny access to one particular subpath for spring cloud gateway route

We're using Spring Cloud Gateway in front of our backend services. We have a route similar to the following:
routes:
- id: foobar-service
uri: lb://foobar-service
predicates:
- Path=/foobar/**
filters:
- StripPrefix=1
We want to deny access to one particular subpath (e.g. /foobar/baz/**) but leave the rest open. Is it possible to do this using the YAML syntax? Perhaps we need to implement the routes using the Fluent API instead?
routes:
- id: foobar-baz-service
uri: no://op
predicates:
- Path=/foobar/baz/**
filters:
- SetStatus=403
- id: foobar-service
uri: lb://foobar-service
predicates:
- Path=/foobar/**
filters:
- StripPrefix=1
Sharing my experience with the API implementation with a single route.
#Bean
public RouteLocator routes( final RouteLocatorBuilder locatorBuilder ) {
RouteLocatorBuilder.Builder builder = locatorBuilder.routes();
builder.route(p -> p //
.path(getAllowedPaths()) //
.and() //
.not(n -> n.path(getRestrictedPaths()) //
.filters(f -> f //
//filters
.uri(uri)));
return builder.build();
}

IdentityServer 4 in k8s behind loadbalancer

I've a identityserver deployed to kubernetes. I also konfigured google and facebook auth (see below). The HTTPS Termination is done but the K8s Ingress.
To get the identity still working with https i set forwarding rules (see below).
But from now on i get the following error and a HTTP 500 When a User tries to login. Terror occurs when the
System.InvalidOperationException: No authentication handler is
configured to handle the scheme: Identity.External
The line of code that triggers the error is in the account controller:
signInManager.ExternalLoginSignInAsync(provider, userIdClaim.Value, true);
My identity server startup looks like this:
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedProto,
ForwardLimit = null,
RequireHeaderSymmetry = false
});
app.UseIdentityServer();
app.UseGoogleAuthentication(new GoogleOptions
{
AuthenticationScheme = "Google",
DisplayName = "Google",
SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
ClientId = "dfdfsf",
ClientSecret = "-cf-"
});
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
what am I missing?

Eureka never unregisters a service

I'm currently facing an issue where Eureka does not unregister a registered service. I've pulled the Eureka server example straight from git hub and made only one change, eureka.enableSelfPreservation = false. My application.yml looks like this:
server:
port: 8761
eureka:
enableSelfPreservation: false
client:
registerWithEureka: false
fetchRegistry: false
server:
waitTimeInMsWhenSyncEmpty: 0
I've read that if 85% of the registered services stop delivering heartbeats within 15 minutes, Eureka assumes the issue is network related and does not de-register the services that are not responding. In my case I have only one service running, so I disabled self-preservation mode. I am abruptly killing the process and Eureka leaves the service registered for what seems like an indefinite amount of time.
My client's application.yml looks like this:
eureka:
instance:
leaseRenewalIntervalInSeconds: 3
client:
healthcheck:
enabled: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
appInfo:
replicate:
interval: 3
initial:
replicate:
time: 3
spring:
rabbitmq:
addresses: ${vcap.services.${PREFIX:}rabbitmq.credentials.uri:amqp://${RABBITMQ_HOST:localhost}:${RABBITMQ_PORT:5672}}
My goal is to create a demo where Eureka quickly detects the service is no longer running and another service that is started can quickly register itself.
As of now, once the eureka client is started, it registers in 3 seconds. It just never un-registers when the service is abruptly terminated. After I kill the service, the Eureka dashboard reads:
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
How can I prevent this behavior?
I realized that self preservation mode was never actually being disabled. It turns out the actual property is
eureka.server.enableSelfPreservation=false
(See DefaultEurekaServerConfig Code), which I haven't found documented anywhere. This resolved my issue.
I made service de-registration work by setting the below values
Eureka server application.yml
eureka:
server:
enableSelfPreservation: false
Service application.yml
eureka:
instance:
leaseRenewalIntervalInSeconds: 1
leaseExpirationDurationInSeconds: 2
The full example is here https://github.com/ExampleDriven/spring-cloud-eureka-example
After struggling a lot, finally I got solution if any service unregistered from Eureka server due to some issue. It will notify to the Admin by extending the HealthCallback of Eureka-Server APIs.
Let Say Service-A register with Eureka. Hence Eureka Client is integrate with Service-A and Implement following Callbacks in Service A.
Service-A [Eureka-Client]
Add following properties in properties files.
#Eureka Configuration
eureka.client.eureka-server-port=8761
eureka.client.register-with-eureka=true
eureka.client.healthcheck.enabled=false
eureka.client.prefer-same-zone-eureka=true
eureka.client.fetchRegistry=true
eureka.client.serviceUrl.defaultZone=${eurekaServerURL1}, ${eurekaServerURL2}
eureka.client.eureka.service-url.defaultZone=${eurekaServerURL1}, ${eurekaServerURL2}
eureka.instance.hostname=${hostname}
eureka.client.lease.duration=30
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=30
Add following java files.
#Component
public class EurekaHealthCheckHandler implements HealthCheckHandler, ApplicationContextAware, InitializingBean {
static Logger logger = LoggerFactory.getLogger(EurekaHealthCheckHandler.class);
private static final Map<Status, InstanceInfo.InstanceStatus> healthStatuses = new HashMap<Status, InstanceInfo.InstanceStatus>() {{
put(Status.UNKNOWN, InstanceInfo.InstanceStatus.UNKNOWN);
put(Status.OUT_OF_SERVICE, InstanceInfo.InstanceStatus.OUT_OF_SERVICE);
put(Status.DOWN, InstanceInfo.InstanceStatus.DOWN);
put(Status.UP, InstanceInfo.InstanceStatus.UP);
}};
#Autowired
ComunocationService comunocationService ;
private final CompositeHealthIndicator healthIndicator;
private ApplicationContext applicationContext;
public EurekaHealthCheckHandler(HealthAggregator healthAggregator) {
Assert.notNull(healthAggregator, "HealthAggregator must not be null");
this.healthIndicator = new CompositeHealthIndicator(healthAggregator);
Health health = healthIndicator.health();
logger.info(" =========== Testing =========== {}", health.toString() );
}
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
#Override
public void afterPropertiesSet() throws Exception {
final Map<String, HealthIndicator> healthIndicators = applicationContext.getBeansOfType(HealthIndicator.class);
for (Map.Entry<String, HealthIndicator> entry : healthIndicators.entrySet()) {
logger.info("======"+ entry.getKey() +"============= "+entry.getValue());
healthIndicator.addHealthIndicator(entry.getKey(), entry.getValue());
}
}
#Override
public InstanceInfo.InstanceStatus getStatus(InstanceInfo.InstanceStatus instanceStatus) {
logger.info("============== Custome Eureka Implementation ==================="+ getHealthStatus());
return getHealthStatus();
}
protected InstanceInfo.InstanceStatus getHealthStatus() {
final Status status = healthIndicator.health().getStatus();
return mapToInstanceStatus(status);
}
protected InstanceInfo.InstanceStatus mapToInstanceStatus(Status status) {
logger.info("============== Test Custome Eureka Implementation ==================={}", status);
if(status.equals(InstanceInfo.InstanceStatus.UP)) {
// Send mail after configured times
comunocationService.sendEmail("ServiceName");
}
if(!healthStatuses.containsKey(status)) {
return InstanceInfo.InstanceStatus.UNKNOWN;
}
return healthStatuses.get(status);
}
public void getstatusChangeListner() {
ApplicationInfoManager.StatusChangeListener statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
#Override
public String getId() {
return "statusChangeListener";
}
#Override
public void notify(StatusChangeEvent statusChangeEvent) {
if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
// log at warn level if DOWN was involved
logger.warn("Saw local status change event {}", statusChangeEvent);
} else {
logger.info("Saw local status change event {}", statusChangeEvent);
}
}
};
}
}
and
#Configuration
public class EurekaHealthCheckHandlerConfiguration {
#Autowired(required = false)
private HealthAggregator healthAggregator = new OrderedHealthAggregator();
#Bean
#ConditionalOnMissingBean
public EurekaHealthCheckHandler eurekaHealthCheckHandler() {
return new EurekaHealthCheckHandler(healthAggregator);
}
}
This is absolutely working and well tested code