Using WebClient to propagate request headers received in a Spring Webflux applications to downstream services - spring

I have two kinds of Webflux applications, annotation-based and route-based. These applications are called with a set of headers, some of which (Open Tracing) I need to propagate in downstream calls using WebClient.
If these were normal Spring WebMvc applications I would use a Filter to keep the selected headers in a ThreadLocal, access it in a RestTemplate interceptor to send them to subsequent services and clear the ThreadLocal.
What's the proper way to replicate this behaviour in WebFlux applications?

I solved it using Project Reactor's Context to store the headers in a WebFilter. Then they are gotten in the WebClient's ExchangeFilterFunction. Here's the whole solution:
WebFilter
class OpenTracingFilter(private val openTracingHeaders: Set<String>) : WebFilter {
private val logger = LoggerFactory.getLogger(javaClass)
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
return chain.filter(exchange)
.subscriberContext { ctx ->
var updatedContext = ctx
exchange.request.headers.forEach {
if (openTracingHeaders.contains(it.key.toLowerCase())) {
logger.debug("Found OpenTracing Header - key {} - value {}", it.key, it.value[0])
updatedContext = updatedContext.put(it.key, it.value[0])
}
}
updatedContext
}
}
}
OpenTracingExchangeFilterFunction
class OpenTracingExchangeFilterFunction(private val headers: Set<String>) : ExchangeFilterFunction {
private val logger = LoggerFactory.getLogger(javaClass)
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
logger.debug("OpenTracingExchangeFilterFunction - filter()")
return OpenTracingClientResponseMono(request, next, headers)
}
}
OpenTracingClientResponseMono
class OpenTracingClientResponseMono(private val request: ClientRequest,
private val next: ExchangeFunction,
private val headersToPropagate: Set<String>) : Mono<ClientResponse>() {
private val logger = LoggerFactory.getLogger(javaClass)
override fun subscribe(subscriber: CoreSubscriber<in ClientResponse>) {
val context = subscriber.currentContext()
val requestBuilder = ClientRequest.from(request)
requestBuilder.headers { httpHeaders ->
headersToPropagate.forEach {
if(context.hasKey(it)) {
logger.debug("Propagating header key {} - value{}", it, context.get<String>(it))
httpHeaders[it] = context.get<String>(it)
}
}
}
val mutatedRequest = requestBuilder.build()
next.exchange(mutatedRequest).subscribe(subscriber)
}
}
OpenTracingConfiguration
#Configuration
class OpenTracingConfiguration(private val openTracingConfigurationProperties: OpenTracingConfigurationProperties) {
#Bean
fun webClient(): WebClient {
return WebClient.builder().filter(openTracingExchangeFilterFunction()).build()
}
#Bean
fun openTracingFilter(): WebFilter {
return OpenTracingFilter(openTracingConfigurationProperties.headers)
}
#Bean
fun openTracingExchangeFilterFunction(): OpenTracingExchangeFilterFunction {
return OpenTracingExchangeFilterFunction(openTracingConfigurationProperties.headers)
}
}
OpenTracingConfigurationProperties
#Configuration
#ConfigurationProperties("opentracing")
class OpenTracingConfigurationProperties {
lateinit var headers: Set<String>
}
application.yml
opentracing:
headers:
- x-request-id
- x-b3-traceid
- x-b3-spanid
- x-b3-parentspanid
- x-b3-sampled
- x-b3-flags
- x-ot-span-context

I needed to pass x-request-id header to a downstream service in my application. Achieved this by adding WebFilter that writes x-request-id to a reactor context
class ContextWebFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val headers = exchange.request.headers
val xRequestId = headers[X_REQUEST_ID]?.firstOrNull() ?: ""
val requestId = xRequestId.ifBlank { UUID.randomUUID().toString() }
return chain
.filter(exchange)
.contextWrite { it.put(X_REQUEST_ID, requestId) }
}
companion object {
const val X_REQUEST_ID = "X-REQUEST-ID"
}
}
and updating WebClient with ExchangeFilterFunction that updates outgoing request
WebClient.builder()
.filter(
ExchangeFilterFunction.ofRequestProcessor { request ->
Mono.deferContextual { context ->
val xRId = context.getOrDefault<String>("X-REQUEST-ID", "")
logger.debug("Set X-REQUEST-ID={} as a header to outgoing call", xRId)
Mono.just(
ClientRequest.from(request)
.header("X-REQUEST-ID", xRId)
.build()
)
}
}
)
.baseUrl("http://localhost:8080")
.build()

