Spring Boot OAuth Ressource Server Proxy Configuration - java

I am currently struggeling using a proxy in combination with Spring-Webflux. In other services I always followed this approach, which worked perfectly (proxy configuration is retrieved from standard environment variables):
#Bean
public RestTemplate restTemplate() {
final RestTemplate restTemplate = new RestTemplate();
final CloseableHttpClient client = HttpClientBuilder.create().useSystemProperties().build();
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(client));
return restTemplate;
}
But now I am trying to setup an OAuth-Ressource-Server using the Spring Oauth-Resource-Server package. This package uses the Spring-Webflux for HTTP(S). The service now tries to fetch the jwk-set from the given uri (proxy needed) and fails because of a connection refused error. Did anyone get a combination of Spring-Webflux/OAuth-Ressource and proxy working?

Found out by myself that providing a NimbusReactiveJwtDecoder bean with a correctly configured webclient solves the problem.
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import reactor.netty.tcp.ProxyProvider;
#Data
#Component
#Configuration
#ConfigurationProperties(value = "proxy")
public class ProxyConfig {
private String host;
private int port;
private String username;
private String password;
#Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
private String jwkSetUri;
#Bean
public WebClient webClient(ReactorClientHttpConnector reactorClientHttpConnector) {
return WebClient.builder().clientConnector(reactorClientHttpConnector).build();
}
#Bean
public HttpClient httpClient() {
return HttpClient.create()
.tcpConfiguration(tcpClient ->
tcpClient.proxy(
proxy -> proxy.type(ProxyProvider.Proxy.HTTP).host(host)
.port(port).username(username)
.password(s -> password)));
}
#Bean
ReactorClientHttpConnector reactorClientHttpConnector(HttpClient httpClient) {
return new ReactorClientHttpConnector(httpClient);
}
#Bean
public NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder(WebClient webClient) {
return NimbusReactiveJwtDecoder
.withJwkSetUri(jwkSetUri)
.webClient(webClient).build();
}
}

Related

Getting an error message when I try to send a message through RabbitMQ

I am getting this message when I try to send a message: Cannot invoke "org.springframework.amqp.core.DirectExchange.getName()" because "this.directExchange" is null although I made the right on configiration for rabbitmq according to their documentation.
package com.todoist.server.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
public class ClientConfig {
#Value("thequeue")
private String queueName;
#Value("xchange")
private String directXchangeName;
#Bean
public Queue queue() {
return new Queue(queueName);
}
#Bean
public DirectExchange directExchange() {
return new DirectExchange(directXchangeName);
}
#Bean
public Binding binding(DirectExchange exchange, Queue queue) {
return BindingBuilder.bind(queue).to(exchange).with("routing_key");
}
}
I am using springboot and I am trying to do the RPC pattern.

Spring IntegrationFlow with RestController

