feign request microservice using domain url - spring-cloud

I am invoking microservice api using feign like this now:
Response<List<AppResponse>> apps = appController.getApps();
And this is server side:
#RequestMapping(value = "/app")
#FeignClient(name = "soa-service")
public interface IAppController {
#GetMapping(value = "/list")
Response<List<AppResponse>> getApps();
}
Because the client side and server side registerd to eureka(the eureka could find the internal registed ip address),the invoke works fine.My question is : when the client and server not in one network(maybe the client not registed to eureka and deploy to external net). Is it possible to invoke microservice using domain url like "www.api.example.com/app/list"?
ps:I know one solution to change my invoke using okhttpclient,but the problem is: I must change all old feign invoke to new okhttp rest invoke.

#RequestMapping(value = "/app")
#FeignClient(name = "soa-service", url = "http://www.api.example.com/app/list")
public interface IAppController {
#GetMapping(value = "/list")
Response<List<AppResponse>> getApps();
}

Related

Keycloak authorization policy evaluation with spring cloud gateway

I am trying to use keycloak for authorization in spring cloud gateway. Keycloak does not provide any spring based adapters for policy enforcement for reactive stack.However, it does provide an endpoint for policy evaluation.
http://localhost:8080/realms/myrealm/protocol/openid-connect/token -- POST
Request:
grant_type:urn:ietf:params:oauth:grant-type:uma-ticket
response_mode:decision
audience:b2b
permission:spm_audit#GET
Header:
Authorization : bearer <JWT>
# spm_audit is the resource that I have created in keycloak and GET is the scope(using HTTP methods as api scopes).
RESPONSE:
{
"result": true
}
My problem is that above endpoint does not accept URI as permission in request body and I don't have any resource-name to request URL mapping at gateway.
One possible solution could be to use gateway's route id as resource name and pass it in permission
cloud:
gateway:
routes:
- id: spm_audit
uri: http://localhost:8001
predicates:
- Path=/gateway/spm/api/v1/registrations/{regUUID}/audit
filters:
- StripPrefix=1
metadata:
custom_scope: "test scope"
#Fetch the route info in auth manager
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); //(ServerWebExchange exchange)
route.getId();
The problem with this approch is that the route matching filters are applied after authorization filter and exchange.getAttribute(GATEWAY_ROUTE_ATTR) is coming as null, plus I will have to map all api paths in route configuration and will end up with a huge configuration file.
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, #Qualifier("keycloKWebClient")WebClient kycloakWebClient) {
http
.authorizeExchange()
.pathMatchers(
"/gateway/*/public/**")
.permitAll()
.and()
.authorizeExchange()
.anyExchange()
.access(keyalokAuthManager(kycloakWebClient))....#this is where I call policy evaluation api
https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_authorization_api
What about using spring-security for resource-servers with a JWT decoder? It would be far more efficient as it would save many round trips to authorization-server (JWT decoder validates access-token with authorization-server public key downloaded once when policy enforcer requires a call to authorization-server for each and every incoming "non public" request).
You can map Keycloak "roles" to spring "granted authorities" and apply Role Based Access Control either with:
http.authorizeExchange().pathMatchers("/protected-route/foo").hasAuthority("ROLE_IN_KEYCLOAK") in your Java conf
#PreAuthorize("hasAnyAuthority('ROLE_IN_KEYCLOAK')") on your components methods.
For that, all you have to do is provide with an authentication converter with custom authorities converter:
interface AuthenticationConverter extends Converter<Jwt, Mono<JwtAuthenticationToken>> {}
interface AuthoritiesConverter extends Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> {}
#Bean
AuthoritiesConverter authoritiesConverter() {
return claims -> {
final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());
// concat client roles to following stream if your app uses client roles in addition to realm ones
return realmRoles.stream().map(SimpleGrantedAuthority::new).toList();
}
}
#Bean
public AuthenticationConverter authenticationConverter(AuthoritiesConverter authoritiesConverter) {
return jwt -> Mono.just(new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt.getClaims())));
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, AuthenticationConverter authenticationConverter) {
http.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(authenticationConverter);
}
You should also have a look at this repo: https://github.com/ch4mpy/spring-addons
There is a spring-addons-webflux-jwt-resource-server spring-boot (2.7 or later) starter which would save you quite some configuration hassle. Tutorials in this repo are using servlet variants of the libs, but the 4 starters (servlet/reactive with JWT-decoder/introspection) work the same and you should easily find what to adapt for your reactive app.