Related

Spring-Security: Getting 401s with CORS Preflights (despite http.cors())

For starters i want to secure a part of my rest-api with basic auth.
When I try to access endpoints from a react client, I keep getting 401's in the preflight requests.
I tried to follow this guide without success:
https://www.baeldung.com/spring-security-cors-preflight
i'm not sure if this is part of the problem, but another part can only be accessed with certain custom http headers.
I'm using Method Security:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = false)
class MethodSecurityConfig : GlobalMethodSecurityConfiguration() {
override fun customMethodSecurityMetadataSource(): MethodSecurityMetadataSource = SecurityMetadataSource()
override fun accessDecisionManager(): AccessDecisionManager = super.accessDecisionManager().apply {
this as AbstractAccessDecisionManager
decisionVoters.add(PrivilegeVoter())
}
}
And this is my Security config:
#Configuration
#EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class SecurityConfig : WebSecurityConfigurerAdapter() {
private val deviceRequestHeaderName: String = "X-DEVICE-ID"
private val platformRequestHeaderName: String = "X-PLATFORM-ID"
#Autowired
lateinit var users: AppUserRepository
#Autowired
lateinit var backendUsers: BackendUserRepository
#Autowired
lateinit var roles: RoleRepository
val authManager by lazy { authenticationManager() }
private val authProvider by lazy {
PreAuthenticatedAuthenticationProvider().apply {
setPreAuthenticatedUserDetailsService {
val authId = it.principal as UserAuthId
if (authId.deviceId == null) throw UsernameNotFoundException("No device-id to search for.")
if (authId.platform == null) throw UsernameNotFoundException("Platform not specified.")
val platform = try {
ApplicationPlatform.valueOf(authId.platform)
} catch (e: IllegalArgumentException) {
throw UsernameNotFoundException("Unknown platform ${authId.platform}.")
}
val existingUser = users.findByUserDeviceIdAndPlatform(authId.deviceId, platform)
if (existingUser != null) return#setPreAuthenticatedUserDetailsService existingUser
users.save(AppUser(authId.deviceId, platform, roles))
}
}
}
val passwordEncoder by lazy { BCryptPasswordEncoder() }
private val deviceIdFilter by lazy {
HeaderFieldAuthFilter(deviceRequestHeaderName, platformRequestHeaderName).apply {
setAuthenticationManager(authManager)
}
}
override fun configure(auth: AuthenticationManagerBuilder) = auth {
authenticationProvider(authProvider)
val userDetailsService = BackendUserDetailsService(backendUsers)
userDetailsService(userDetailsService).passwordEncoder(passwordEncoder)
}
override fun configure(http: HttpSecurity) = http {
session {
sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
exceptionHandling()
addFilter(deviceIdFilter)
authorizeRequests().anyRequest().authenticated()
csrf().disable()
httpBasic()
cors().configurationSource { request ->
CorsConfiguration().apply {
allowedOrigins = listOf(ALL)
allowedMethods = listOf(GET, POST, DELETE, PUT, OPTIONS).map { it.name }
allowedHeaders = listOf(ALL)
allowCredentials = true
maxAge = 3600
}
}
}
#Bean
fun auditorProvider(): AuditorAware<User> = AuditorAware<User> {
val authentication = SecurityContextHolder.getContext().authentication
val user = authentication.run { if (isAuthenticated) principal as? User else null }
return#AuditorAware Optional.ofNullable(user)
}
}
I could solve by manually exclude the preflight requests from authentication.
adding this
antMatchers(OPTIONS, "/**").permitAll()
to the authorizeRequests() configuration accomplishes that.
Note that Options is a direct reference to the HttpMethod enum value, imported like this
import org.springframework.http.HttpMethod.*
Stackoverflow posts that helped me get there:
Response for preflight has invalid HTTP status code 401 - Spring
How to configure CORS in a Spring Boot + Spring Security application?
Originally I had assumed, that this should have been handled by the cors configuration - which it was apparently not.
To enable CORS for a single rest endpoint you can annotate it with:
#CrossOrigin
To allow for CORS for all endpoints you can have a bean like so:
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "POST", "PUT", "DELETE").allowedOrigins("*")
.allowedHeaders("*");
}
};