I would like to create an application that would perform the following steps:
Receive request thru RestController
Send the received message to a queue (AMQP - MessageChannel) (correlationId?)
Wait for the reply in another queue (AMQP - MessageChannel) (correlationId?)
Return the response on the same thread as the request in step 1.
I thought about using IntegrationFlow for this, but I'm not able to adapt the steps.
Besides, would you know what is the best way to implement this flow?
I tried to implement with the following code:
package poc.integration.http;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.integration.amqp.channel.PollableAmqpChannel;
import org.springframework.integration.amqp.dsl.Amqp;
import org.springframework.integration.amqp.outbound.AmqpOutboundEndpoint;
import org.springframework.integration.amqp.support.DefaultAmqpHeaderMapper;
import org.springframework.integration.annotation.ServiceActivator;
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.http.dsl.Http;
import org.springframework.integration.http.inbound.HttpRequestHandlingMessagingGateway;
import org.springframework.integration.http.inbound.RequestMapping;
import org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler;
import org.springframework.integration.scheduling.PollerMetadata;
import org.springframework.messaging.MessageChannel;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
#Configuration
#EnableIntegration
public class IntegrationFlowConfig {
final Logger logger = LoggerFactory.getLogger(IntegrationFlowConfig.class);
#Bean
public HttpRequestHandlingMessagingGateway inbound() {
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true);
gateway.setRequestMapping(mapping());
gateway.setRequestPayloadTypeClass(String.class);
gateway.setRequestChannelName("httpRequest");
return gateway;
}
#Bean
public RequestMapping mapping() {
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/foo");
requestMapping.setMethods(HttpMethod.GET);
return requestMapping;
}
#ServiceActivator(inputChannel = "httpResponse")
#Bean
public HttpRequestExecutingMessageHandler outbound() {
HttpRequestExecutingMessageHandler handler =
new HttpRequestExecutingMessageHandler("http://10.141.201.206:80/foo");
handler.setHttpMethod(HttpMethod.GET);
handler.setExpectedResponseType(String.class);
return handler;
}
#ServiceActivator(inputChannel="httpRequest", outputChannel="httpResponse")
public Object processarMensagem(Object mensagem) {
return mensagem + " - done";
}
#Bean(name = PollerMetadata.DEFAULT_POLLER)
public PollerMetadata pollerAmqp(ThreadPoolTaskScheduler taskScheduler) {
final PollerMetadata poller = new PollerMetadata();
poller.setTaskExecutor(taskScheduler);
poller.setReceiveTimeout(-1);
return poller;
}
#Bean
public MessageChannel httpRequest(AmqpTemplate amqpTemplate) {
PollableAmqpChannel channel = new PollableAmqpChannel("httpRequest", amqpTemplate,
DefaultAmqpHeaderMapper.outboundMapper(), DefaultAmqpHeaderMapper.inboundMapper());
channel.setExtractPayload(true);
return channel;
}
#Bean
public MessageChannel httpResponse(AmqpTemplate amqpTemplate) {
PollableAmqpChannel channel = new PollableAmqpChannel("httpResponse", amqpTemplate,
DefaultAmqpHeaderMapper.outboundMapper(), DefaultAmqpHeaderMapper.inboundMapper());
channel.setExtractPayload(true);
return channel;
}
}
but i'm receiving the message:
No reply received within timeout
You just need an IntegrationFlow with an Http.inboundControllerAdapter() as a starting point. It fully replaces the mentioned RestController, but let you to avoid extra work bridging from the #RestController to the IntegrationFlow and back.
The next step in the flow should be an Amqp.outboundGateway() to send and receive over AMQP. This one takes care about a correlation for you.
See more in docs:
https://docs.spring.io/spring-integration/docs/5.3.0.RELEASE/reference/html/http.html#http-java-config
https://docs.spring.io/spring-integration/docs/5.3.0.RELEASE/reference/html/amqp.html#configuring-with-the-java-dsl-4

Spring data Redis to Elastic Cache Connection Checklist

How to connect to AWS ELasticCache Redis Cluster using Spring-data-redis.
I am using redis as a datastore and would like to achieve
High availability
Low Latency
Assuming :
Auth required
Data encryption at transit required
Cluster mode on
package com.example.config;
import java.time.Duration;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
#Configuration
public class RedisConfiguration {
#Bean
public JedisClientConfiguration getJedisClientConfiguration(#Value("$REDIS_CONNECTION_TIMEOUT}") final Duration timeout) {
JedisClientConfiguration.JedisClientConfigurationBuilder jedisClientConfigurationBuilder = JedisClientConfiguration.builder();
jedisClientConfigurationBuilder.connectTimeout(timeout);
jedisClientConfigurationBuilder.usePooling();
return jedisClientConfigurationBuilder.build();
}
#Bean
public RedisClusterConfiguration getRedisClusterConfiguration(#Value("${REDIS_HOST}") final String host,
#Value("${REDIS_PORT}") final Integer port) {
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
redisClusterConfiguration = redisClusterConfiguration.clusterNode(host, port);
return redisClusterConfiguration;
}
#Bean
public JedisConnectionFactory jedisConnectionFactory(
#Qualifier("getRedisClusterConfiguration") final RedisClusterConfiguration redisClusterConfiguration,
#Qualifier("getJedisClientConfiguration") final JedisClientConfiguration jedisClientConfiguration) {
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(redisClusterConfiguration, jedisClientConfiguration);
return connectionFactory;
}
}
Missing
shrad
auth
Data encryption ..To be done at AWS ElastiCache

Why requests are returned 404 status when using WebFilter corsFilter in webflux?