Strange issue with Vertx Http request

I configured an HTTPS website on AWS, which allows visiting from a white list of IPs.
My local machine runs with a VPN connection, which is in the white list.
I could visit the website from web browser or by the java.net.http package with the below code:
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://mywebsite/route"))
.GET() // GET is default
.build();
HttpResponse<Void> response = client.send(request,
HttpResponse.BodyHandlers.discarding());
But if I replaced the code with a Vertx implementation from io.vertx.ext.web.client package, I got a 403 forbidden response from the same website.
WebClientOptions options = new WebClientOptions().setTryUseCompression(true).setTrustAll(true);
HttpRequest<Buffer> request = WebClient.create(vertx, options)
.getAbs("https://mywebsite/route")
.ssl(true).putHeaders(headers);
request.send(asyncResult -> {
if (asyncResult.succeeded()) {
HttpResponse response = asyncResult.result();
}
});
Does anyone have an idea why the Vertx implementation is rejected?
Finally got the root cause. I started a local server that accepts the testing request and forwards it to the server on AWS. The testing client sent the request to localhost and thus "Host=localhost:8080/..." is in the request header. In the Vert.X implementation, a new header entry "Host=localhost:443/..." is wrongly put into the request headers. I haven't debug the Vert.X implementation so I have no idea why it behaviors as this. But then the AWS firewall rejected the request with a rule that a request could not come from localhost.

404 Errors using Vert.x web proxy code taken from its documentation

I am using the Vert.x Web library to create a test application for a reverse proxy. I am using the information provided on the Vert.x site:
https://vertx.io/docs/4.1.0/vertx-web-proxy/java/#_using_vert_x_web_proxy
I have written some simple test code, based on the documentation at the above site:
public class WebTest
{
public static void main(String[] args)
{
Vertx vertx = Vertx.vertx();
HttpClient client = vertx.createHttpClient();
HttpProxy proxy = HttpProxy.reverseProxy(client);
proxy.origin(8080,"localhost");
HttpServer server = vertx.createHttpServer();
Router router = Router.router(vertx);
router.route(HttpMethod.GET,"/foo").handler(ProxyHandler.create(proxy));
server.requestHandler(router);
server.listen(8010);
}
}
I have a web server (the so- called "origin" in Vert.x terminology) running on Port 8080. The only difference between the code above and the code on the web site is that I am using different port numbers for the proxy and the origin server. According to the documentation on the Vert.x website, a browser going to http://localhost:8010/foo should access the origin server.
Instead, I am getting 404 Not found errors.
Is there something missing in this code? Maybe something not covered in the documentation?
Does anyone have any idea how to make it work properly?
#Factor Three you are missing backend part of the guide and also you are mixing parts in an unorganized way. Following the guide it can be like this:
Vertx vertx = Vertx.vertx();
// Origin Server (Backend 8080)---------
// Here you deploy the real server with the endpoint ("hello world" in this case)
HttpServer backendServer = vertx.createHttpServer();
Router backendRouter = Router.router(vertx);
backendRouter
.route(HttpMethod.GET, "/foo")
.handler(
rc -> {
rc.response().putHeader("content-type", "text/plain").end("hello world");
});
backendServer.requestHandler(backendRouter).listen(8080);
// Proxy Server (8010)
// Here you deploy the server that will act as a proxy
HttpServer proxyServer = vertx.createHttpServer();
Router proxyRouter = Router.router(vertx);
proxyServer.requestHandler(proxyRouter);
proxyServer.listen(8010);
// Proxy Magic
// Here you route proxy server requests (8010) to the origin server (8080)
HttpClient proxyClient = vertx.createHttpClient();
HttpProxy httpProxy = HttpProxy.reverseProxy(proxyClient);
httpProxy.origin(8080, "localhost");
proxyRouter.route(HttpMethod.GET, "/foo").handler(ProxyHandler.create(httpProxy));

how use application name replace ip:port about spring cloud eureka?