Spring Cloud gateway send response in filter

I am using spring cloud gateway as edge server.
This is the flow
If request has a header named 'x-foo' then find the header value, get a string from another server and send that string as response instead of actually proxying the request.
Here is code for Filter DSL
#Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("foo-filter", r -> r.header('x-foo').and().header("x-intercepted").negate()
.filters(f -> f.filter(fooFilter))
.uri("http://localhost:8081")) // 8081 is self port, there are other proxy related configurations too
.build();
}
Code for Foo filter
#Component
#Slf4j
public class FooFilter implements GatewayFilter {
#Autowired
private ReactiveRedisOperations<String, String> redisOps;
#Value("${header-name}")
private String headerName;
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
var foo = request.getHeaders().getFirst(headerName);
return redisOps.opsForHash()
.get("foo:" + foo, "response")
.doOnSuccess(s -> {
log.info("data on success");
log.info(s.toString()); // I am getting proper response here
if (s != null) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().set("x-intercepted", "true");
byte[] bytes = s.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
response.writeWith(Mono.just(buffer));
response.setComplete();
}
})
.then(chain.filter(exchange));
}
}
The problem is, the response has the response is getting proper 200 code, the injected header is present on response but the data is not available in response.
This is how I got working.
Use flatMap instead of doOnSuccess
don't use then or switchIfEmpty instead use onErrorResume
Return the response.writeWith
#Component
#Slf4j
public class FooFilter implements GatewayFilter {
#Autowired
private ReactiveRedisOperations<String, String> redisOps;
#Value("${header-name}")
private String headerName;
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
var foo = request.getHeaders().getFirst(headerName);
return redisOps.opsForHash()
.get("foo:" + foo, "response")
.flatMap(s -> {
log.info("data on success");
log.info(s.toString()); // I am getting proper response here
if (s != null) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().set("x-intercepted", "true");
byte[] bytes = s.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
}else{ return chain.filter(exchange).then(Mono.fromRunnable(() -> {log.info("It was empty")} }
})
.onErrorResume(chain.filter(exchange));
}
}

Spring Boot 2 OIDC (OAuth2) client / resource server not propagating the access token in the WebClient

