io.netty.channel.unix.Errors$NativeIoException: newSocketStream(..) failed: Too many open files
I am searching for a long time on the net. But no use. Please help or try to give some ideas on how to achieve this.
spring-boot: 2.3.1.RELEASE
#Configuration
public class AppConfig {
#Bean
public WebClient webClient(WebClient.Builder builder) throws SSLException {
final SslContext sslContext =
SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
HttpClient httpClient =
HttpClient.create().secure(sslContextSpec -> sslContextSpec.sslContext(sslContext));
return builder.clientConnector(new ReactorClientHttpConnector(httpClient)).build();
}
#Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
#RestController
#RequestMapping("/test")
public class WebClientTestController {
private final WebClient webClient;
private final RestTemplate restTemplate;
public WebClientTestController(WebClient webClient, RestTemplate restTemplate) {
this.webClient = webClient;
this.restTemplate = restTemplate;
}
#GetMapping("/webclient")
public Mono<Object> main() {
new SimpleAsyncTaskExecutor()
.execute(
() -> {
for (int i = 0; i <= 65536; ++i) {
System.out.println(
webClient
.get()
.uri("http://127.0.0.1:8080/hello")
.retrieve()
.bodyToMono(String.class)
.block()
+ i);
}
});
return Mono.empty();
}
#GetMapping("/restTemplate")
public Mono<Object> main2() {
new SimpleAsyncTaskExecutor()
.execute(
() -> {
for (int i = 0; i <= 65536; ++i) {
System.out.println(
restTemplate.getForEntity("http://127.0.0.1:8080/hello", String.class).getBody()
+ i);
}
});
return Mono.empty();
}
}
#Component
public class GreetingHandler {
public Mono<ServerResponse> hello(ServerRequest request) {
return ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromValue("Hello, Spring!"));
}
}
#Configuration
public class GreetingRouter {
#Bean
public RouterFunction<ServerResponse> route(GreetingHandler greetingHandler) {
return RouterFunctions.route(
RequestPredicates.GET("/hello").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
greetingHandler::hello);
}
}
request in windows:
curl http://127.0.0.1:8080/test/restTemplate
...
Hello, Spring!65535
Hello, Spring!65536
curl http://127.0.0.1:8080/test/webclient
...
Hello, Spring!65535
Hello, Spring!65536
request in Linux:
curl http://127.0.0.1:8080/test/restTemplate
...
Hello, Spring!65535
Hello, Spring!65536
curl http://127.0.0.1:8080/test/webclient
Hello, Spring!10196
Hello, Spring!10197
Exception in thread "SimpleAsyncTaskExecutor-1" io.netty.channel.ChannelException: io.netty.channel.unix.Errors$NativeIoException: newSocketStream(..) failed: Too many open files
at io.netty.channel.unix.Socket.newSocketStream0(Socket.java:421)
at io.netty.channel.epoll.LinuxSocket.newSocketStream(LinuxSocket.java:319)
at io.netty.channel.epoll.LinuxSocket.newSocketStream(LinuxSocket.java:323)
at io.netty.channel.epoll.EpollSocketChannel.<init>(EpollSocketChannel.java:45)
at reactor.netty.resources.DefaultLoopEpoll.getChannel(DefaultLoopEpoll.java:45)
at reactor.netty.resources.LoopResources.onChannel(LoopResources.java:187)
at reactor.netty.resources.LoopResources.onChannel(LoopResources.java:169)
at reactor.netty.tcp.TcpResources.onChannel(TcpResources.java:215)
at reactor.netty.http.client.HttpClientConnect$HttpTcpClient.connect(HttpClientConnect.java:141)
at reactor.netty.tcp.TcpClientOperator.connect(TcpClientOperator.java:43)
at reactor.netty.tcp.TcpClientOperator.connect(TcpClientOperator.java:43)
at reactor.netty.tcp.TcpClientOperator.connect(TcpClientOperator.java:43)
at reactor.netty.tcp.TcpClientOperator.connect(TcpClientOperator.java:43)
at reactor.netty.tcp.TcpClient.connect(TcpClient.java:202)
at reactor.netty.http.client.HttpClientFinalizer.connect(HttpClientFinalizer.java:77)
at reactor.netty.http.client.HttpClientFinalizer.responseConnection(HttpClientFinalizer.java:94)
at org.springframework.http.client.reactive.ReactorClientHttpConnector.connect(ReactorClientHttpConnector.java:111)
at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.exchange(ExchangeFunctions.java:104)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.lambda$exchange$0(DefaultWebClient.java:338)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44)
at reactor.core.publisher.Mono.subscribe(Mono.java:4219)
at reactor.core.publisher.Mono.block(Mono.java:1678)
at com.example.demo.controller.WebClientTestController.lambda$main$0(WebClientTestController.java:38)
at java.lang.Thread.run(Thread.java:748)
Suppressed: java.lang.Exception: #block terminated with an error
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:99)
at reactor.core.publisher.Mono.block(Mono.java:1679)
... 2 more
Caused by: io.netty.channel.unix.Errors$NativeIoException: newSocketStream(..) failed: Too many open files
≈ ulimit -n
Where is wrong? I hope to learn more details.
Related
I have a communication problem in my spring boot microservices.
I created some services as well as eureka server, api gateway and config server.
I defined auth service connecting to api gateway for the process of authentication and authorization. I used this service as creating a user, logining and refreshing token.
After I created a user and login in auth service through the port number of api gateway, I tried to make a request to the order service like http://localhost:9090/order/placeorder or http://localhost:9090/order/{order_id} but I got 403 forbidden issue.
I knew there can be spring security problem among api gateway, auth service and order service but I couldn't find where the issue is.
Except for that, I cannot run any test method defined in OrderControllerTest because of this reason.
How can I fix these issues?
I shared some code snippets regarding security config defined in 2 services and api gateway and gateway filter located in api gateway.
Here is SecurityConfig in auth service.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationEntryPoint authenticationEntryPoint;
private final JWTAccessDeniedHandler accessDeniedHandler;
private final JwtUtils jwtUtils;
private final CustomUserDetailsService customUserDetailsService;
#Bean
public AuthenticationManager authenticationManager(final AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.headers().frameOptions().disable().and()
.csrf().disable()
.cors().and()
.authorizeRequests(auth -> {
auth.anyRequest().authenticated();
})
.formLogin().disable()
.httpBasic().disable()
.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(authenticationJwtTokenFilter(jwtUtils,customUserDetailsService), UsernamePasswordAuthenticationFilter.class)
.build();
}
#Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers("/authenticate/signup","/authenticate/login", "/authenticate/refreshtoken");
}
#Bean
public AuthTokenFilter authenticationJwtTokenFilter(JwtUtils jwtUtils, CustomUserDetailsService customUserDetailsService) {
return new AuthTokenFilter(jwtUtils, customUserDetailsService);
}
}
Here is SecurityConfig in api gateway.
#Configuration
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity serverHttpSecurity){
serverHttpSecurity.cors().and().csrf().disable()
.authorizeExchange(exchange -> exchange
.anyExchange()
.permitAll());
return serverHttpSecurity.build();
}
}
Here is the gatewayconfig in api gateway
#Configuration
#RequiredArgsConstructor
public class GatewayConfig {
private final JwtAuthenticationFilter filter;
#Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes().route("AUTH-SERVICE", r -> r.path("/authenticate/**").filters(f -> f.filter(filter)).uri("lb://AUTH-SERVICE"))
.route("PRODUCT-SERVICE", r -> r.path("/product/**").filters(f -> f.filter(filter)).uri("lb://PRODUCT-SERVICE"))
.route("PAYMENT-SERVICE", r -> r.path("/payment/**").filters(f -> f.filter(filter)).uri("lb://PAYMENT-SERVICE"))
.route("ORDER-SERVICE", r -> r.path("/order/**").filters(f -> f.filter(filter)).uri("lb://ORDER-SERVICE")).build();
}
}
Here is SecurityConfig in order service.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
#Bean
public SecurityFilterChain securityWebFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/order/**")
.access("hasAnyAuthority('ROLE_USER') or hasAnyAuthority('ROLE_ADMIN')");
return http.build();
}
}
Here is the OrderControllerTest shown below.
#SpringBootTest({"server.port=0"})
#EnableConfigurationProperties
#AutoConfigureMockMvc
#ContextConfiguration(classes = {OrderServiceConfig.class})
#ActiveProfiles("test")
public class OrderControllerTest {
#RegisterExtension
static WireMockExtension wireMockserver
= WireMockExtension.newInstance()
.options(WireMockConfiguration
.wireMockConfig()
.port(8080))
.build();
#Autowired
private OrderService orderService;
#Autowired
private OrderRepository orderRepository;
#Autowired
private MockMvc mockMvc;
private ObjectMapper objectMapper
= new ObjectMapper()
.findAndRegisterModules()
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
#Autowired
JwtUtils jwtUtils;
#BeforeEach
void setup() throws IOException {
getProductDetailsResponse();
doPayment();
getPaymentDetails();
reduceQuantity();
}
private void reduceQuantity() {
wireMockserver.stubFor(put(urlMatching("/product/reduceQuantity/.*"))
.willReturn(aResponse()
.withStatus(HttpStatus.OK.value())
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)));
}
private void getPaymentDetails() throws IOException {
wireMockserver.stubFor(get("/payment/order/1")
.willReturn(aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withStatus(HttpStatus.OK.value())
.withBody(copyToString(OrderControllerTest.class.getClassLoader().getResourceAsStream("mock/GetPayment.json"), defaultCharset()))));
}
private void doPayment() {
wireMockserver.stubFor(post(urlEqualTo("/payment"))
.willReturn(aResponse()
.withStatus(HttpStatus.OK.value())
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)));
}
private void getProductDetailsResponse() throws IOException {
wireMockserver.stubFor(get("/product/1")
.willReturn(aResponse()
.withStatus(HttpStatus.OK.value())
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody(copyToString(
OrderControllerTest.class
.getClassLoader()
.getResourceAsStream("mock/GetProduct.json"),
defaultCharset()))));
}
private OrderRequest getMockOrderRequest() {
return OrderRequest.builder()
.productId(1)
.paymentMode(PaymentMode.CASH)
.quantity(1)
.totalAmount(100)
.build();
}
#Test
#DisplayName("Place Order -- Success Scenario")
#WithMockUser(username = "User", authorities = { "ROLE_USER" })
void test_When_placeOrder_DoPayment_Success() throws Exception {
OrderRequest orderRequest = getMockOrderRequest();
String jwt = getJWTTokenForRoleUser();
MvcResult mvcResult
= mockMvc.perform(MockMvcRequestBuilders.post("/order/placeorder")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.header("Authorization", "Bearer " + jwt)
.content(objectMapper.writeValueAsString(orderRequest)))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
String orderId = mvcResult.getResponse().getContentAsString();
Optional<Order> order = orderRepository.findById(Long.valueOf(orderId));
assertTrue(order.isPresent());
Order o = order.get();
assertEquals(Long.parseLong(orderId), o.getId());
assertEquals("PLACED", o.getOrderStatus());
assertEquals(orderRequest.getTotalAmount(), o.getAmount());
assertEquals(orderRequest.getQuantity(), o.getQuantity());
}
#Test
#DisplayName("Place Order -- Failure Scenario")
#WithMockUser(username = "Admin", authorities = { "ROLE_ADMIN" })
public void test_When_placeOrder_WithWrongAccess_thenThrow_403() throws Exception {
OrderRequest orderRequest = getMockOrderRequest();
String jwt = getJWTTokenForRoleAdmin();
MvcResult mvcResult
= mockMvc.perform(MockMvcRequestBuilders.post("/order/placeorder")
.header("Authorization", "Bearer " + jwt)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(orderRequest)))
.andExpect(MockMvcResultMatchers.status().isForbidden())
.andReturn();
}
#Test
//#WithMockUser(username = "Admin", authorities = { "ROLE_ADMIN" })
public void test_WhenGetOrder_Success() throws Exception {
String jwt = getJWTTokenForRoleUser();
MvcResult mvcResult
= mockMvc.perform(MockMvcRequestBuilders.get("/order/1")
.header("Authorization", "Bearer " + jwt)
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
String actualResponse = mvcResult.getResponse().getContentAsString();
Order order = orderRepository.findById(1l).get();
String expectedResponse = getOrderResponse(order);
assertEquals(expectedResponse,actualResponse);
}
#Test
//#WithMockUser(username = "Admin", authorities = { "ROLE_ADMIN" })
public void testWhen_GetOrder_Order_Not_Found() throws Exception {
String jwt = getJWTTokenForRoleAdmin();
MvcResult mvcResult
= mockMvc.perform(MockMvcRequestBuilders.get("/order/4")
.header("Authorization", "Bearer " + jwt)
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(MockMvcResultMatchers.status().isNotFound())
.andReturn();
}
private String getOrderResponse(Order order) throws IOException {
OrderResponse.PaymentDetails paymentDetails
= objectMapper.readValue(
copyToString(
OrderControllerTest.class.getClassLoader()
.getResourceAsStream("mock/GetPayment.json"
),
defaultCharset()
), OrderResponse.PaymentDetails.class
);
paymentDetails.setPaymentStatus("SUCCESS");
OrderResponse.ProductDetails productDetails
= objectMapper.readValue(
copyToString(
OrderControllerTest.class.getClassLoader()
.getResourceAsStream("mock/GetProduct.json"),
defaultCharset()
), OrderResponse.ProductDetails.class
);
OrderResponse orderResponse
= OrderResponse.builder()
.paymentDetails(paymentDetails)
.productDetails(productDetails)
.orderStatus(order.getOrderStatus())
.orderDate(order.getOrderDate())
.amount(order.getAmount())
.orderId(order.getId())
.build();
return objectMapper.writeValueAsString(orderResponse);
}
private String getJWTTokenForRoleUser(){
var loginRequest = new LoginRequest("User1","user1");
String jwt = jwtUtils.generateJwtToken(loginRequest.getUsername());
return jwt;
}
private String getJWTTokenForRoleAdmin(){
var loginRequest = new LoginRequest("Admin","admin");
String jwt = jwtUtils.generateJwtToken(loginRequest.getUsername());
return jwt;
}
#Data
#AllArgsConstructor
#NoArgsConstructor
public class LoginRequest {
private String username;
private String password;
}
}
Here is the repo : Link
Here are the screenshots : Link
To run the app,
1 ) Run Service Registery (Eureka Server)
2 ) Run config server
3 ) Run zipkin and redis through these commands shown below on docker
docker run -d -p 9411:9411 openzipkin/zipkin
docker run -d --name redis -p 6379:6379 redis
4 ) Run api gateway
5 ) Run other services
I am writing a RestTemplateErrorHandler.java class to handle RestTemplate exception.
Scenarion: I have a jar file in my class path which has the resttemplate exception handling functionality.
Jar name-
aws-common-config.jar
|- RestTemplateConfig.class
|- RestTemplateErrorHandler.class
RestTemplateConfig.class
#EnableConfigurationProperties({ HttpProperties.class, MksProperties.class })
#RequiredArgsConstructor
public class RestTemplateConfig {
private static final Logger LOG = LogManager
.getLogger(RestTemplateConfig.class);
private final HttpProperties httpProperties;
private final MksProperties mksProperties;
private final RestTemplateErrorHandler restTemplateHandler;
#Bean("NoSSLRestTemplate")
#ConditionalOnProperty(name = "aws.common.resttemplate.enabled", havingValue = "true", matchIfMissing = false)
#Primary
public RestTemplate configRestTemplate()
throws CertificateException, IOException, NoSuchAlgorithmException,
KeyStoreException, KeyManagementException {
RestTemplate restTemplate;
restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory(
getRestTemplateFromOkhttp3Template()));
restTemplate.setErrorHandler(restTemplateHandler);
return restTemplate;
}
private OkHttpClient getRestTemplateFromOkhttp3Template() {
//some code
}
#Bean("AWSRestTemplate")
#ConditionalOnProperty(name = "aws.common.resttemplate.awsenabled", havingValue = "true", matchIfMissing = false)
public RestTemplate configureRestTemplateforMTLS() throws Exception {
RestTemplate restTemplate = new RestTemplate();
try {
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(
httpClient());
requestFactory.setConnectTimeout(
httpProperties.getConnect().getTimeout());
requestFactory.setReadTimeout(
httpProperties.getConnect().getReadTimeout());
restTemplate.setRequestFactory(requestFactory);
restTemplate.setErrorHandler(restTemplateHandler);
} catch (Exception ex) {
LOG.error("Exception occurred while creating MTLS Rest Template ",
ex);
throw ex;
}
return restTemplate;
}
private CloseableHttpClient httpClient() throws Exception {
//some code
}
}
RestTemplateErrorHandler.java -
#Component
#Slf4j
public class RestTemplateErrorHandler implements ResponseErrorHandler {
private static final int BUFFER_SIZE = 200;
#Override
public boolean hasError(ClientHttpResponse clientHttpResponse)
throws IOException {
return clientHttpResponse.getStatusCode() != HttpStatus.OK;
}
#Override
public void handleError(ClientHttpResponse clientHttpResponse)
throws IOException {
String errMessage = getErrMessage(clientHttpResponse);
HttpStatus status = clientHttpResponse.getStatusCode();
switch (status) {
case BAD_REQUEST:
throw new CustomException(errMessage,
ErrorResponse.INVALID_REQUEST);
case NOT_FOUND:
throw new CustomException(errMessage, ErrorResponse.NOT_FOUND);
case SERVICE_UNAVAILABLE:
throw new CustomException(errMessage, ErrorResponse.TIME_OUT);
case METHOD_NOT_ALLOWED:
case INTERNAL_SERVER_ERROR:
default:
throw new CustomException(errMessage,
ErrorResponse.INTERNAL_SERVER_ERROR);
}
}
private String getErrMessage(ClientHttpResponse clientHttpResponse) {
try {
return StreamUtils.copyToString(clientHttpResponse.getBody(),
StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("[RestTemplate]read body", e);
return "Error reading response body";
}
}
}
application.properties
aws.common:
appId: TTP
resttemplate:
enabled: true
awsenabled: true
As you could see here
#Override
public boolean hasError(ClientHttpResponse clientHttpResponse)
throws IOException {
return clientHttpResponse.getStatusCode() != HttpStatus.OK;
}
HttpStatus 201 Created is not being handled here.
To Handle this I have written both the java classes in my application.
Now I want to exclude the aws-common-config.jar file's RestTemplateConfig.class and want to load the one I have written in my application.
But everytime aws-common-config.jar file's RestTemplateConfig.class file gets loaded during the startup.
here is my configuration file:
#EnableConfigurationProperties({ HttpProperties.class, MksProperties.class })
#Configuration
#RequiredArgsConstructor
#Component
public class RestTemplateConfiguration {
//body code is same as jar file
}
My RestTemplateErrorHandler.class -
#Component
#Slf4j
public class RestTemplateErrorHandler implements ResponseErrorHandler {
//same code as Jar file
//have created my own CustomException.class and ErrorReponse.class and imported them
}
Here I am calling API using rest template:
#Qualifier("AWSRestTemplate")
#Autowired
private RestTemplate restTemplateAWS;
AWSRestTemplate.exchange(url, HttpMethod.POST, entity, Response.class);
Had tried with
#import
and
#ComponentScan(basePackages = {"com.efx.ews.es"}, excludeFilters = {#ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
value = {RestTemplateConfig.class}
)
})
but nothing help.
Can Someone please help me here how can I exclude jar file config class and lode my RestTemplateConfiguration.class.
I can't modify the jar file code and can't remove the jar file dependency as some other features are getting used.
I try to create a simple authentication schema with Reactive approach.
I've created a project from scratch with dependencies to reactive components and security.
Introduced Configuration file where I configure authentication manager and security context repository.
The problem is that I notice, that Mono injected into controller initiates double requests to "login" endpoint.
Why does it happens and how to prevent it?
Here is the code of configuration:
#Configuration
#EnableWebFluxSecurity
public class WebFluxSecurityConfiguration {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private SecurityContextRepository securityContextRepository;
#Bean
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http) {
return http
.csrf().disable()
.cors().disable()
.httpBasic().disable()
.logout().disable()
.formLogin().disable()
.authorizeExchange()
.pathMatchers("/login").permitAll()
.anyExchange().authenticated()
.and()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
.build();
}
}
Here is the authentication manager
#Component
public class AuthenticationManager implements ReactiveAuthenticationManager {
private final WebClient webClient;
public AuthenticationManager(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder.baseUrl("http://localhost:8080/login")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return webClient.post()
.header("Authorization","Bearer bla-bla")
.retrieve()
.bodyToMono(String.class)
.map(r->new AuthenticatedUser());
}
}
And here is a security context repository
#Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
private static final String TOKEN_PREFIX = "Bearer ";
private AuthenticationManager authenticationManager;
List<PathPattern> pathPatternList;
public SecurityContextRepository(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
PathPattern pathPattern1 = new PathPatternParser().parse("/login");
pathPatternList = new ArrayList<>();
pathPatternList.add(pathPattern1);
}
#Override
public Mono load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
RequestPath path = request.getPath();
if (pathPatternList.stream().anyMatch(pathPattern -> pathPattern.matches(path.pathWithinApplication()))) {
System.out.println(path.toString() + " path excluded");
return Mono.empty();
}
System.out.println("executing logic for " + path.toString() + " path");
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
String authToken = null;
//test
authHeader = "Bearer bla-bla";
//~test
if (authHeader != null && authHeader.startsWith(TOKEN_PREFIX)) {
authToken = authHeader.replace(TOKEN_PREFIX, "");
}else {
System.out.println("couldn't find bearer string, will ignore the header.");
}
if (authToken != null) {
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
return this.authenticationManager.authenticate(auth).map((authentication) -> new SecurityContextImpl(authentication));
} else {
return Mono.empty();
}
}
#Override
public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) {
return null;
}
}
link to repository of full project
A quick solution to this particular case is to implement caching of WebClient result. So inside of post method to authorization server for retrieval of user profile I just introduced a method cache().
return webClient.post()
.header("Authorization","Bearer bla-bla")
.retrieve()
.bodyToMono(String.class)
.cache()
.map(r->new AuthenticatedUser());
That helped to avoid repeating queries to authorization endpoint during the request.
I'm trying to mock the following method:
public Mono<PResponse> pay(final String oId,final Double amount) {
return webClient
.put()
.uri("/order/{oId}/amount/{amount}",oId,amount)
.body(BodyInserts
.fromObject(PRequest))
.exchange()
.flatMap(
response -> {
if(response.statusCode().is4xxClientError()) {
// call error Function
} else {
return response
.bodyToMono(PResponse.class)
.flatMap(pResponse -> {
return Mono.just(pResposne)
});
}
}
);
}
For your information, webClient is a private Instance.
You can use MockWebServer.Here is an example, using code from this blog post:
Service
class ApiCaller {
private WebClient webClient;
ApiCaller(WebClient webClient) {
this.webClient = webClient;
}
Mono<SimpleResponseDto> callApi() {
return webClient.put()
.uri("/api/resource")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "customAuth")
.syncBody(new SimpleRequestDto())
.retrieve()
.bodyToMono(SimpleResponseDto.class);
}
}
Test
class ApiCallerTest {
private final MockWebServer mockWebServer = new MockWebServer();
private final ApiCaller apiCaller = new ApiCaller(WebClient.create(mockWebServer.url("/").toString()));
#AfterEach
void tearDown() throws IOException {
mockWebServer.shutdown();
}
#Test
void call() throws InterruptedException {
mockWebServer.enqueue(
new MockResponse()
.setResponseCode(200)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setBody("{\"y\": \"value for y\", \"z\": 789}")
);
SimpleResponseDto response = apiCaller.callApi().block();
assertThat(response, is(not(nullValue())));
assertThat(response.getY(), is("value for y"));
assertThat(response.getZ(), is(789));
RecordedRequest recordedRequest = mockWebServer.takeRequest();
//use method provided by MockWebServer to assert the request header
recordedRequest.getHeader("Authorization").equals("customAuth");
DocumentContext context = JsonPath.parse(recordedRequest.getBody().inputStream());
//use JsonPath library to assert the request body
assertThat(context, isJson(allOf(
withJsonPath("$.a", is("value1")),
withJsonPath("$.b", is(123))
)));
}
}
I try to authenticate user (it works) and get a user token from the context and it doesn't work.
I have a simple microservice application as a my pet-project and use a WebFlux as a web-framework. I tried to debug ReactiveSecurityContextHolder#getContext and inside flatMap I see my user token inside context, but in my application I steel have an empty context.
SecurityConfiguration.java
#EnableWebFluxSecurity
public class SecurityConfiguration {
#Bean
public SecurityWebFilterChain securityWebFilterChain(
AuthenticationWebFilter authenticationWebFilter,
ServerHttpSecurity http) {
return http
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.authorizeExchange()
.anyExchange().authenticated()
.and()
.build();
}
#Bean
public AuthenticationWebFilter authenticationWebFilter(
SecurityTokenBasedAuthenticationManager authenticationManager,
TokenAuthenticationConverter tokenAuthenticationConverter) {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(tokenAuthenticationConverter);
authenticationWebFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
return authenticationWebFilter;
}
}
ReactiveAuthenticationManager.java
#Slf4j
#Component
#AllArgsConstructor
public class SecurityTokenBasedAuthenticationManager implements ReactiveAuthenticationManager {
private final AuthWebClient authWebClient;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication)
.switchIfEmpty(Mono.defer(this::raiseBadCredentials))
.cast(Authorization.class)
.flatMap(this::authenticateToken)
.map(user ->
new Authorization(user, (AuthHeaders) authentication.getCredentials()));
}
private <T> Mono<T> raiseBadCredentials() {
return Mono.error(new TokenValidationException("Invalid Credentials"));
}
private Mono<User> authenticateToken(Authorization authenticationToken) {
AuthHeaders authHeaders = (AuthHeaders) authenticationToken.getCredentials();
return Optional.of(authHeaders)
.map(headers -> authWebClient.validateUserToken(authHeaders.getAuthToken(), authHeaders.getRequestId())
.doOnSuccess(user -> log
.info("Authenticated user " + user.getUsername() + ", setting security context")))
.orElseThrow(() -> new MissingHeaderException("Authorization is missing"));
}
}
SecurityUtils.java
#Slf4j
public class SecurityUtils {
public static Mono<AuthHeaders> getAuthHeaders() {
return getSecurityContext()
.map(SecurityContext::getAuthentication)
.map(Authentication::getCredentials)
.cast(AuthHeaders.class)
.doOnSuccess(authHeaders -> log.info("Auth headers: {}", authHeaders));
}
private static Mono<SecurityContext> getSecurityContext() {
return ReactiveSecurityContextHolder.getContext();
}
}
WebClient building
(...)
WebClient.builder()
.baseUrl(url)
.filter(loggingFilter)
.defaultHeaders(httpHeaders ->
getAuthHeaders()
.doOnNext(headers -> httpHeaders.putAll(
Map.of(
REQUEST_ID, singletonList(headers.getRequestId()),
AUTHORIZATION, singletonList(headers.getAuthToken())))))
.build()
(...)
The main problem is inside a building of my WebClient - I expect, that I'll have complete webclient with requested header, but as I described above - I have an empty context inside SecurityUtils.java
So, after investigating and debugging I found a reason using combining streams into single one. IMHO, this is not quite reason for this issue, but now it works.
(...)
getAuthHeaders()
.flatMap(authHeaders -> buildWebClient(url, authHeaders)
.get()
(...)