I want to build a REST api in Spring Webflux using functional endpoints. For CORS I use a WebFilter corsFilter method which sets the required headers. I do see that the method is called (I see the log messages from it) and I see that the headers on the response are indeed the ones I set in my Webflux api. However, as soon as I started to use the corsFilter the requests return 404 status (earlier they would return JSON). I suspect that corsFilter doesn't pass over the request to the router functions. Why would that be?
Specifically I'm wondering if this line is enough to connect the cors config with the routes:
HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(RouterFunctions.toWebHandler(route))
.applicationContext(ctx).build();
This is my main class:
package com.mypackage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import reactor.ipc.netty.http.server.HttpServer;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
import static org.springframework.web.reactive.function.server.RequestPredicates.method;
import static org.springframework.web.reactive.function.server.RequestPredicates.path;
import static org.springframework.web.reactive.function.server.RouterFunctions.nest;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.RouterFunctions.toHttpHandler;
#SpringBootApplication
public class Server {
private static final Logger log = LogManager.getLogger(Server.class);
public static final String HOST = "localhost";
public static final int PORT = 8080;
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(CorsConfiguration.class);
Server server = new Server();
server.startReactorServer(ctx);
System.out.println("Press ENTER to exit.");
System.in.read();
}
public RouterFunction<ServerResponse> routingFunction() {
PersonRepository repository = new DummyPersonRepository();
PersonHandler handler = new PersonHandler(repository);
return nest(path("/person"),
nest(accept(APPLICATION_JSON),
route(GET("/{id}"), handler::getPerson)
.andRoute(method(HttpMethod.GET), handler::listPeople)
).andRoute(POST("/").and(contentType(APPLICATION_JSON)), handler::createPerson));
}
public void startReactorServer(AnnotationConfigApplicationContext ctx) {
RouterFunction<ServerResponse> route = this.routingFunction().filter((request, next) -> {
log.warn(request.path());
if (request.path().contains("person")) {
log.warn("calling next()");
return next.handle(request);
} else {
return ServerResponse.status(UNAUTHORIZED).build();
}
});
HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(RouterFunctions.toWebHandler(route))
.applicationContext(ctx).build();
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
HttpServer server = HttpServer.create(HOST, PORT);
server.newHandler(adapter).block();
}
}
and this is my CORS config class:
package com.mypackage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
#Configuration
#EnableWebFlux
public class CorsConfiguration {
private static final Logger log = LogManager.getLogger(CorsConfiguration.class);
private static final String ALLOWED_HEADERS = "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN, mode";
private static final String ALLOWED_METHODS = "GET, PUT, POST, DELETE, OPTIONS";
private static final String ALLOWED_ORIGIN = "*";
private static final String MAX_AGE = "3600";
#Bean
public WebFilter corsFilter() {
log.warn("from CorsConfiguration!!!");
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
log.warn("after ServerHttpRequest");
if (CorsUtils.isCorsRequest(request)) {
log.warn("inside isCorsRequest");
ServerHttpResponse response = ctx.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);
headers.add("Access-Control-Max-Age", MAX_AGE);
headers.add("Access-Control-Allow-Headers", ALLOWED_HEADERS);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
return chain.filter(ctx);
};
}
}
To use the functional approach when defining your endpoints Spring Boot's official documentation has a very simple example.
FooBarApplication.class this is our main class.
#SpringBootApplication
public class FooBarApplication {
public static void main(String[] args) {
SpringApplication.run(FooBarApplication.class, args);
}
}
RoutingConfiguration.class (or whatever you wanna call it)
#Configuration
public class RoutingConfiguration {
#Bean
public RouterFunction<ServerResponse> monoRouterFunction(UserHandler userHandler) {
return route(GET("/{user}").and(accept(APPLICATION_JSON)), userHandler::getUser)
.andRoute(GET("/{user}/customers").and(accept(APPLICATION_JSON)), userHandler::getUserCustomers)
.andRoute(DELETE("/{user}").and(accept(APPLICATION_JSON)), userHandler::deleteUser);
}
}
#Component
public class UserHandler {
public Mono<ServerResponse> getUser(ServerRequest request) {
// ...
}
public Mono<ServerResponse> getUserCustomers(ServerRequest request) {
// ...
}
public Mono<ServerResponse> deleteUser(ServerRequest request) {
// ...
}
}
any class annotated with #Configuration will be run at startup and run all #Bean annotated methods. So this will run the monoRouterFunction and set up all our routes for us.
Example taken from the official spring boot documentation Spring boot webflux scroll down a little bit.
EDIT:
and as a side note the #EnableWebFlux annotation means that you will disable the auto-configuration of webflux and set upp configuration manually. I do not recommend this if you are just starting out (i know the name is very misleading) you can read about the webflux auto-configuration here Spring WebFlux Auto-configuration
EDIT2:
WebFlux has a built in CorsFilter that you can use all you need is to configure it.
#Bean
CorsWebFilter corsWebFilter() {
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.setAllowedOrigins(Arrays.asList("http://allowed-origin.com"));
corsConfig.setMaxAge(8000L);
corsConfig.addAllowedMethod("PUT");
corsConfig.addAllowedHeader("Baeldung-Allowed");
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);
return new CorsWebFilter(source);
}
Example taken from Enabling CORS with a WebFilter

Spring MVC Test with HtmlUnit - How to test a JSON response