Sample project available on Github
I have successfully configured two Spring Boot 2 application2 as client/resource servers against Keycloak and SSO between them is fine.
Besides, I am testing authenticated REST calls to one another, propagating the access token as an Authorization: Bearer ACCESS_TOKEN header.
After starting Keycloak and the applications I access either http://localhost:8181/resource-server1 or http://localhost:8282/resource-server-2 and authenticate in the Keycloak login page. The HomeController uses a WebClient to invoke the HelloRestController /rest/hello endpoint of the other resource server.
#Controller
class HomeController(private val webClient: WebClient) {
#GetMapping
fun home(httpSession: HttpSession,
#RegisteredOAuth2AuthorizedClient authorizedClient: OAuth2AuthorizedClient,
#AuthenticationPrincipal oauth2User: OAuth2User): String {
val authentication = SecurityContextHolder.getContext().authentication
println(authentication)
val pair = webClient.get().uri("http://localhost:8282/resource-server-2/rest/hello").retrieve()
.bodyToMono(Pair::class.java)
.block()
return "home"
}
}
This call returns a 302 since the request is not authenticated (it's not propagating the access token):
2019-12-25 14:09:03.737 DEBUG 8322 --- [nio-8181-exec-5] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
OAuth2Configuration:
#Configuration
class OAuth2Config : WebSecurityConfigurerAdapter() {
#Bean
fun webClient(): WebClient {
return WebClient.builder()
.filter(ServletBearerExchangeFilterFunction())
.build()
}
#Bean
fun clientRegistrationRepository(): ClientRegistrationRepository {
return InMemoryClientRegistrationRepository(keycloakClientRegistration())
}
private fun keycloakClientRegistration(): ClientRegistration {
val clientRegistration = ClientRegistration
.withRegistrationId("resource-server-1")
.clientId("resource-server-1")
.clientSecret("c00670cc-8546-4d5f-946e-2a0e998b9d7f")
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("openid", "profile", "email", "address", "phone")
.authorizationUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/auth")
.tokenUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/token")
.userInfoUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/certs")
.clientName("Keycloak")
.providerConfigurationMetadata(mapOf("end_session_endpoint" to "http://localhost:8080/auth/realms/insight/protocol/openid-connect/logout"))
.build()
return clientRegistration
}
override fun configure(http: HttpSecurity) {
http.authorizeRequests { authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
}.oauth2Login(withDefaults())
.logout { logout ->
logout.logoutSuccessHandler(oidcLogoutSuccessHandler())
}
}
private fun oidcLogoutSuccessHandler(): LogoutSuccessHandler? {
val oidcLogoutSuccessHandler = OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository())
oidcLogoutSuccessHandler.setPostLogoutRedirectUri(URI.create("http://localhost:8181/resource-server-1"))
return oidcLogoutSuccessHandler
}
}
As you can see I'm setting a ServletBearerExchangeFilterFunction in the WebClient. This is what I've seen debugging:
The SubscriberContext isn't setting anything because authentication.getCredentials() instanceof AbstractOAuth2Token is false. Actually it is just a String:
public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
...
#Override
public Object getCredentials() {
// Credentials are never exposed (by the Provider) for an OAuth2 User
return "";
}
What's the problem here? How can I automate the propagation of the token?
There doesn't seem to be an out of the box solution for pure OAuth2/OIDC login applications, I've created a Github issue for this.
In the meantime, I've created a specific ServletBearerExchangeFilterFunction that retrieves the access token from the OAuth2AuthorizedClientRepository.
This is my custom solution:
#Autowired
lateinit var oAuth2AuthorizedClientRepository: OAuth2AuthorizedClientRepository
#Bean
fun webClient(): WebClient {
val servletBearerExchangeFilterFunction = ServletBearerExchangeFilterFunction("resource-server-1", oAuth2AuthorizedClientRepository)
return WebClient.builder()
.filter(servletBearerExchangeFilterFunction)
.build()
}
...
private fun keycloakClientRegistration(): ClientRegistration {
return ClientRegistration
.withRegistrationId("resource-server-1")
...
const val SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY = "org.springframework.security.SECURITY_CONTEXT_ATTRIBUTES"
class ServletBearerExchangeFilterFunction(private val clientRegistrationId: String,
private val oAuth2AuthorizedClientRepository: OAuth2AuthorizedClientRepository?) : ExchangeFilterFunction {
/**
* {#inheritDoc}
*/
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
return oauth2Token()
.map { token: AbstractOAuth2Token -> bearer(request, token) }
.defaultIfEmpty(request)
.flatMap { request: ClientRequest -> next.exchange(request) }
}
private fun oauth2Token(): Mono<AbstractOAuth2Token> {
return Mono.subscriberContext()
.flatMap { ctx: Context -> currentAuthentication(ctx) }
.map { authentication ->
val authorizedClient = oAuth2AuthorizedClientRepository?.loadAuthorizedClient<OAuth2AuthorizedClient>(clientRegistrationId, authentication, null)
if (authorizedClient != null) {
authorizedClient.accessToken
} else {
Unit
}
}
.filter { it != null }
.cast(AbstractOAuth2Token::class.java)
}
private fun currentAuthentication(ctx: Context): Mono<Authentication> {
return Mono.justOrEmpty(getAttribute(ctx, Authentication::class.java))
}
private fun <T> getAttribute(ctx: Context, clazz: Class<T>): T? { // NOTE: SecurityReactorContextConfiguration.SecurityReactorContextSubscriber adds this key
if (!ctx.hasKey(SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY)) {
return null
}
val attributes: Map<Class<T>, T> = ctx[SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY]
return attributes[clazz]
}
private fun bearer(request: ClientRequest, token: AbstractOAuth2Token): ClientRequest {
return ClientRequest.from(request)
.headers { headers: HttpHeaders -> headers.setBearerAuth(token.tokenValue) }
.build()
}
}