spring cloud eureka question: there are 3 microservices, 1 eureka server, 2 eureka client;
microservice A,B use annotation #EnableEurekaClient;
microservice A have a RESTful api "http://localhost:8080/hi". the api return "hello".
now, I call the api , use url "http://client/hi", but it doesn't work.
how use application name replace ip:port about spring cloud eureka?
the bootstrap.yml content:
spring:
application:
name: client
eureka:
client:
service-url:
defaultZone: http://${eureka.host:localhost}:${eureka.port:8761}/eureka/
There are many ways to do that and it depends on how you call REST API in your code.
If you are using RestTemplate to call the API, you can do that with #LoadBalanced RestTemplate
In your code that wants to invoke REST api, please define RestTemplate with #LoadBalanced like below.
#LoadBalanced
#Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
And when you call API, just use application name instead of host:port like below.
this.restTemplate.getForObject("http://client/hi", String.class)
If you are using SpringCloud Feign, you can define the interface to call your REST api like below (without URL)
#FeignClient(name="client")
public interface ProductResource {
:
}
And add annotation #EnableFeignClients in your spring boot application like below.
#SpringBootApplication
#EnableFeignClients
: // other annotations you need.
public class YourAPIInvokerApplication {
In both ways, you need to add a few dependencies.
I'm using RestTemplate, but i got Connection timed out: connect after put serviceName instead of localhost:port
#Bean
#LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
call API:
FraudCheckResponse fraudCheckResponse = customerConfig.restTemplate().getForObject(
"http://fraud/api/v1/fraud-check/{customerId}",
FraudCheckResponse.class,
customer.getId()
);

How can I force UriBuilder to use https?

I'm currently running Dropwizard behind Apache httpd acting as a reverse proxy, configured like so:
<VirtualHost *:443>
<Location /api>
ProxyPass "http://my.app.org:8080/api"
<Location>
...
</VirtualHost>
With other Location settings serving static assets and some authentication thrown in. Now, httpd also performs SSL offloading, so my Dropwizard only receives the plain HTTP request.
In my Dropwizard API, I like to return a Location header indicating newly created resources:
#Path("/comment")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
class CommentResource() {
#PUT
fun create(#Context uri: UriInfo, entity: EventComment): Response {
val stored: EventComment = createEntity(entity)
return Response.created(uri.baseUriBuilder.path(MessageStream::class.java)
.resolveTemplate("realmish", entity.realmId)
.path(stored.id.toString()).build()).build()
}
This creates a Response with a Location header from JerseyUriBuilder:
Location http://my.app.org/api/messages/123
Which, on my SSL-only app, naturally fails to load (I'm actually surprised this didn't turn out to render as http://my.app.org:8080/api/messages/123 - probably also the reason why ProxyPassReverse didn't help).
I know I can force the scheme to be https by using baseUriBuilder.scheme("https"), but this gets repetitive and is an easy source of errors.
Thus my question: how can I either make Jersey generate correct front-end URLs or successfully make httpd rewrite those generated by Dropwizard?
For Jersey, you can use a pre-matching ContainerRequestFilter to rewrite the URI. For example
#PreMatching
public class SchemeRewriteFilter implements ContainerRequestFilter {
#Override
public void filter(ContainerRequestContext request) throws IOException {
URI newUri = request.getUriInfo().getRequestUriBuilder().scheme("https").build();
request.setRequestUri(newUri);
}
}
Then just register it with Jersey (Dropwizard)
env.jersey().register(SchemeRewriteFilter.class);
EDIT
The above only works when you use uriInfo.getAbsolutePathBuilder(). If you want to use uriInfo.getBaseUriBuilder(), then you need to use the overloaded setRequestUri that accepts the base uri as the first arg.
URI newUri = request.getUriInfo().getRequestUriBuilder().scheme("https").build();
URI baseUri = request.getUriInfo().getBaseUriBuilder().scheme("https").build();
request.setRequestUri(baseUri, newUri);
If using Jetty, then you can avoid the hacks by registering the org.eclipse.jetty.server.ForwardedRequestCustomizer with your server. This will look at the X-Forwarded-* headers to build the base URI.
Sample using embedded Jetty:
Server jettyServer = new Server();
HttpConfiguration config = new HttpConfiguration();
config.addCustomizer(new ForwardedRequestCustomizer());
ServerConnector serverConnector = new ServerConnector(jettyServer,
new HttpConnectionFactory(config));
serverConnector.setPort(8080);
jettyServer.setConnectors(new Connector[] {serverConnector});
This seems to work whether or not you are behind a reverse proxy, so I don't know why it isn't just enabled by default.