Access service's endpoint /actuator/health through Spring Cloud Netflix: Zuul - java

I have Spring gateway: localhost:7856 and microservice - "my-service", for example localhost:8081. I can get access to endpoint localhost:8081/actuator/health -> {"status": "UP"}. But I need to access such endpoint through gateway like localhost:7856/my-service/actuator/health
My gateway config:
zuul:
ignoredServices: '*'
routes:
my-service:
path: /my-service/**
serviceId: my-service
stripPrefix: false
Here, crucial moment, I can't change stripPrefix to true. I know, that I can add
management:
endpoints:
web:
base-path: /my-service/actuator
but it wouldn't be good solution, because in that case also need to change eureka config (for eureka default endpoint is service-name/actuator/health) for check health status for microservices. Or I can create additional endpoint that would redirect to what I need. But I'm trying to find the best decision, may be its a special property for zuul or overriding zuul classes ?

Finally, I found a solution.
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute;
public class CustomRouteLocator extends SimpleRouteLocator {
public CustomRouteLocator(String servletPath, ZuulProperties properties) {
super(servletPath, properties);
}
#Override
protected Route getRoute(ZuulRoute route, String path) {
boolean oldPrefix = route.isStripPrefix();
if (path.matches(".*/actuator/.*")) {
route.setStripPrefix(true);
}
Route resultRoute = super.getRoute(route, path);
route.setStripPrefix(oldPrefix);
return resultRoute;
}
}
I turn on/off stripPrefix property. That works well.

Related

Session Validation - Spring Security with Microservices