Custom authentication with spring-security and reactive spring

I'm having a custom authentication scheme. I'm having a REST endpoint that has userId in http uri path and token in http header. I would like to check that such request is perform by valid user with valid token. Users and tokens are stored in mongo collection.
I don't know in which class I should authorize user.
My SecurityConfig:
#EnableWebFluxSecurity
class SecurityConfig {
#Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
val build = http
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable()
.authenticationManager(CustomReactiveAuthenticationManager())
.securityContextRepository(CustomServerSecurityContextRepository())
.authorizeExchange().pathMatchers("/api/measurement/**").hasAuthority("ROLE_USER")
.anyExchange().permitAll().and()
return build.build()
}
#Bean
fun userDetailsService(): MapReactiveUserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("sampleDeviceIdV1")
.password("foo")
.roles("USER")
.build()
return MapReactiveUserDetailsService(user)
}
}
My ServerSecurityContextRepository:
class CustomServerSecurityContextRepository : ServerSecurityContextRepository {
override fun load(exchange: ServerWebExchange): Mono<SecurityContext> {
val authHeader = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)
val path = exchange.request.uri.path
return if (path.startsWith("/api/measurement/") && authHeader != null && authHeader.startsWith(prefix = "Bearer ")) {
val deviceId = path.drop(17)
val authToken = authHeader.drop(7)
val auth = UsernamePasswordAuthenticationToken(deviceId, authToken)
Mono.just(SecurityContextImpl(auth))
} else {
Mono.empty()
}
}
override fun save(exchange: ServerWebExchange?, context: SecurityContext?): Mono<Void> {
return Mono.empty()
}
}
Two questions arise:
Is ServerSecurityContextRepository good place to obtain username and token from exchange - or there is a better place to do it?
Where should I perform authentication (check token and username against mongo collection)?
My custom AuthenticationManager does not get called anywhere. Should I do everything inside ServerSecurityContextRepository or perform user and token validation inside ReactiveAuthenticationManager? Or maybe other class would be even more suitable?
It turns out that some tutorials on the web are plain wrong.
I've managed to configure everything using following code:
class DeviceAuthenticationConverter : Function<ServerWebExchange, Mono<Authentication>> {
override fun apply(exchange: ServerWebExchange): Mono<Authentication> {
val authHeader: String? = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)
val path: String? = exchange.request.uri.path
return when {
isValidPath(path) && isValidHeader(authHeader) -> Mono.just(UsernamePasswordAuthenticationToken(path?.drop(17), authHeader?.drop(7)))
else -> Mono.empty()
}
}
private fun isValidPath(path: String?) = path != null && path.startsWith(API_MEASUREMENT)
private fun isValidHeader(authHeader: String?) = authHeader != null && authHeader.startsWith(prefix = "Bearer ")
}
And config:
#EnableWebFluxSecurity
class SecurityConfig {
companion object {
const val API_MEASUREMENT = "/api/measurement/"
const val API_MEASUREMENT_PATH = "$API_MEASUREMENT**"
const val DEVICE = "DEVICE"
const val DEVICE_ID = "deviceId"
}
#Bean
fun securityWebFilterChain(http: ServerHttpSecurity, authenticationManager: ReactiveAuthenticationManager) =
http
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable()
.authorizeExchange().pathMatchers(API_MEASUREMENT_PATH).hasRole(DEVICE)
.anyExchange().permitAll().and().addFilterAt(authenticationWebFilter(authenticationManager), AUTHENTICATION).build()
#Bean
fun userDetailsService(tokenRepository: TokenRepository) = MongoDeviceTokenReactiveUserDetailsService(tokenRepository)
#Bean
fun tokenRepository(template: ReactiveMongoTemplate, passwordEncoder: PasswordEncoder) = MongoTokenRepository(template, passwordEncoder)
#Bean
fun tokenFacade(tokenRepository: TokenRepository) = TokenFacade(tokenRepository)
#Bean
fun authManager(userDetailsService: ReactiveUserDetailsService) = UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService)
private fun authenticationWebFilter(reactiveAuthenticationManager: ReactiveAuthenticationManager) =
AuthenticationWebFilter(reactiveAuthenticationManager).apply {
setAuthenticationConverter(DeviceAuthenticationConverter())
setRequiresAuthenticationMatcher(
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, API_MEASUREMENT_PATH)
)
}
#Bean
fun passwordEncoder() = PasswordEncoderFactories.createDelegatingPasswordEncoder()
}

