I want to invoke the below method when the given route is called.
How do I modify/rewrite the path of the route?
URL in the postman/browser: "http://localhost:8080/compliance/status/{id}"
Actual URL to be called: https://m.com/v1/myPage/getByBillOfLadingId/{id}
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/**")
.uri("https://m.com/v1/myPage/getByBillOfLadingId/"))
.build();
}
Use the RewritePath filter
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/**")
.filters(f -> f.rewritePath(".*", "/v1/myPage/getByBillOfLadingId/"))
.uri("https://m.com"))
.build();
}
The SetPath filter may be useful as well.
Related
Preamble
Since there are a lot of questions on StackOverflow about this already, I first want to ensure that this is not a duplicate and differentiate.
This is about
Having 2(or more) different AuthenticationProviders in 2 different AuthenticationManagers to be used on different routes.
Using the methods in Spring Security 5.5 not 3.x
Using a non XML configuration based approach
So the question is not about:
How to include several AuthenticationProvideres in on AuthenticationManager to offer "alternative authentications" (which most questions tend to be)
Case
Assume one has 2 custom AuthenticationProviders: CATApiTokenProvider and DOGApiTokenProvider. It is by design that we not talk about AOuth/JWT/Basic/Form providers, since they offer shortcuts.
Now we have 2 REST API endpoints /dog/endpoint and /cat/endpoint.
Question
How would one properly implement this today, with Spring Security 5.5:
We want the authentication provider CATApiTokenProvider to only be able to authenticate requests on /cat/endpoint
We want the authentication provider DOGApiTokenProvider to only be able to authenticate requests on /dog/endpoint
So one cannot authenticate with a cat token on /dog/endpoint and neither with a dog token on /cat/endpoint.
My Ideas/Approaches
a) I understand that since I have custom Cat/Dog filters, one can use the AuthenticationManagerResolver and pass one instance into the filter when creating the bean. This resolver might look like
public AuthenticationManagerResolver<HttpServletRequest> resolver()
{
return request -> {
if (request.getPathInfo().startsWith("/dog/")) {
try {
return ???;
} catch (Exception exception) {
log.error(exception);
}
}
if (request.getPathInfo().startsWith("/cat/")) {
try {
return ???;
} catch (Exception exception) {
log.error(exception);
}
}
};
}
Two questions with that would be:
how to return different authentication managers here? How to instantiate 2 different AM with each one CatAP and DogAP? Currently I use public void configure(AuthenticationManagerBuilder auth) but as far as I understand, I would only configure 'the one' AuthenticationManager and I could add DogAP and CatAP there, but this would let as having 1 AM with 2 APs, so when using this AM i could auth with the dog token on the cat endpoint
is this really the right way to implement this? I would have expected to be able to provide the AM on the SecurityConfiguration level
b) Somehow instantiate 2 different AuthenticationManagers and then use the SecurityConfiguration to assign them to different matchers.
Two questions:
what is the right way to spawn 2 different AMs with different providers?
I cannot understand how I would add an AM for a spec
http.authorizeRequests()
.antMatchers("/dog/**")
.?
You can either publish multiple filter chains or wire your own AuthenticationFilter with an AuthenticationManagerResolver
You may use AuthenticationManagerResolver to return different AuthenticationManagers. Since Spring Security 5.4.0, we don't need to extend the WebSecurityConfigurerAdapter to configure our SecurityFilterChain anymore, you can instead define a bean of SecurityFilterChain type.
I'll go into detail on wiring your own AuthenticationFilter.
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Bean
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated());
http.addFilterBefore(apiAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
private AuthenticationFilter apiAuthenticationFilter() {
AuthenticationFilter authenticationFilter = new AuthenticationFilter(new ApiAuthenticationManagerResolver(), new BasicAuthenticationConverter());
authenticationFilter.setSuccessHandler((request, response, authentication) -> {});
return authenticationFilter;
}
public static class ApiAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {
private final Map<RequestMatcher, AuthenticationManager> managers = Map.of(
new AntPathRequestMatcher("/dog/**"), new DogAuthenticationProvider()::authenticate,
new AntPathRequestMatcher("/cat/**"), new CatAuthenticationProvider()::authenticate
);
#Override
public AuthenticationManager resolve(HttpServletRequest request) {
for (Map.Entry<RequestMatcher, AuthenticationManager> entry : managers.entrySet()) {
if (entry.getKey().matches(request)) {
return entry.getValue();
}
}
throw new IllegalArgumentException("Unable to resolve AuthenticationManager");
}
}
public static class DogAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication.getName().endsWith("_dog")) {
return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(),
AuthorityUtils.createAuthorityList("ROLE_DOG"));
}
throw new BadCredentialsException("Username should end with _dog");
}
#Override
public boolean supports(Class<?> authentication) {
return true;
}
}
public static class CatAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication.getName().endsWith("_cat")) {
return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(),
AuthorityUtils.createAuthorityList("ROLE_CAT"));
}
throw new BadCredentialsException("Username should end with _cat");
}
#Override
public boolean supports(Class<?> authentication) {
return true;
}
}
}
In the example above, we have two AuthenticationProviders, one for cat and other for dog. They are resolved upon an AntPathRequestMatcher matching for both /dog/** and /cat/** endpoints, inside the ApiAuthenticationManagerResolver. There is no need to defined an AuthenticationManager for each dog and cat, since AuthenticationProvider/Manager have the same interface.
The ApiAuthenticationManagerResolver is then wired inside an AuthenticationFilter in your filter chain.
You can also define two different filter chains for each endpoint, like so:
#Bean
public SecurityFilterChain dogApiSecurity(HttpSecurity http) throws Exception {
http.requestMatchers((matchers) -> matchers
.antMatchers("/dog/**"));
http.authorizeRequests((authz) -> authz
.anyRequest().authenticated());
http.httpBasic();
http.authenticationProvider(new DogAuthenticationProvider());
return http.build();
}
#Bean
public SecurityFilterChain catApiSecurity(HttpSecurity http) throws Exception {
http.requestMatchers((matchers) -> matchers
.antMatchers("/cat/**"));
http.authorizeRequests((authz) -> authz
.anyRequest().authenticated());
http.httpBasic();
http.authenticationProvider(new CatAuthenticationProvider());
return http.build();
}
Please, when defining multiple filter chains, the ordering is important, make use of the #Order annotation in those scenarios.
When you do http.requestMatcher(new AntPathRequestMatcher("/endpoint/**")); you are telling Spring Security to only call the filter chain when the request matches that path.
There is also a ticket within Spring Security's repository to provide a AuthenticationManagerResolver implementation which accepts Map<RequestMatcher, AuthenticationManager>, it would be nice if you think it makes sense, give a thumbs up there.
This is what I want to do:
call first rest API
if first succeeds call seconds rest API
if both are successful -> create an aggregated response
I'm using RxJava2 in Micronaut.
This is what I have but I'm not sure it's correct. What would happen if the first or second API call fails?
#Singleton
public class SomeService {
private final FirstRestApi firstRestApi;
private final SecondRestApi secondRestApi;
public SomeService(FirstRestApi firstRestApi, SecondRestApi secondRestApi) {
this.firstRestApi = firstRestApi;
this.secondRestApi = secondRestApi;
}
public Single<AggregatedResponse> login(String data) {
Single<FirstResponse> firstResponse = firstRestApi.call(data);
Single<SecondResponse> secondResponse = secondRestApi.call();
return firstResponse.zipWith(secondResponse, this::convertResponse);
}
private AggregatedResponse convertResponse(FirstResponse firstResponse, SecondResponse secondResponse) {
return AggregatedResponse
.builder()
.something1(firstResponse.getSomething1())
.something2(secondResponse.getSomething2())
.build();
}
}
This should be as simple as
public Single<AggregatedResponse> login(String data) {
return firstRestApi.call(data)
.flatMap((firstResponse) -> secondRestApi.call().map((secondResponse) -> {
return Pair.create(firstResponse, secondResponse);
})
.map((pair) -> {
return convertResponse(pair.getFirst(), pair.getSecond());
});
}
In which case you no longer need zipWith. Errors just go to error stream as usual.
I am have two versions of a Webflux/Reactor handler class. This class mimics a user sign up use case - as far as the business logic is concerned.
The first version of the class relies upon a Mono<User> whereas the second version uses a plain User.
First version of the class: this is the version relying on a Mono<User> argument down the chain. Notice the top level public method createUser uses a userMono.
#Component
#RequiredArgsConstructor
public class UserHandler {
private final #NonNull UserRepository userRepository;
private final #NonNull UserValidator userValidator;
public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
Mono<User> userMono = serverRequest.bodyToMono(User.class).cache();
return validateUser(userMono)
.switchIfEmpty(validateEmailNotExists(userMono))
.switchIfEmpty(saveUser(userMono))
.single();
}
private Mono<ServerResponse> validateUser(Mono<User> userMono) {
return userMono
.map(this::computeErrors)
.filter(AbstractBindingResult::hasErrors)
.flatMap(err ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(err.getAllErrors()))
);
}
private AbstractBindingResult computeErrors(User user) {
AbstractBindingResult errors = new BeanPropertyBindingResult(user, User.class.getName());
userValidator.validate(user, errors);
return errors;
}
private Mono<ServerResponse> validateEmailNotExists(Mono<User> userMono) {
return userMono
.flatMap(user -> userRepository.findByEmail(user.getEmail()))
.flatMap(existingUser ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject("User already exists."))
);
}
private Mono<ServerResponse> saveUser(Mono<User> userMono) {
return userMono
.flatMap(userRepository::save)
.flatMap(newUser -> status(CREATED)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(newUser))
);
}
}
Second version of the class: that is the version relying on a User argument.
#Component
#RequiredArgsConstructor
public class UserHandler {
private final #NonNull UserRepository userRepository;
private final #NonNull UserValidator userValidator;
public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
return serverRequest
.bodyToMono(User.class)
.flatMap(user ->
validateUser(user)
.switchIfEmpty(validateEmailNotExists(user))
.switchIfEmpty(saveUser(user))
.single()
);
}
private Mono<ServerResponse> validateUser(User user) {
return Mono.just(new BeanPropertyBindingResult(user, User.class.getName()))
.doOnNext(err -> userValidator.validate(user, err))
.filter(AbstractBindingResult::hasErrors)
.flatMap(err ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(err.getAllErrors()))
);
}
private Mono<ServerResponse> validateEmailNotExists(User user) {
return userRepository.findByEmail(user.getEmail())
.flatMap(existingUser ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject("User already exists."))
);
}
private Mono<ServerResponse> saveUser(User user) {
return userRepository.save(user)
.flatMap(newUser -> status(CREATED)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(newUser))
);
}
}
Now my questions:
What are the pros and cons of each of the two versions?
Which of the two would you recommend keeping?
Any feedback, advice and opinion welcome.
you want feedback here is my feedback
your methods dont really make sense. If you look at the declarations of your methods without the containing code.
private Mono<ServerResponse> validateUser(User user)
this makes no sense, this method should validate a user but you return a ServerResponse? In my opinion a validation should first takes place, and return some sort of boolean, or a list of validation errors.
I do not recommend any of the solutions you have presented, you should instead looking into and start using Mono.error instead of doing switchIfEmpty
You should seperate response building from validation logic.
What happens if validation rules change? or you want other responses based on what validation fails? right now they are together.
you can already see that you are returning the same bad request in two places but with different error messages. Duplications
This is my opinion and what i would do:
receive a request
map request to a user (bodyToMono)
validate the user in a method that vill return a list containing number of errors
check this list if user validation has failed, and if failed map the mono user to a mono error containing a illegalArgumentException with some sort of text of the error.
map the exception in the mono error to a status code
if validation passes save the user in a Mono.doOnSuccess block
this to me is much more clear and predictable with separations of return codes, and validation logic.
Using Thomas Andolf's advice together with that of other users, I came up with the following implementation:
#Component
#RequiredArgsConstructor
public class UserHandler {
private final #NonNull UserRepository userRepository;
private final #NonNull UserValidator userValidator;
public Mono<ServerResponse> findUsers(ServerRequest serverRequest) {
return ok()
.contentType(APPLICATION_JSON)
.body(userRepository.findAll(), User.class);
}
public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
return serverRequest.bodyToMono(User.class)
.flatMap(this::validate)
.flatMap(this::validateEmailNotExists)
.flatMap(this::saveUser)
.flatMap(newUser -> status(CREATED)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromValue(newUser))
)
.onErrorResume(ValidationException.class, e -> status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromValue(e.getErrors()))
)
.onErrorResume(DuplicateUserException.class, e -> status(CONFLICT)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromValue(e.getErrorMessage()))
);
}
private Mono<User> validateEmailNotExists(User user) {
return userRepository.findByEmail(user.getEmail())
.flatMap(userMono -> Mono.<User>error(new DuplicateUserException("User already exists")))
.switchIfEmpty(Mono.just(user));
}
private Mono<User> saveUser(User user) {
return userRepository.save(user);
}
private Mono<User> validate(User user) {
AbstractBindingResult errors = computeErrors(user);
return errors.hasErrors() ? Mono.error(new ValidationException(errors.getAllErrors())) : Mono.just(user);
}
private AbstractBindingResult computeErrors(User user) {
AbstractBindingResult errors = new BeanPropertyBindingResult(user, User.class.getName());
userValidator.validate(user, errors);
return errors;
}
}
It relies upon Mono.error, custom exceptions, and the onErrorResume() operator. It is equivalent to the two implementations in the question but somewhat leaner.
I am confused with CircuitBreaker using WebClient. When the dependent service is down it, the fallback is not executed.
Do I need additional configuration?
For the CircuitBreaker using RestTemplate this is working without any further configuration. See my example code here: https://github.com/altfatterz/resilience4j-demo
Here is my example
#GetMapping("/")
public Mono<String> hello() {
return webClient.build()
.get().uri(uriBuilder -> uriBuilder
.scheme("http")
.host("slow-service").path("/slow")
.build())
.retrieve().bodyToMono(String.class).transform(it -> {
CircuitBreaker cb = circuitBreakerFactory.create("slow");
return cb.run(() -> it, throwable -> Mono.just("fallback"));
});
}
using the following configuration:
#Bean
public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() {
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.circuitBreakerConfig(ofDefaults())
.timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(3)).build()).build());
}
#Bean
#LoadBalanced
public WebClient.Builder webClient() {
return WebClient.builder();
}
The problem is here https://github.com/altfatterz/resilience4j-demo/blob/master/slow-service-reactive-client/src/main/java/com/example/SlowServiceReactiveClientApplication.java#L27
and here https://github.com/altfatterz/resilience4j-demo/blob/master/slow-service-reactive-client/src/main/java/com/example/SlowServiceReactiveClientApplication.java#L43
Since you are using WebClient you need to use a ReactiveCircuitBreakerFactory and a ReactiveCircuitBreaker.
In our controller class we reach out to another service to get some data :
Future<JsonNode> futureSite = someClient.getSite(siteId, queryParams);
return FutureConverters.toJava(futureSite).thenApplyAsync((siteJson) -> {
Site site = Json.fromJson(siteJson, Site.class);
try {
return function.apply(site);
} catch (RequestException e) {
return e.result;
}
}).exceptionally(throwable -> {
if(throwable instanceof OurClientException) {
if(((OurClientException) throwable).httpStatusCode == 404) {
return entityNotFound("Site", siteId);
}
}
return null;
});
What we notice is that context which is set in unit tests (we use scalatest-play) is lost and becomes null after we make the Async call (FutureConverters.toJava(futureSite).thenApplyAsync((siteJson), as t is on a separate thread.
Which causes problem down in the controller code, where we use the above function ... request() would now throw a runtime exception saying there is no context available.
How can we preserve the context ?
You should inject play.libs.concurrent.HttpExecutionContext to your controller and then specify current context as second argument for CompletionStage#thenApplyAsync(..,..).
public class Application extends Controller {
#Inject HttpExecutionContext ec;
public CompletionStage<Result> index() {
someCompletableFuture.supplyAsync(() -> {
// do something with request()
}, ec.current());
}}
P.S. https://www.playframework.com/documentation/2.5.x/JavaAsync#Using-CompletionStage-inside-an-Action
I addition to Nick's V answer.
If you are building a non-blocking app using Play Java API, it might become quite cumbersome to inject HttpExecutionContext and pass ec.current()) every time you need to call methods on CompletionStage.
To make life easier you can use a decorator, which will preserve the context between calls.
public class ContextPreservingCompletionStage<T> implements CompletionStage<T> {
private HttpExecutionContext context;
private CompletionStage<T> delegate;
public ContextPreservingCompletionStage(CompletionStage<T> delegate,
HttpExecutionContext context) {
this.delegate = delegate;
this.context = context;
}
...
}
So you will need to pass context only once:
return new ContextPreservingCompletionStage<>(someCompletableFuture, context)
.thenCompose(something -> {...});
.thenApply(something -> {...});
Instead of
return someCompletableFuture.thenComposeAsync(something -> {...}, context.current())
.thenApplyAsync(something -> {...}, context.current());
That is particularly useful if you are building a multi-tier app, and passing CompletionStages between different classes.
Full decorator implementation example is here.