1
0

chore: add custom error handlers

Signed-off-by: Alan Brault <alan.brault@visus.io>
This commit is contained in:
2026-03-28 08:00:34 -04:00
parent 7c742fca14
commit be59dcb2bf
5 changed files with 75 additions and 7 deletions
@@ -10,7 +10,7 @@ import org.springframework.web.reactive.config.WebFluxConfigurer
@Configuration @Configuration
class ApiVersionConfig : WebFluxConfigurer { class ApiVersionConfig : WebFluxConfigurer {
override fun configurePathMatching(configurer: PathMatchConfigurer) { override fun configurePathMatching(configurer: PathMatchConfigurer) {
configurer.addPathPrefix("/api/v1") { controller -> configurer.addPathPrefix("/v1") { controller ->
val apiVersion = AnnotatedElementUtils.findMergedAnnotation(controller, ApiVersion::class.java) val apiVersion = AnnotatedElementUtils.findMergedAnnotation(controller, ApiVersion::class.java)
AnnotatedElementUtils.hasAnnotation(controller, RestController::class.java) && apiVersion?.version == 1 AnnotatedElementUtils.hasAnnotation(controller, RestController::class.java) && apiVersion?.version == 1
} }
@@ -36,6 +36,6 @@ class OpenApiConfig {
GroupedOpenApi GroupedOpenApi
.builder() .builder()
.group("v1") .group("v1")
.pathsToMatch("/api/v1/**") .pathsToMatch("/v1/**")
.build() .build()
} }
@@ -1,6 +1,8 @@
package io.visus.demos.kotlinapi.config package io.visus.demos.kotlinapi.config
import io.visus.demos.kotlinapi.domain.service.UserService import io.visus.demos.kotlinapi.domain.service.UserService
import io.visus.demos.kotlinapi.infrastructure.security.CustomAccessDeniedHandler
import io.visus.demos.kotlinapi.infrastructure.security.CustomAuthenticationEntryPoint
import io.visus.demos.kotlinapi.infrastructure.security.JwtAuthenticationFilter import io.visus.demos.kotlinapi.infrastructure.security.JwtAuthenticationFilter
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
@@ -24,6 +26,8 @@ import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
class SecurityConfig( class SecurityConfig(
private val jwtAuthenticationFilter: JwtAuthenticationFilter, private val jwtAuthenticationFilter: JwtAuthenticationFilter,
private val userDetailsService: UserService, private val userDetailsService: UserService,
private val customAuthenticationEntryPoint: CustomAuthenticationEntryPoint,
private val customAccessDeniedHandler: CustomAccessDeniedHandler,
) { ) {
@Bean @Bean
fun corsConfigurationSource(): CorsConfigurationSource { fun corsConfigurationSource(): CorsConfigurationSource {
@@ -56,16 +60,20 @@ class SecurityConfig(
.csrf { it.disable() } .csrf { it.disable() }
.cors { it.configurationSource(corsConfigurationSource()) } .cors { it.configurationSource(corsConfigurationSource()) }
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance()) .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.authorizeExchange { authorize -> .exceptionHandling { handlers ->
handlers
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
}.authorizeExchange { authorize ->
authorize authorize
.pathMatchers( .pathMatchers(
"/swagger-ui.html", "/swagger-ui.html",
"/swagger-ui/**", "/swagger-ui/**",
"/api-docs/**", "/api-docs/**",
"/api/v1/health", "/v1/health",
"/api/v1/auth/login", "/v1/auth/login",
"/api/v1/auth/refresh", "/v1/auth/refresh",
"/api/v1/auth/register", "/v1/auth/register",
).permitAll() ).permitAll()
.anyExchange() .anyExchange()
.authenticated() .authenticated()
@@ -0,0 +1,30 @@
package io.visus.demos.kotlinapi.infrastructure.security
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
@Component
class CustomAccessDeniedHandler : ServerAccessDeniedHandler {
override fun handle(
exchange: ServerWebExchange,
denied: AccessDeniedException,
): Mono<Void> =
Mono
.fromRunnable<Void> {
exchange.response.statusCode = HttpStatus.FORBIDDEN
exchange.response.headers.contentType = MediaType.APPLICATION_JSON
}.then(
exchange.response.writeWith(
Mono.just(
exchange.response.bufferFactory().wrap(
"""{"error":"Forbidden","message":"Access denied"}""".toByteArray(),
),
),
),
)
}
@@ -0,0 +1,30 @@
package io.visus.demos.kotlinapi.infrastructure.security
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.server.ServerAuthenticationEntryPoint
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
@Component
class CustomAuthenticationEntryPoint : ServerAuthenticationEntryPoint {
override fun commence(
exchange: ServerWebExchange,
ex: AuthenticationException,
): Mono<Void> =
Mono
.fromRunnable<Void> {
exchange.response.statusCode = HttpStatus.UNAUTHORIZED
exchange.response.headers.contentType = MediaType.APPLICATION_JSON
}.then(
exchange.response.writeWith(
Mono.just(
exchange.response.bufferFactory().wrap(
"""{"error":"Unauthorized","message":"Authentication required"}""".toByteArray(),
),
),
),
)
}