How to intercept requests by handler method in Spring WebFlux

I've got following interceptor in Spring MVC that checks if user can access handler method:
class AccessInterceptor : HandlerInterceptorAdapter() {
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any?): Boolean {
val auth: Auth =
(if (method.getAnnotation(Auth::class.java) != null) {
method.getAnnotation(Auth::class.java)
} else {
method.declaringClass.getAnnotation(Auth::class.java)
}) ?: return true
if (auth.value == AuthType.ALLOW) {
return true
}
val user = getUserFromRequest(request) // checks request for auth token
// and checking auth for out user in future.
return renderError(403, response)
In my Controller I do annotate methods, like this:
#GetMapping("/foo")
#Auth(AuthType.ALLOW)
fun doesntNeedAuth(...) { ... }
#GetMapping("/bar")
#Auth(AuthType.ADMIN)
fun adminMethod(...) { ... }
In case if user has wrong token or no permissions, error is being returned.
Is it possible to do this in Spring WebFlux with annotation-style controllers?
My implementation, w/o using toFuture().get() which is potentially blocking.
#Component
#ConditionalOnWebApplication(type = Type.REACTIVE)
public class QueryParameterValidationFilter implements WebFilter {
#Autowired
private RequestMappingHandlerMapping handlerMapping;
#NonNull
#Override
public Mono<Void> filter(#NonNull ServerWebExchange exchange, #NonNull WebFilterChain chain) {
return handlerMapping.getHandler(exchange)
.doOnNext(handler -> validateParameters(handler, exchange))
.then(chain.filter(exchange));
}
private void validateParameters(Object handler, ServerWebExchange exchange) {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Set<String> expectedQueryParams = Arrays.stream(handlerMethod.getMethodParameters())
.map(param -> param.getParameterAnnotation(RequestParam.class))
.filter(Objects::nonNull)
.map(RequestParam::name)
.collect(Collectors.toSet());
Set<String> actualQueryParams = exchange.getRequest().getQueryParams().keySet();
actualQueryParams.forEach(actual -> {
if (!expectedQueryParams.contains(actual)) {
throw new InvalidParameterException(ERR_MSG, actual);
}
});
}
}
}
To solve that problem I would most probably use:
A Spring Reactive Web WebFilter from the WebHandler API to intercept the incoming request
The RequestMappingHandlerMapping to retrieve the method which handles the current request
#Autowired
RequestMappingHandlerMapping requestMappingHandlerMapping;
...
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
...
HandlerMethod handler = (HandlerMethod) requestMappingHandlerMapping.getHandler(exchange).toProcessor().peek();
//your logic
}
#Component
class AuditWebFilter(
private val requestMapping: RequestMappingHandlerMapping
): WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
// if not to call - then exchange.attributes will be empty
// so little early initializate exchange.attributes by calling next line
requestMapping.getHandler(exchange)
val handlerFunction = exchange.attributes.get(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE) as HandlerMethod
val annotationMethod = handlerFunction.method.getAnnotation(MyAnnotation::class.java)
// annotationMethod proccesing here
}
}
In newer versions of Spring the .toProcessor() call is deprecated. What worked for me is to use .toFuture().get() instead:
if(requestMappingHandlerMapping.getHandler(exchange).toFuture().get() instanceof HandlerMethod handlerMethod) { ... }
Unfortunately this requires handling of checked exceptions so the code will be a bit less readable but at least not deprecated anymore.

Resources