My current architecture for my web app has a gateway server that orchestrates a bunch of microservices, authorisation occurs at the gateway if a given principle is authenticated they can talk to some downstream services.
The downstream service gets hold of the required data to identify a given authenticated client. However spring securities default behaviour kicks in and throws the expected:
org.springframework.security.access.AccessDeniedException: Access is denied
Given that I can use the session id and + XSRF token in any given Microservice to validate the user is authenticated and know which user is logged in (i'm currently using Http Basic).
My question is there a simpler / declarative approach that could be used in place of having to adding a filter to every Microservice to override spring securities default behaviour? (see my example Pseudo code)
See the attached diagram: Architecture.
Spring web security config for resource server:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public SessionRepository<ExpiringSession> sessionRepository() {
return new MapSessionRepository();
}
#Bean
HeaderHttpSessionStrategy sessionStrategy() {
return new HeaderHttpSessionStrategy();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and().authorizeRequests().anyRequest().authenticated();
final SessionRepositoryFilter<ExpiringSession> sessionRepositoryFilter = new SessionRepositoryFilter<ExpiringSession>(
sessionRepository());
sessionRepositoryFilter
.setHttpSessionStrategy(new HeaderHttpSessionStrategy());
http.addFilterBefore(sessionRepositoryFilter,
ChannelProcessingFilter.class).csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
}
public SessionRepository<ExpiringSession> getSessionRepository(){
return sessionRepository();
}
}
Header values at the resource microservice:
KEY: cookie VALUE: XSRF-TOKEN=[token_value]; SESSION=[session_value]
KEY: x-requested-with VALUE: XMLHttpRequest
KEY: x-auth-token VALUE: a32302fd-589b-42e1-8b9d-1991a080e904
...
Planned approach (Pseudo code) attach a new filter to the spring securities filter chain that if given flags are true, allow access to secured endpoints.
**
* A custom filter that can grant access to the current resource
* if there is a valid XSRF-TOKEN and SESSION present in the shared
* session cache.
*/
public class CustomAuthenticationFilter extends AnAppropriateFilterChainFilter {
#Autowired
SessionRepository sessionRepository;
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
boolean csrfTokenExists = sessionRepository.findByCsrfTokenId(request);
boolean sessionExists = sessionRepository.findBySessionId(request);
if (csrfTokenExists && sessionExists) {
// everything is okay
} else {
// invalidate the request as being authenticated
throw new InsufficientAuthenticationException("Invalid csrf + session pair");
}
}
}
After updating my spring boot parent to 2.0.1 Release and changing my spring cloud version to Finchley the issue was resolved by spring boot.
Note session repository and HttpSessionStrategy aren't required,
HttpSessionStrategy is now depreciated since spring session became spring session core.
Using the externalised config for redis and spring boot, all the dependant systems automatically use that cache for validating a valid session providing you have spring security on your class path.
Note if you are using the gateway pattern and Zuul Proxy ensure your proxy routes include the sensitive-headers: property in your app YML / properties, see examples below:
Example auth + gateway using Springboot Configuration of a shared session cache.
Auth Gateway
spring:
profiles: dev
redis:
host: localhost
port: 6379
session:
store-type: redis
server:
port: 8080
zuul:
routes:
# local routes
api:
url: forward:/api
path: /api/**
sensitive-headers:
# cloud-resource
resource:
url: http://localhost:9002
path: /resource/**
strip-prefix: false
sensitive-headers:
proxy:
auth:
routes:
resource: passthru
ui: none
api: passthru
security:
sessions: ALWAYS
Some Secured Resource Server
spring:
profiles: dev
redis:
host: localhost
port: 6379
session:
store-type: redis
security:
enabled: false
server:
port: 9002
security:
# Never create a session, but if one exists use it
sessions: NEVER
# don't display the auth box
basic:
enabled: false
management:
security:
enabled: false

Spring Cloud Gateway 2.0 forward path variable

How to forward a path variable in Spring Cloud Gateway 2.0?
If we have an microservice that has 2 endpoints: /users and /users/{id} and is running on port 8080, how to forward the request to the endpoint with the id path variable?
The following gateway configuration successfully forwards to the /users end point, but the second route forwards the request to the same /users endpoint of the real service.
#Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("users", t -> t.path("/users").uri("http://localhost:8080/users"))
.route("userById", t -> t.path("/users/**").uri("http://localhost:8080/users/"))
.build();
}
I'm using spring-cloud-starter-gateway from spring cloud Finchley.BUILD-SNAPSHOT
A rewritePath filter has to be used:
#Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("users", t -> t.path("/users")
.uri("http://localhost:8080/users"))
.route("userById", t -> t.path("/users/**")
.filters(rw -> rw.rewritePath("/users/(?<segment>.*)", "/users/${segment}"))
.uri("http://localhost:8080/users/"))
.build();
}
The YAML version is specified in the documentation:
spring:
cloud:
gateway:
routes:
- id: rewritepath_route
uri: http://example.org
predicates:
- Path=/foo/**
filters:
- RewritePath=/foo/(?<segment>.*), /$\{segment}
After some research, please find below what worked for me. Both methods produce the same result.
Here is my set-up:
the gateway is running on http://localhost:8090
a base path called /context serves as the entry point of the gateway
a service called my-resources running on http://localhost:8091/my-resources. When /my-resources is invoked without parameters, it returns all resources. When it is invoked with a parameters it returns the resource with the corresponding RID (if any)
The gateway is configured so that all path variables (possibly none) transmitted to http://localhost:8090/context/my-resources/ is forwarded to uri http://localhost:8091/my-resources/.
Method 1: using application.yml
spring:
cloud:
gateway:
routes:
- id: route_id
predicates:
- Path=/context/my-resources/**
filters:
- RewritePath=/context/my-resources/(?<RID>.*), /my-resources/$\{RID}
uri: http://localhost:8091
Method 2: using Java like configuration
#Bean
public RouteLocator routes(RouteLocatorBuilder routeBuilder) {
return routeBuilder.routes()
.route("route_id",
route -> route
.path("/context/my-resources/**")
.filters(f -> f.rewritePath("/context/my-resources/(?<RID>.*)", "/my-resources/${RID}"))
.uri("http://localhost:8091")
)
.build();
}

Spring Cloud Sidecar configuration issue

I have a very simple sidecar application - just the required annotations and the main method, like this:
#SpringBootApplication
#EnableSidecar
public class SidecarApplication {
...
}
I also have a service sitting behind it, on the same host, which has a GET /joke endpoint.
The sidecar configuration:
server.port=5678
spring.application.name=joker
sidecar.port=8083
sidecar.home-page-uri=http://localhost:8083/
sidecar.health-uri=http://localhost:8083/health.json
management.security.enabled=false
logging.level.root=DEBUG
However, when calling GET http://localhost:5678/joke, I get 404. If I call GET http://localhost:8083/joke, I do get a valid response.
Now, If I add this to the sidecar configuration:
zuul.routes.joker.url=http://localhost:8083/
then calling GET http://localhost:5678/joker/joke works as expected.
Am I missing something, or is this expected behavior? I would have expected no additional configuration to be necessary for the sidecar to route all incoming requests to the wrapped service, and I'd want the url to be used for accessing the service behind the sidecar not to need to contain a service name.
I think you need more notations like these in application.properties:
spring.application.name = joke
server.port = 5678
eureka.client.serviceUrl.defaultZone = http: // localhost: 9000 / eureka
eureka.client.register-with-eureka = true
eureka.client.fetch-registry = true
sidecar.port = 8083
sidecar.health-uri = http: // localhost: 8083 / health
management.security.enabled = false
/ * example * /
zuul.routes.wstore.path = / wstore / **
zuul.routes.wstore.url = http: // localhost: 8083
In the application should have these 3 notations
#SpringBootApplication
#EnableSidecar
#EnableDiscoveryClient
public class SidecarApplication {...}
note this configuration was useful for spring-boot 1.5.6.RELEASE
If you are using Eureka yes, the routing is transparent. The sidecar app registers itself with Eureka BUT sets homePageUrl value of Eureka InstanceInfo object to the actual host and port of your microservice. And this is the value your clients will use to make calls to your microservice.

Spring disable #EnableResourceServer

I have resource server, when it's starts - it's sending request to Authentication server ("http://localhost:xxxx/auth/oauth/token_key"), and it's okay when all up and running.
But when I testing my services I do not need this at all. How can I disable resource server or maybe I should mock something so it won't be dependent on auth server(for future security tests for controllers)?
My spring boot main:
#SpringBootApplication
#EnableEurekaClient
#EnableResourceServer
public class CalendarApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(CalendarApplication.class, args);
}
}
application.yml
security:
basic:
enabled: false
oauth2:
resource:
jwt:
keyUri: http://localhost:xxxx/auth/oauth/token_key
Test class annotations:
#RunWith(SpringJUnit4ClassRunner.class)
#WebMvcTest(value = TypeController.class, secure = false)
public class TypeControllerTest {}
Why don't you create a separate #Configuration for your #AuthenticationServer with a separate profile (#Profile("test"))? That way, you don't need to disable security and can have an in-memory Token. That's how I dealt with it. You can also disable Spring Security for your tests completely. Have a look at this question.
You can use #WithMockUser for tests
Testing Method Security
The way I've worked around this was to create a token in the database I'm using for test and to ensure that requests to my API used the token before making a request to the resource under test.
You do want your token there, since it acts as a reasonable sanity check for security. If you expect this resource to not be accessible without a specific token, then that is a useful test to have.

DropWizard not registering my health check

In my DropWizard (v0.7.0) app, I have a DummyHealthCheck like so:
public class DummyHealthCheck extends HealthCheck {
#Override
protected Result check() throws Exception {
return Result.healthy();
}
}
Then in my main Application impl:
public class MyApplication extends Application<MyConfiguration> {
#Override
public void run(MyConfiguration configuration, Environment environment)
throws Exception {
environment.jersey().register(new DummyHealthCheck());
}
}
When I start up the server, it starts successfuly (no exceptions/errors), however I get the following message:
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
! THIS APPLICATION HAS NO HEALTHCHECKS. THIS MEANS YOU WILL NEVER KNOW !
! IF IT DIES IN PRODUCTION, WHICH MEANS YOU WILL NEVER KNOW IF YOU'RE !
! LETTING YOUR USERS DOWN. YOU SHOULD ADD A HEALTHCHECK FOR EACH OF YOUR !
! APPLICATION'S DEPENDENCIES WHICH FULLY (BUT LIGHTLY) TESTS IT. !
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
But when I go to http://localhost:8081/healthcheck I see:
{"deadlocks":{"healthy":true}}
What is going on here? How do I register my health check?
Also, I have configured DropWizard to use SSL (self-signed) on port 8443; I have verified this works with my normal endpoints. I am surprised, however, to see that my admin app is still exposed on 8081 over HTTP. How do I configure it for HTTPS as well?
Question 1:
You don't register it with Jersey, as Health Checks are DropWizard specific. They should be registered as follows
environment.healthChecks().register("dummy", new DummyHealthCheck());
as explained here. If it was registered as above, you would see
{"deadlocks":{"healthy":true}, "dummy":{"healthy":true}}
Question 2:
I assume you already have done something similar to
server:
applicationConnectors:
- type: https
port: 8443
keyStorePath: example.keystore
keyStorePassword: example
validateCerts: false
in your yaml, as seen here. That is just for the application. You will also need to configure the admin
server:
applicationConnectors:
- ...
adminConnectors:
- type: https
port: 8444 // should a different port from the application
keyStorePath: example.keystore
keyStorePassword: example
validateCerts: false

Categories