diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 7ff8be4..04a1a0c 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -8,7 +8,7 @@ jobs: build: runs-on: ubuntu-latest env: - SPRING_BASE_COMMONS_VERSION: 2.3.0 + SPRING_BASE_COMMONS_VERSION: 2.4.1 steps: - uses: actions/checkout@v4 - name: Set up JDK 25 diff --git a/Dockerfile b/Dockerfile index 40c2739..ac0fc50 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ ENV APP_NAME=app.jar ENV DEPS_FILE=deps.info # Change this when there is an update -ENV SPRING_BASE_COMMONS_VERSION=2.3.0 +ENV SPRING_BASE_COMMONS_VERSION=2.4.1 # Clone the spring-base-commons repository RUN git clone --depth 1 --branch ${SPRING_BASE_COMMONS_VERSION} https://github.com/vulinh64/spring-base-commons.git diff --git a/create-data-classes.cmd b/create-data-classes.cmd index 455afae..747955e 100644 --- a/create-data-classes.cmd +++ b/create-data-classes.cmd @@ -1,6 +1,6 @@ @echo off -SET SPRING_BASE_COMMONS_VERSION=2.3.0 +SET SPRING_BASE_COMMONS_VERSION=2.4.1 IF EXIST .\build\spring-base-commons rmdir /s /q .\build\spring-base-commons diff --git a/create-data-classes.sh b/create-data-classes.sh index 59cd689..f3aead6 100755 --- a/create-data-classes.sh +++ b/create-data-classes.sh @@ -2,7 +2,7 @@ set -e -SPRING_BASE_COMMONS_VERSION=2.3.0 +SPRING_BASE_COMMONS_VERSION=2.4.1 if [ -d "./build/spring-base-commons" ]; then rm -rf ./build/spring-base-commons diff --git a/pom.xml b/pom.xml index 8987ae6..2caccf6 100644 --- a/pom.xml +++ b/pom.xml @@ -68,7 +68,7 @@ 4.2.9.Final - 2.3.0 + 2.4.1 diff --git a/src/main/java/com/vulinh/SpringBaseProjectApplication.java b/src/main/java/com/vulinh/SpringBaseProjectApplication.java index 6d56edb..0487993 100644 --- a/src/main/java/com/vulinh/SpringBaseProjectApplication.java +++ b/src/main/java/com/vulinh/SpringBaseProjectApplication.java @@ -1,5 +1,6 @@ package com.vulinh; +import com.vulinh.aspect.ExecutionTimeAspect; import com.vulinh.configuration.AuditorConfiguration; import com.vulinh.configuration.data.ApplicationProperties; import lombok.AccessLevel; @@ -7,6 +8,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Import; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.web.config.EnableSpringDataWebSupport; import org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode; @@ -18,6 +20,7 @@ @EnableConfigurationProperties(ApplicationProperties.class) @EnableAsync @NoArgsConstructor(access = AccessLevel.PRIVATE) +@Import(ExecutionTimeAspect.class) class SpringBaseProjectApplication { static void main(String[] args) { diff --git a/src/main/java/com/vulinh/configuration/SecurityConfiguration.java b/src/main/java/com/vulinh/configuration/SecurityConfiguration.java index e49698a..906e4de 100644 --- a/src/main/java/com/vulinh/configuration/SecurityConfiguration.java +++ b/src/main/java/com/vulinh/configuration/SecurityConfiguration.java @@ -6,6 +6,8 @@ import com.vulinh.configuration.data.ApplicationProperties.SecurityProperties; import com.vulinh.data.constant.UserRole; import com.vulinh.utils.JwtUtils; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -13,7 +15,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -24,6 +25,7 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.servlet.HandlerExceptionResolver; @Configuration @EnableWebSecurity @@ -31,6 +33,9 @@ @Slf4j public class SecurityConfiguration { + // One of the best and the most elegant ways to handle exceptions in Spring Security filters + private final HandlerExceptionResolver handlerExceptionResolver; + static final String ROLE_ADMIN_NAME = UserRole.ADMIN.name(); @Bean @@ -47,7 +52,6 @@ public SecurityFilterChain securityFilterChain( xssConfig -> xssConfig.headerValue(HeaderValue.ENABLED_MODE_BLOCK)) .contentSecurityPolicy(cps -> cps.policyDirectives("script-src 'self'"))) .csrf(AbstractHttpConfigurer::disable) - .cors(Customizer.withDefaults()) .sessionManagement( sessionManagementConfigurer -> sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) @@ -57,12 +61,17 @@ public SecurityFilterChain securityFilterChain( .cors(corsConfigurer -> corsConfigurer.configurationSource(createCorsFilter())) .oauth2ResourceServer( oAuth2ResourceServerProperties -> - oAuth2ResourceServerProperties.jwt( - jwtConfigurer -> - jwtConfigurer.jwtAuthenticationConverter( - jwt -> - JwtUtils.parseAuthoritiesByCustomClaims( - jwt, security.clientName())))) + oAuth2ResourceServerProperties + // Return something to client rather than a blank 403 page + .accessDeniedHandler(this::delegateToHandlerExceptionResolver) + // Return something to client rather than a blank 401 page + .authenticationEntryPoint(this::delegateToHandlerExceptionResolver) + .jwt( + jwtConfigurer -> + jwtConfigurer.jwtAuthenticationConverter( + jwt -> + JwtUtils.parseAuthoritiesByCustomClaims( + jwt, security.clientName())))) .build(); } @@ -86,6 +95,11 @@ CorsConfigurationSource createCorsFilter() { return source; } + private void delegateToHandlerExceptionResolver( + HttpServletRequest request, HttpServletResponse response, Exception exception) { + handlerExceptionResolver.resolveException(request, response, null, exception); + } + static void configureAuthorizeHttpRequestCustomizer( AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry authorizeHttpRequestsCustomizer, diff --git a/src/main/java/com/vulinh/controller/impl/SubscriptionController.java b/src/main/java/com/vulinh/controller/impl/SubscriptionController.java index 5445012..e2206e4 100644 --- a/src/main/java/com/vulinh/controller/impl/SubscriptionController.java +++ b/src/main/java/com/vulinh/controller/impl/SubscriptionController.java @@ -3,7 +3,7 @@ import module java.base; import com.vulinh.controller.api.SubscriptionAPI; -import com.vulinh.service.event.UserSubscriptionService; +import com.vulinh.service.UserSubscriptionService; import com.vulinh.utils.ResponseUtils; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/com/vulinh/exception/AuthorizationException.java b/src/main/java/com/vulinh/exception/AuthorizationException.java index 72880e5..4dcefa4 100644 --- a/src/main/java/com/vulinh/exception/AuthorizationException.java +++ b/src/main/java/com/vulinh/exception/AuthorizationException.java @@ -24,7 +24,7 @@ public class AuthorizationException extends ApplicationException { /// @return A new [AuthorizationException] instance public static AuthorizationException invalidAuthorization(Object... args) { return invalidAuthorization( - "Invalid user authorization", ServiceErrorCode.MESSAGE_INVALID_AUTHORIZATION, args); + "Invalid user authorization", ServiceErrorCode.MESSAGE_INVALID_AUTHENTICATION, args); } /// Creates an [AuthorizationException] with a custom message and error code. diff --git a/src/main/java/com/vulinh/exception/GlobalExceptionHandler.java b/src/main/java/com/vulinh/exception/GlobalExceptionHandler.java index d065d12..e187636 100644 --- a/src/main/java/com/vulinh/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/vulinh/exception/GlobalExceptionHandler.java @@ -6,11 +6,13 @@ import com.vulinh.data.dto.response.GenericResponse.ResponseCreator; import com.vulinh.locale.LocalizationSupport; import com.vulinh.locale.ServiceErrorCode; +import com.vulinh.utils.validator.ApplicationError; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.TypeMismatchException; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.security.core.AuthenticationException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -112,6 +114,19 @@ GenericResponse handleKeycloakUserDisabledException( return logAndReturn(keycloakUserDisabledException); } + @ExceptionHandler(AuthenticationException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + GenericResponse handleInvalidBearerTokenException( + AuthenticationException authenticationException) { + return securityError(ServiceErrorCode.MESSAGE_INVALID_AUTHENTICATION, authenticationException); + } + + @ExceptionHandler(AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + GenericResponse handleAccessDeniedException(AccessDeniedException accessDeniedException) { + return securityError(ServiceErrorCode.MESSAGE_INSUFFICIENT_PERMISSION, accessDeniedException); + } + static GenericResponse badRequestBody(String additionalMessage) { return GenericResponse.builder() .errorCode(ServiceErrorCode.MESSAGE_INVALID_BODY_REQUEST.getErrorCode()) @@ -132,4 +147,11 @@ static GenericResponse stackTraceAndReturn(ApplicationException applicat return ResponseCreator.toError(applicationException); } + + static GenericResponse securityError( + ApplicationError applicationError, Throwable throwable) { + log.info(throwable.getMessage(), throwable); + + return ResponseCreator.toError(applicationError); + } } diff --git a/src/main/java/com/vulinh/locale/ServiceErrorCode.java b/src/main/java/com/vulinh/locale/ServiceErrorCode.java index ae77081..ce813f3 100644 --- a/src/main/java/com/vulinh/locale/ServiceErrorCode.java +++ b/src/main/java/com/vulinh/locale/ServiceErrorCode.java @@ -24,7 +24,8 @@ public enum ServiceErrorCode implements ApplicationError { MESSAGE_INVALID_CATEGORY_SLUG("M3002"), MESSAGE_DEFAULT_CATEGORY_IMMORTAL("M3003"), - MESSAGE_INVALID_AUTHORIZATION("M9102"), + MESSAGE_INVALID_AUTHENTICATION("M9401"), + MESSAGE_INSUFFICIENT_PERMISSION("M9403"), MESSAGE_INVALID_BODY_REQUEST("M9106"), MESSAGE_INVALID_OWNER_OR_NO_RIGHT("M9107"), diff --git a/src/main/java/com/vulinh/service/event/UserSubscriptionService.java b/src/main/java/com/vulinh/service/UserSubscriptionService.java similarity index 96% rename from src/main/java/com/vulinh/service/event/UserSubscriptionService.java rename to src/main/java/com/vulinh/service/UserSubscriptionService.java index 80bad5c..bfdd991 100644 --- a/src/main/java/com/vulinh/service/event/UserSubscriptionService.java +++ b/src/main/java/com/vulinh/service/UserSubscriptionService.java @@ -1,8 +1,9 @@ -package com.vulinh.service.event; +package com.vulinh.service; import module java.base; import com.vulinh.exception.KeycloakUserDisabledException; +import com.vulinh.service.event.EventService; import com.vulinh.service.keycloak.KeycloakAdminClientService; import com.vulinh.service.post.PostValidationService; import com.vulinh.utils.SecurityUtils; diff --git a/src/main/java/com/vulinh/service/event/EventService.java b/src/main/java/com/vulinh/service/event/EventService.java index c1f6b9a..88950dd 100644 --- a/src/main/java/com/vulinh/service/event/EventService.java +++ b/src/main/java/com/vulinh/service/event/EventService.java @@ -2,6 +2,7 @@ import module java.base; +import com.vulinh.annotation.ExecutionTime; import com.vulinh.configuration.data.ApplicationProperties; import com.vulinh.configuration.data.ApplicationProperties.TopicProperties; import com.vulinh.data.dto.response.KeycloakUserResponse; @@ -29,6 +30,7 @@ public class EventService { final ApplicationProperties applicationProperties; + @ExecutionTime public void sendNewPostEvent(Post post, UserBasicResponse actionUser) { sendMessageInternal( applicationProperties.messageTopic().newPost(), @@ -36,6 +38,7 @@ public void sendNewPostEvent(Post post, UserBasicResponse actionUser) { EVENT_MAPPER.toNewPostEvent(post)); } + @ExecutionTime public void sendSubscribeToUserEvent( UserBasicResponse basicActionUser, KeycloakUserResponse subscribedUser) { sendMessageInternal( @@ -44,6 +47,7 @@ public void sendSubscribeToUserEvent( EVENT_MAPPER.toNewSubscriptionEvent(subscribedUser)); } + @ExecutionTime public void sendNewCommentEvent(Comment comment, Post post, UserBasicResponse basicActionUser) { sendMessageInternal( applicationProperties.messageTopic().newComment(), @@ -51,6 +55,7 @@ public void sendNewCommentEvent(Comment comment, Post post, UserBasicResponse ba EVENT_MAPPER.toNewCommentEvent(comment, post)); } + @ExecutionTime public void sendNewPostFollowingEvent(Post post, UserBasicResponse basicActionUser) { sendMessageInternal( applicationProperties.messageTopic().newPostFollowing(), diff --git a/src/main/java/com/vulinh/service/keycloak/KeycloakAdminClientService.java b/src/main/java/com/vulinh/service/keycloak/KeycloakAdminClientService.java index 1d27bb9..2a3fdb0 100644 --- a/src/main/java/com/vulinh/service/keycloak/KeycloakAdminClientService.java +++ b/src/main/java/com/vulinh/service/keycloak/KeycloakAdminClientService.java @@ -2,6 +2,7 @@ import module java.base; +import com.vulinh.annotation.ExecutionTime; import com.vulinh.configuration.data.ApplicationProperties; import com.vulinh.data.dto.response.KeycloakUserResponse; import com.vulinh.data.mapper.KeycloakMapper; @@ -22,6 +23,7 @@ public class KeycloakAdminClientService { final Keycloak keycloak; + @ExecutionTime @NonNull public KeycloakUserResponse getKeycloakUser(UUID userId) { try { diff --git a/src/main/resources/application-development.yaml b/src/main/resources/application-development.yaml index b4f5b24..e60b3ec 100644 --- a/src/main/resources/application-development.yaml +++ b/src/main/resources/application-development.yaml @@ -10,4 +10,5 @@ logging.level: PostEditValidationService: DEBUG category.CategoryService: DEBUG configuration.com.vulinh.configuration.SecurityConfiguration: DEBUG - org.springframework.security.oauth2: TRACE \ No newline at end of file + # org.springframework.security.oauth2: TRACE + # Remove the comment sign for extra debugging information \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 563bafe..817fb5b 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -35,6 +35,7 @@ application-properties: no-authenticated-urls: - /free/** - /health + - /health/** - /v3/api-docs/** - /swagger-ui.html - /swagger-ui/** diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 31d339a..6a6cd05 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -6,7 +6,8 @@ M9999=Internal Server error, please contact the development team! # # Authorization errors # -M9102=Invalid authorization info +M9401=Invalid authentication info +M9403=Insufficient permission to access the resource M9106=Invalid request body (%s) M9107=Invalid author or no permission to edit # diff --git a/src/main/resources/i18n/messages_vi.properties b/src/main/resources/i18n/messages_vi.properties index e741c50..4a1c751 100644 --- a/src/main/resources/i18n/messages_vi.properties +++ b/src/main/resources/i18n/messages_vi.properties @@ -6,7 +6,8 @@ M9999=\u0110\u00E3 c\u00F3 l\u1ED7i x\u1EA3y ra, vui l\u00F2ng li\u00EAn h\u1EC7 # # Authorization errors # -M9102=Th\u00F4ng tin x\u00E1c th\u1EF1c kh\u00F4ng h\u1EE3p l\u1EC7 +M9401=Th\u00F4ng tin x\u00E1c th\u1EF1c kh\u00F4ng h\u1EE3p l\u1EC7 +M9403=Kh\u00F4ng c\u00F3 quy\u1EC1n h\u1EA1n truy c\u1EADp t\u00E0i nguy\u00EAn M9106=B\u1EA3n tin kh\u00F4ng h\u1EE3p l\u1EC7 (%s) M9107=Kh\u00F4ng ph\u1EA3i t\u00E1c gi\u1EA3 ho\u1EB7c kh\u00F4ng c\u00F3 quy\u1EC1n #