To simplify this, Micronaut includes the ability to define classes that are applied to all matching HTTP client requests.
As an example say you want to build a client to communicate with the Bintray REST API. It would be terribly tedious to have to specify authentication for every single HTTP call.
To resolve this burden you can define a filter. The following is an example :
class BintrayApi {
public static final String URL = 'https://api.bintray.com'
}
@Singleton
class BintrayService {
final RxHttpClient client
final String org
BintrayService(
@Client(BintrayApi.URL) RxHttpClient client, (1)
@Value('${bintray.organization}') String org ) {
this.client = client
this.org = org
}
Flowable<HttpResponse<String>> fetchRepositories() {
return client.exchange(HttpRequest.GET("/repos/$org"), String) (2)
}
Flowable<HttpResponse<String>> fetchPackages(String repo) {
return client.exchange(HttpRequest.GET("/repos/${org}/${repo}/packages"), String) (2)
}
}
class BintrayApi {
public static final String URL = 'https://api.bintray.com'
}
@Singleton
internal class BintrayService(
@param:Client(BintrayApi.URL) val client: RxHttpClient, (1)
@param:Value("\${bintray.organization}") val org: String) {
fun fetchRepositories(): Flowable<HttpResponse<String>> {
return client.exchange(HttpRequest.GET<Any>("/repos/$org"), String::class.java) (2)
}
fun fetchPackages(repo: String): Flowable<HttpResponse<String>> {
return client.exchange(HttpRequest.GET<Any>("/repos/$org/$repo/packages"), String::class.java) (2)
}
}
@Filter("/repos/**") (1)
class BintrayFilter implements HttpClientFilter {
final String username;
final String token;
BintrayFilter(
@Value("${bintray.username}") String username, (2)
@Value("${bintray.token}") String token ) { (2)
this.username = username;
this.token = token;
}
@Override
public Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
return chain.proceed(
request.basicAuth(username, token) (3)
);
}
}
@Filter('/repos/**') (1)
class BintrayFilter implements HttpClientFilter {
final String username
final String token
BintrayFilter(
@Value('${bintray.username}') String username, (2)
@Value('${bintray.token}') String token ) { (2)
this.username = username
this.token = token
}
@Override
Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
return chain.proceed(
request.basicAuth(username, token) (3)
)
}
}
@Filter("/repos/**") (1)
internal class BintrayFilter(
@param:Value("\${bintray.token}") val token: String)(2)
: HttpClientFilter {
override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher<out HttpResponse<*>> {
return chain.proceed(
)
}
}
1 | You can match only a subset of paths with a Client filter. |
2 | The username and token are injected via configuration |
3 | The basicAuth method is used include the HTTP BASIC credentials |
Now, whenever you invoke the bintrayService.fetchRepositories()
method, the Authorization
HTTP header is included in the request.
It should be noted that in order to create a Micronaut needs to resolve all of the HttpClientFilter
instances which creates a circular dependency in the case where you need to inject another RxHttpClient or a @Client
bean into an instance of a HttpClientFilter
.
To resolve this issue you should use the javax.inject.Provider
interface to inject another or a @Client
bean into an instance of HttpClientFilter
.
import io.micronaut.context.annotation.Requires
import io.micronaut.context.env.Environment
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.annotation.Filter
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.filter.ClientFilterChain
import io.micronaut.http.filter.HttpClientFilter
import io.reactivex.Flowable
import org.reactivestreams.Publisher
import javax.inject.Provider
@Requires(env = Environment.GOOGLE_COMPUTE)
@Filter(patterns = "/google-auth/api/**")
class GoogleAuthFilter implements HttpClientFilter {
private final Provider<RxHttpClient> authClientProvider
GoogleAuthFilter(Provider<RxHttpClient> httpClientProvider) { (1)
this.authClientProvider = httpClientProvider
}
@Override
Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
Flowable<String> token = Flowable.fromCallable(() -> encodeURI(request))
.flatMap(authURI -> authClientProvider.get().retrieve(HttpRequest.GET(authURI).header( (2)
"Metadata-Flavor", "Google"
)))
return token.flatMap(t -> chain.proceed(request.bearerAuth(t)))
}
private static String encodeURI(MutableHttpRequest<?> request) {
String receivingURI = "$request.uri.scheme://$request.uri.host"
return "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=" +
URLEncoder.encode(receivingURI, "UTF-8")
}
}
import io.micronaut.context.annotation.Requires
import io.micronaut.context.env.Environment
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.annotation.Filter
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.filter.ClientFilterChain
import io.micronaut.http.filter.HttpClientFilter
import io.reactivex.Flowable
import org.reactivestreams.Publisher
import java.net.URLEncoder
import javax.inject.Provider
@Requires(env = [Environment.GOOGLE_COMPUTE])
@Filter(patterns = ["/google-auth/api/**"])
class GoogleAuthFilter (
private val authClientProvider: Provider<RxHttpClient>) : HttpClientFilter { (1)
override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher<out HttpResponse<*>?> {
val token = Flowable.fromCallable { encodeURI(request) }
.flatMap { authURI: String ->
authClientProvider.get().retrieve(HttpRequest.GET<Any>(authURI).header( (2)
"Metadata-Flavor", "Google"
))
}
return token.flatMap { t -> chain.proceed(request.bearerAuth(t)) }
}
private fun encodeURI(request: MutableHttpRequest<*>): String {
val receivingURI = "${request.uri.scheme}://${request.uri.host}"
return "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=" +
URLEncoder.encode(receivingURI, "UTF-8")
}
}
Filter Matching By Annotation
For cases where a filter should be applied to a client, regardless of the URL, filters can be matched by the presence of an annotation applied to both the filter and the client. Given the following client:
import io.micronaut.http.annotation.Get;
@Client("/message")
public interface BasicAuthClient {
@Get
String getMessage();
}
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
@BasicAuth (1)
@Client("/message")
interface BasicAuthClient {
@Get
String getMessage()
}
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
@BasicAuth (1)
@Client("/message")
interface BasicAuthClient {
@Get
fun getMessage(): String
}
1 | The @BasicAuth annotation is applied to the client |
The following filter will filter the client requests:
import io.micronaut.http.HttpResponse
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.filter.ClientFilterChain
import io.micronaut.http.filter.HttpClientFilter
import org.reactivestreams.Publisher
import javax.inject.Singleton
@BasicAuth (1)
@Singleton (2)
class BasicAuthClientFilter implements HttpClientFilter {
@Override
Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
chain.proceed(request.basicAuth("user", "pass"))
}
}
import io.micronaut.http.HttpResponse
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.filter.ClientFilterChain
import io.micronaut.http.filter.HttpClientFilter
import org.reactivestreams.Publisher
import javax.inject.Singleton
@BasicAuth (1)
@Singleton (2)
class BasicAuthClientFilter : HttpClientFilter {
override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher<out HttpResponse<*>> {
return chain.proceed(request.basicAuth("user", "pass"))
}
}
The @BasicAuth
annotation is just an example and can be replaced with your own custom annotation.
import io.micronaut.http.annotation.FilterMatcher;
import java.lang.annotation.*;
@FilterMatcher (1)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.PARAMETER})
public @interface BasicAuth {
}
import io.micronaut.http.annotation.FilterMatcher
import java.lang.annotation.*
@FilterMatcher (1)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.TYPE, ElementType.PARAMETER])
@interface BasicAuth {
}
import io.micronaut.http.annotation.FilterMatcher
@FilterMatcher (1)
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.VALUE_PARAMETER)
annotation class BasicAuth
1 | The only requirement for custom annotations is that the annotation must be present |