Background: Search form in a webapp, with an auto-complete/suggestion. Using jQuery's autocomplete, suggestions are shown after typing a few characters. These suggesitons are retrieved as JSON data from one of the webapp's controllers.
Issue: I am testing the application via the HtmlUnit Integration of Spring Test, which works fine for Text/Html Pages, but for the JSON responses here, the setup seems to fail (see error below).
Manually testing (the actual webapp) via Browser works and fetching JSON from "real" pages works as well ( see "json()" test) -> Should testing JSON responses work via HtmlUnit / Spring Test setup and if yes, what am I doing wrong?
Update (2017-06-21):
Using
#ResponseBody String
and building the JSON "manually" (not letting Spring automagically doing it) works; not really what I wanted, but at least I can properly test it this way ...
Test:
package my.project;
import com.gargoylesoftware.htmlunit.*;
import org.junit.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.*;
import org.springframework.test.*;
import org.springframework.web.context.support.GenericWebApplicationContext;
import javax.servlet.ServletException;
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = {TestWebAppConfig.class,
// avoid context caching
MyControllerIT.class})
#WebAppConfiguration
public class MyControllerIT {
protected static final String SERVER_URL = "http://localhost";
protected WebClient webClient;
protected MockHttpServletRequest request;
protected MockHttpServletResponse response;
#Autowired
protected GenericWebApplicationContext webApplicationContext;
#Before
public void setUp() throws Exception {
this.request = new MockHttpServletRequest();
this.request.setServerName("Gondor.Osgiliath");
this.response = new MockHttpServletResponse();
this.webClient = this.initClient();
}
protected final WebClient initClient() throws ServletException {
DefaultMockMvcBuilder mockMvcBuilder = MockMvcBuilders.webAppContextSetup(this.webApplicationContext);
MockMvc mockMvc = mockMvcBuilder.build();
WebClient webClient = new WebClient();
webClient.setWebConnection(new MockMvcWebConnection(mockMvc));
return webClient;
}
// https://stackoverflow.com/questions/2932857/html-handling-a-json-response
#Test
public void json() throws Exception {
webClient = new WebClient(BrowserVersion.INTERNET_EXPLORER, "some.proxy", 7890);
Page page = webClient.getPage("https://stackoverflow.com/users/flair/97901.json");
WebResponse webResponse = page.getWebResponse();
String contentType = webResponse.getContentType();
String contentAsString = webResponse.getContentAsString();
}
#Test
public void suggestShouldReturnJSON() throws Exception {
Page page = webClient.getPage(SERVER_URL + MyController.SUGGEST_URL + "?term=asdf");
WebResponse webResponse = page.getWebResponse();
String contentType = webResponse.getContentType();
String contentAsString = webResponse.getContentAsString();
}
#Test
public void suggestShouldReturnJSONViaMockMvc() throws Exception {
MockMvc springMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
ResultActions resultActions = springMvc.perform(MockMvcRequestBuilders
.get(MyController.SUGGEST_URL + "?term=asdf")
.accept(MediaType.APPLICATION_JSON_VALUE));
resultActions.andDo(MockMvcResultHandlers.print());
}
}
Controller:
package my.project;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Arrays;
import java.util.List;
#Controller
public class MyController {
public static final String SUGGEST_URL = "/suggest";
//http://api.jqueryui.com/autocomplete/#option-source
#RequestMapping(value = {SUGGEST_URL,}, produces = MediaType.APPLICATION_JSON_VALUE)
public #ResponseBody
List<Suggestion> suggest(#RequestParam(name = "term") String term) {
return Arrays.asList(new Suggestion("label1", "value1"), new Suggestion("label2", "value2"));
}
}
Config (Thymeleaf omitted for the moment):
package my.project;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
#EnableWebMvc
#ComponentScan(basePackages = {"my.project.*",})
#Configuration
#Import(ThymeleafConfig.class)
abstract class TestWebAppConfig extends WebMvcConfigurerAdapter {
}
Error:
com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException: 406 Not Acceptable for http://localhost/suggest?term=asdf
at com.gargoylesoftware.htmlunit.WebClient.throwFailingHttpStatusCodeExceptionIfNecessary(WebClient.java:571)
at com.gargoylesoftware.htmlunit.WebClient.getPage(WebClient.java:396)
at com.gargoylesoftware.htmlunit.WebClient.getPage(WebClient.java:304)
at com.gargoylesoftware.htmlunit.WebClient.getPage(WebClient.java:451)
at com.gargoylesoftware.htmlunit.WebClient.getPage(WebClient.java:436)
at my.project.MyControllerIt.suggestShouldReturnJSON(MyControllerIt.java:
...

Categories

Resources