I am using spring feign client for making http requests.
Fiegn configuration class
DefaultConfig.class
public class DefaultConfig {
#Bean
public OkHttpClient client() {
return new OkHttpClient();
}
#Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
#Bean
public Decoder feignDecoder() {
return new JacksonDecoder();
}
#Bean
public Encoder feignEncoder() {
return new JacksonEncoder();
}
#Bean
public RequestInterceptor requestInterceptor() {
return template -> {
template.header(Authorization, apiKey);
};
}
}
My client interface:
#FeignClient(name = "default", url = "${base-url}",
configuration = DefaultConfig.class)
public interface {
#PostMapping(value = "/users/")
Response createUser(#RequestBody Map<String, ?> requestBody);
#GetMapping(value = "/users/{id}")
Response getUserDetails(#PathVariable String id);
}
The thing is now I need to use different authorization key for GET and POST request. In the configuration class I need to differentiate the request by url called. How can we get the url the requester interceptor or any other way we can achieve this. We can create separate interceptor for this but I try to use the same interceptor for both cases.
Everything you need seems to be in the template object.
#Bean
public RequestInterceptor requestInterceptor() {
return template -> {
if ("GET".equals(template.method()) {
template.header(Authorization, apiKey);
} else {
template.header(Authorization, differentApiKey);
}
};
}
Everything you need seems to be in the code below.
#Bean.
public RequestInterceptor requestInterceptor() {
return template -> {
template.request().url() // this is request url which is from feign!
template.requsst().httpMethod() // this is request method!
};
}
Related
I have a service that is holding an WebClient instance:
#Service
#Getter
public class SomeApiWebClient {
private final WebClient webClient;
private final String someApiUrl;
public SomeApiWebClient(#Value("${some.api.url}") String someApiUrl) {
this.someApiUrl= someApiUrl;
this.webClient = WebClient.builder()
.baseUrl(someApiUrl)
.defaultHeaders(getDefaultHttpHeaders())
.build();
}
public Consumer<HttpHeaders> getDefaultHttpHeaders() {
return headers -> {
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setContentType(MediaType.APPLICATION_JSON);
};
}
}
To call this API from UAT/PROD environments - this is just fine. But to call it from our local machines, we need to use Client-ID and Secret HTTP headers:
public Consumer<HttpHeaders> getDefaultHttpHeaders() {
return headers -> {
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add("Client-Id", "someId");
headers.add("Secret", "someSecret");
};
}
I have put the someId and someSecret values into application-dev.properties:
some.api.client-id=someId
some.api.token=someSecret
Now I would like to use #Configuration combined with #ConditionalOnProperty({"some.api.client-id", "some.api.token"}) to intercept and add a filter to MDSWebClient that will add those headers when the some.api.client-id and some.api.token are present.
I have tried doing something like this:
#Configuration
#ConditionalOnProperty({"some.api.client-id", "some.api.token"})
public class MDSWebClientInterceptor implements WebFilter {
#Value("some.api.client-id")
private String clientId;
#Value("some.api.token")
private String token;
private static final String CLIENT_ID_HEADER = "Client-Id";
private static final String CLIENT_TOKEN_HEADER = "Secret";
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(
exchange.mutate().request(
exchange.getRequest().mutate()
.header(CLIENT_ID_HEADER, clientId)
.header(CLIENT_TOKEN_HEADER, token)
.build())
.build());
}
}
But this doesn't work at all, and I have a hunch that if it would work, it would affect ALL WebClient instances, not just the one in SomeApiWebClient.
Is something like this even possible?
Background
My use case is to authenticate the calls using a Gateway. I assume that all the calls must have user_id and a token. I have an API that will take both user_id, token and return a boolean after checking that it's a valid request or not.
Current Work
I have written a Gateway configuration which is working fine in routing the APIs to concerned micro-service but it's not working fine for authentication.
working code,
#Configuration
public class RouteConfig {
#Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("app1/**")
.filters(f -> f.rewritePath("app1/(?<segment>.*)",
"app1/${segment}"))
.uri("http://localhost:8099"))
.build();
}
}
Firsly , I need to get the Headers variables and then make a POST call to an API which returns BOOLEAN response. If its true then the above code should call. Otherwise the call should be rejected.
Tried to get Headers value
#Configuration
public class RouteConfig {
#Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder,#RequestHeader("user_id")
String userId,#RequestHeader("token")
String token) {
System.out.println(userId);
System.out.println(token);
return builder.routes()
.route("path_route", r -> r.path("app1/**")
.filters(f -> f.rewritePath("app1/(?<segment>.*)",
"app1/${segment}"))
.uri("http://localhost:8099"))
.build();
}
}
But its not working and application crashed.
You can implement a GatewayFilter to check each request on your gateway.
#Component
public class AuthFilter implements GatewayFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
if (!headers.containsKey("user_id") || !headers.containsKey("token"))
return this.onError(exchange, HttpStatus.UNAUTHORIZED);
String userId = request.getHeaders().getOrEmpty("user_id").get(0);
String token = request.getHeaders().getOrEmpty("token").get(0);
Map<String, String> body = new HashMap<>();
body.put("user_id", userId);
body.put("token", token);
WebClient client = WebClient.builder().build();
boolean ok = client
.post()
.uri("your/auth/api")
.body(Mono.just(body), Map.class)
.retrieve()
.bodyToFlux(Boolean.class)
.blockFirst();
if (!ok)
return this.onError(exchange, HttpStatus.UNAUTHORIZED);
return chain.filter(exchange);
}
private Mono<Void> onError(ServerWebExchange exchange, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
return response.setComplete();
}
}
and apply the filter to your route configuration. Not sure if the configuration is correct and the rewrite is applied correctly.
#Configuration
public class RouteConfig {
#Autowired
private AuthFilter filter;
#Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("app1/**")
.filters(f -> f.filters(filter).rewritePath("app1/(?<segment>.*)",
"app1/${segment}"))
.uri("http://localhost:8099"))
.build();
}
}
Note that this code is untested, just to give you an idea how you may solve your issue.
I am currently building a feign client manually and passing Interceptors to it for authorization. I would like to have a smarter Retryer for some Response code.
public class myErrorEncoder extends ErrorDecoder.Default {
#Override
public Exception decode(final String methodKey, final Response response) {
if (response.status() == 401) {
String token = refreshToken(); // I would like to refresh the token and Edit the client
return new RetryableException("Token Expired will retry it", null);
} else {
return super.decode(methodKey, response);
}
}
}
Interceptor
#Bean public CustomInterceptor getInterceptor(String token) {
return new CustomInterceptor(token);}
Feign builder
private <T> T feignBuild(final Class<T> clazz, final String uri, final String token) {
return Feign
.builder().client(new ApacheHttpClient())
.encoder(new GsonEncoder())
.decoder(new ResponseEntityDecoder(feignDecoder())
.retryer(new Retryer.Default(1,100,3))
.errorDecoder(new ErrorDecoder())
.requestInterceptor(getInterceptor(token))
.contract(new ClientContract())
.logger(new Slf4jLogger(clazz)).target(clazz, uri);
}
Now I would like to update feign client with the refreshed token and retry.
Is there a way get access to the client instance and configure it.
Your use of the interceptor is incorrect. Interceptors are re-applied during a retry, but they are instantiated only once and are expected to be thread safe. To achieve what you are looking for will need to separate the token generation from the interceptor and have the interceptor request a new token.
public class TokenInterceptor() {
TokenService tokenService;
public TokenInterceptor(TokenService tokenService) {
this.tokenService = tokenService;
}
public void apply(RequestTemplate template) {
/* getToken() should create a new token */
String token = this.tokenService.getToken();
template.header("Authorization", "Bearer " + token);
}
}
This will ensure that a new token is created each retry cycle.
I have a microservice architecture, both of them securized by spring security an JWT tokens.
So, when I call my first microservice, I want to take the JWT token and send a request to another service using those credentials.
How can I retrieve the token and sent again to the other service?
Basically your token should be located in the header of the request, like for example: Authorization: Bearer . For getting it you can retrieve any header value by #RequestHeader() in your controller:
#GetMapping("/someMapping")
public String someMethod(#RequestHeader("Authorization") String token) {
}
Now you can place the token within the header for the following request:
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", token);
HttpEntity<RestRequest> entityReq = new HttpEntity<RestRequest>(request, headers);
Now you can pass the HttpEntity to your rest template:
template.exchange("RestSvcUrl", HttpMethod.POST, entityReq, SomeResponse.class);
Hope I could help
I've accomplished the task, creating a custom Filter
public class RequestFilter implements Filter{
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(RequestContext.REQUEST_HEADER_NAME);
if (token == null || "".equals(token)) {
throw new IllegalArgumentException("Can't retrieve JWT Token");
}
RequestContext.getContext().setToken(token);
chain.doFilter(request, response);
}
#Override
public void destroy() { }
#Override
public void init(FilterConfig arg0) throws ServletException {}
}
Then, setting in my config
#Bean
public FilterRegistrationBean getPeticionFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new RequestFilter());
registration.addUrlPatterns("/*");
registration.setName("requestFilter");
return registration;
}
With that in mind, I've create another class with a ThreadLocal variable to pass the JWT token from the Controller to the Rest Templace interceptor
public class RequestContext {
public static final String REQUEST_HEADER_NAME = "Authorization";
private static final ThreadLocal<RequestContext> CONTEXT = new ThreadLocal<>();
private String token;
public static RequestContext getContext() {
RequestContext result = CONTEXT.get();
if (result == null) {
result = new RequestContext();
CONTEXT.set(result);
}
return result;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
public class RestTemplateInterceptor implements ClientHttpRequestInterceptor{
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
String token = RequestContext.getContext().getToken();
request.getHeaders().add(RequestContext.REQUEST_HEADER_NAME, token);
return execution.execute(request, body);
}
}
Add interceptor to the config
#PostConstruct
public void addInterceptors() {
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
interceptors.add(new RestTemplateInterceptor());
restTemplate.setInterceptors(interceptors);
}
I think it is better to add the interceptor specifically to the RestTemplate, like this:
class RestTemplateHeaderModifierInterceptor(private val authenticationService: IAuthenticationService) : ClientHttpRequestInterceptor {
override fun intercept(request: org.springframework.http.HttpRequest, body: ByteArray, execution: ClientHttpRequestExecution): ClientHttpResponse {
if (!request.headers.containsKey("Authorization")) {
// don't overwrite, just add if not there.
val jwt = authenticationService.getCurrentUser()!!.jwt
request.headers.add("Authorization", "Bearer $jwt")
}
val response = execution.execute(request, body)
return response
}
}
And add it to the RestTemplate like so:
#Bean
fun restTemplate(): RestTemplate {
val restTemplate = RestTemplate()
restTemplate.interceptors.add(RestTemplateHeaderModifierInterceptor(authenticationService)) // add interceptor to send JWT along with requests.
return restTemplate
}
That way, every time you need a RestTemplate you can just use autowiring to get it. You do need to implement the AuthenticationService still to get the token from the TokenStore, like this:
val details = SecurityContextHolder.getContext().authentication.details
if (details is OAuth2AuthenticationDetails) {
val token = tokenStore.readAccessToken(details.tokenValue)
return token.value
}
May be a little bit late but I think this is a common question, regarding
Spring Security 6.0.0 for web client there is a class called ServletBearerExchangeFilterFunction that you can use to read the token from the security context and inject it.
#Bean
public WebClient rest() {
return WebClient.builder()
.filter(new ServletBearerExchangeFilterFunction())
.build();
For RestTemplate there is no automatic way and is recommended use a filter
#Bean
RestTemplate rest() {
RestTemplate rest = new RestTemplate();
rest.getInterceptors().add((request, body, execution) -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return execution.execute(request, body);
}
if (!(authentication.getCredentials() instanceof AbstractOAuth2Token)) {
return execution.execute(request, body);
}
AbstractOAuth2Token token = (AbstractOAuth2Token) authentication.getCredentials();
request.getHeaders().setBearerAuth(token.getTokenValue());
return execution.execute(request, body);
});
return rest;
}
I'm trying to setup my Spring Cloud Feign Client to use custom HostnameVerifier. I need the custom HostnameVerifier to ignore certificate problems. How can I do that?
here is my current config:
#FeignClient(name = "AccountSettingsClient", url = "${account.settings.service.url}", decode404 = true,
configuration = AccountSettingsClientConfig.class, fallbackFactory = AccountSettingsClientFallbackFactory.class)
public interface AccountSettingsClient {
#RequestMapping(method = RequestMethod.GET, value = "/settings/{uuid}")
AccountSettings accountSettings(#PathVariable("uuid") String uuid);
}
#Component
#Slf4j
class AccountSettingsClientFallbackFactory implements FallbackFactory<AccountSettingsClient> {
#Override
public AccountSettingsClient create(Throwable cause) {
return uuid -> {
log.warn("Falling back to null.", cause);
return null;
};
}
}
#Configuration
#RequiredArgsConstructor
#EnableConfigurationProperties(SomeProperties.class)
#EnableFeignClients
public class AccountSettingsClientConfig {
private final SomeProperties someProperties;
#Bean
RequestInterceptor oauth2FeignRequestInterceptor() {
return new OAuth2FeignRequestInterceptor(new
DefaultOAuth2ClientContext(), resource());
}
}
By default there gets created a LoadBalancerFeignClient with a HttpURLConnection on board and can't override settings of HostnameVerifier of it. In order to override it choose another client like OkHttp or Apache Http Client, add correspondent maven dependency and then you can override the client with all settings.
I chose the OkHttpClient and added to my AccountSettingsConfig follow bean:
#Bean
public okhttp3.OkHttpClient okHttpClient() {
return new OkHttpClient.Builder().hostnameVerifier((s, sslSession) -> true)
.build();
}