diff --git a/src/backend/auth_server/build.gradle b/src/backend/auth_server/build.gradle index 79904f51..0e792533 100644 --- a/src/backend/auth_server/build.gradle +++ b/src/backend/auth_server/build.gradle @@ -15,11 +15,13 @@ java { repositories { mavenCentral() + maven { url 'https://repo.spring.io/milestone' } } dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.2" + mavenBom "org.springframework.security:spring-security-bom:6.3.0-M2" } } @@ -27,14 +29,15 @@ dependencies { //common-module implementation project(":common_module") + //webflux + implementation 'org.springframework.boot:spring-boot-starter-webflux' + // Spring - implementation 'org.springframework.boot:spring-boot-starter-web' developmentOnly 'org.springframework.boot:spring-boot-devtools' - implementation 'org.springframework.boot:spring-boot-starter-actuator' //db - implementation group: 'org.postgresql', name: 'postgresql', version: '42.7.3' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc' + runtimeOnly 'org.postgresql:r2dbc-postgresql' // Lombok compileOnly 'org.projectlombok:lombok' @@ -43,17 +46,11 @@ dependencies { // Validation implementation 'org.springframework.boot:spring-boot-starter-validation' - // Test - testImplementation 'org.springframework.boot:spring-boot-starter-test' - - //junit - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - // Spring Cloud OpenFeign implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' //Swagger - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' + implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.1.0' //JWT implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' @@ -65,7 +62,7 @@ dependencies { //Security implementation 'org.springframework.boot:spring-boot-starter-security' - testImplementation 'org.springframework.security:spring-security-test' + implementation 'org.springframework.security:spring-security-config' } tasks.named('test') { diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/AuthServerApplication.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/AuthServerApplication.java index c168d744..0ffcd685 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/AuthServerApplication.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/AuthServerApplication.java @@ -3,18 +3,30 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.openfeign.FeignAutoConfiguration; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.r2dbc.config.EnableR2dbcAuditing; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; -@EnableJpaAuditing +@EnableR2dbcAuditing @EnableFeignClients @ImportAutoConfiguration(FeignAutoConfiguration.class) -@SpringBootApplication(scanBasePackages = { - "com.jootalkpia.auth_server", - "com.jootalkpia.passport", - "com.jootalkpia.config", -}) +@EnableR2dbcRepositories(basePackages = "com.jootalkpia.auth_server.user.repository") +@EnableRedisRepositories(basePackages = "com.jootalkpia.auth_server.jwt") +@SpringBootApplication( + scanBasePackages = { + "com.jootalkpia.auth_server", + "com.jootalkpia.passport", + "com.jootalkpia.config", + }, + exclude = { + SecurityAutoConfiguration.class, + UserDetailsServiceAutoConfiguration.class + } +) public class AuthServerApplication { public static void main(String[] args) { diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoApiClient.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoApiClient.java index b6228690..6120c4bf 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoApiClient.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoApiClient.java @@ -2,14 +2,28 @@ import com.jootalkpia.auth_server.client.kakao.response.KakaoUserResponse; -import org.springframework.cloud.openfeign.FeignClient; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; -@FeignClient(name = "kakaoApiClient", url = "https://kapi.kakao.com") -public interface KakaoApiClient { +@Service +@RequiredArgsConstructor +public class KakaoApiClient { - @GetMapping(value = "/v2/user/me") - KakaoUserResponse getUserInformation(@RequestHeader(HttpHeaders.AUTHORIZATION) String accessToken); + private final WebClient webClient = WebClient.builder() + .baseUrl("https://kapi.kakao.com") + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .build(); + + public Mono getUserInfo(String accessToken) { + return webClient.get() + .uri("/v2/user/me") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .retrieve() + .bodyToMono(KakaoUserResponse.class); + } } + diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoAuthApiClient.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoAuthApiClient.java deleted file mode 100644 index 1c420a9c..00000000 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoAuthApiClient.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.jootalkpia.auth_server.client.kakao; - - -import com.jootalkpia.auth_server.client.kakao.response.KakaoAccessTokenResponse; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; - -@FeignClient(name = "kakaoAuthApiClient", url = "https://kauth.kakao.com") -public interface KakaoAuthApiClient { - @PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) - KakaoAccessTokenResponse getOAuth2AccessToken( - @RequestParam("grant_type") String grantType, - @RequestParam("client_id") String clientId, - @RequestParam("redirect_uri") String redirectUri, - @RequestParam("code") String code - ); -} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoAuthWebClientService.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoAuthWebClientService.java new file mode 100644 index 00000000..67e92f80 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoAuthWebClientService.java @@ -0,0 +1,36 @@ +package com.jootalkpia.auth_server.client.kakao; + +import com.jootalkpia.auth_server.client.kakao.response.KakaoTokenResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@Service +@RequiredArgsConstructor +public class KakaoAuthWebClientService { + + @Value("${kakao.clientId}") + private String clientId; + + private final WebClient kakaoWebClient = WebClient.builder() + .baseUrl("https://kauth.kakao.com") + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .build(); + + public Mono getAccessToken(String code, String redirectUri) { + return kakaoWebClient.post() + .uri("/oauth/token") + .body(BodyInserters + .fromFormData("grant_type", "authorization_code") + .with("client_id", clientId) + .with("redirect_uri", redirectUri) + .with("code", code)) + .retrieve() + .bodyToMono(KakaoTokenResponse.class); + } +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoSocialService.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoSocialService.java index 6b0aec17..7a00ed64 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoSocialService.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/KakaoSocialService.java @@ -2,65 +2,40 @@ import com.jootalkpia.auth_server.client.dto.UserInfoResponse; import com.jootalkpia.auth_server.client.dto.UserLoginRequest; -import com.jootalkpia.auth_server.client.kakao.response.KakaoAccessTokenResponse; import com.jootalkpia.auth_server.client.kakao.response.KakaoUserResponse; import com.jootalkpia.auth_server.client.service.SocialService; import com.jootalkpia.auth_server.exception.CustomException; import com.jootalkpia.auth_server.response.ErrorCode; import com.jootalkpia.auth_server.user.domain.Platform; -import feign.FeignException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import reactor.core.publisher.Mono; @Service @Slf4j @RequiredArgsConstructor public class KakaoSocialService implements SocialService { - private static final String AUTH_CODE = "authorization_code"; - - @Value("${kakao.clientId}") - private String clientId; private final KakaoApiClient kakaoApiClient; - private final KakaoAuthApiClient kakaoAuthApiClient; + private final KakaoAuthWebClientService kakaoAuthWebClientService; @Transactional @Override - public UserInfoResponse login( + public Mono login( final String authorizationCode, final UserLoginRequest loginRequest ) { - String accessToken; - try { - // 인가 코드로 Access Token + Refresh Token 받아오기 - accessToken = getOAuth2Authentication(authorizationCode, loginRequest.redirectUri()); - } catch (FeignException e) { - throw new CustomException(ErrorCode.AUTHENTICATION_CODE_EXPIRED); - } - // Access Token으로 유저 정보 불러오기 - return getLoginDto(loginRequest.platform(), getUserInfo(accessToken)); - } - - private String getOAuth2Authentication( - final String authorizationCode, - final String redirectUri - ) { - KakaoAccessTokenResponse response = kakaoAuthApiClient.getOAuth2AccessToken( - AUTH_CODE, - clientId, - redirectUri, - authorizationCode - ); - return response.accessToken(); - } - - private KakaoUserResponse getUserInfo( - final String accessToken - ) { - return kakaoApiClient.getUserInformation("Bearer " + accessToken); + return kakaoAuthWebClientService.getAccessToken(authorizationCode, loginRequest.redirectUri()) + .flatMap(tokenResponse -> + kakaoApiClient.getUserInfo(tokenResponse.getAccessToken()) + ) + .map(userResponse -> getLoginDto(loginRequest.platform(), userResponse)) + .onErrorMap(Exception.class, e -> { + log.error("❌ Kakao 로그인 실패: {}", e.getMessage(), e); + return new CustomException(ErrorCode.AUTHENTICATION_CODE_EXPIRED); + }); } private UserInfoResponse getLoginDto( @@ -74,4 +49,4 @@ private UserInfoResponse getLoginDto( userResponse.kakaoAccount().profile().nickname() ); } -} +} \ No newline at end of file diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoTokenResponse.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoTokenResponse.java new file mode 100644 index 00000000..a365a167 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/kakao/response/KakaoTokenResponse.java @@ -0,0 +1,30 @@ +package com.jootalkpia.auth_server.client.kakao.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class KakaoTokenResponse { + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("refresh_token") + private String refreshToken; + + @JsonProperty("expires_in") + private Long expiresIn; + + @JsonProperty("token_type") + private String tokenType; + + // 필요에 따라 추가적인 필드들: + @JsonProperty("scope") + private String scope; + + @JsonProperty("refresh_token_expires_in") + private Long refreshTokenExpiresIn; + +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/service/SocialService.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/service/SocialService.java index 3eb12e5a..9e330729 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/service/SocialService.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/client/service/SocialService.java @@ -1,9 +1,9 @@ package com.jootalkpia.auth_server.client.service; - import com.jootalkpia.auth_server.client.dto.UserInfoResponse; import com.jootalkpia.auth_server.client.dto.UserLoginRequest; +import reactor.core.publisher.Mono; public interface SocialService { - UserInfoResponse login(final String authorizationToken, final UserLoginRequest loginRequest); + Mono login(final String authorizationToken, final UserLoginRequest loginRequest); } diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/config/SecurityConfig.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/config/SecurityConfig.java index 16494a96..95b2f2ab 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/config/SecurityConfig.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/config/SecurityConfig.java @@ -1,26 +1,37 @@ package com.jootalkpia.auth_server.config; -import com.jootalkpia.auth_server.jwt.JwtAuthenticationFilter; -import com.jootalkpia.auth_server.security.CustomAccessDeniedHandler; -import com.jootalkpia.auth_server.security.CustomJwtAuthenticationEntryPoint; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jootalkpia.auth_server.jwt.JwtReactiveAuthenticationManager; +import com.jootalkpia.auth_server.jwt.JwtServerAuthenticationConverter; +import com.jootalkpia.auth_server.jwt.JwtTokenProvider; +import com.jootalkpia.auth_server.response.ApiResponseDto; +import com.jootalkpia.auth_server.response.ErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.ServerAuthenticationEntryPoint; +import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; +import org.springframework.security.web.server.authentication.AuthenticationWebFilter; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import reactor.core.publisher.Mono; @Configuration +@EnableWebFluxSecurity +@EnableReactiveMethodSecurity @RequiredArgsConstructor -@EnableWebSecurity //web Security를 사용할 수 있게 public class SecurityConfig { - private final JwtAuthenticationFilter jwtAuthenticationFilter; - private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; - private final CustomAccessDeniedHandler customAccessDeniedHandler; + private final JwtTokenProvider jwtTokenProvider; + private final ObjectMapper objectMapper = new ObjectMapper(); // JSON 변환용 private static final String[] AUTH_WHITELIST = { "/api/v1/user/login", @@ -31,25 +42,67 @@ public class SecurityConfig { }; @Bean - SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http.csrf(AbstractHttpConfigurer::disable) - .formLogin(AbstractHttpConfigurer::disable) - .httpBasic(AbstractHttpConfigurer::disable) - .sessionManagement(session -> - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .exceptionHandling(exception -> - { - exception.authenticationEntryPoint(customJwtAuthenticationEntryPoint); - exception.accessDeniedHandler(customAccessDeniedHandler); - }); - - http.authorizeHttpRequests(auth -> { - auth.requestMatchers(AUTH_WHITELIST).permitAll(); - auth.anyRequest().authenticated(); - }) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - - return http.build(); + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + return http + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) + .formLogin(ServerHttpSecurity.FormLoginSpec::disable) + + .exceptionHandling(exception -> exception + .authenticationEntryPoint(customJwtAuthenticationEntryPoint()) + .accessDeniedHandler(customAccessDeniedHandler()) + ) + + .authorizeExchange(auth -> auth + .pathMatchers(AUTH_WHITELIST).permitAll() + .anyExchange().authenticated() + ) + + .addFilterAt(jwtAuthenticationFilter(), SecurityWebFiltersOrder.AUTHENTICATION) + .build(); + } + + private AuthenticationWebFilter jwtAuthenticationFilter() { + ReactiveAuthenticationManager authManager = new JwtReactiveAuthenticationManager(jwtTokenProvider); + AuthenticationWebFilter filter = new AuthenticationWebFilter(authManager); + filter.setServerAuthenticationConverter(new JwtServerAuthenticationConverter()); + filter.setRequiresAuthenticationMatcher(ServerWebExchangeMatchers.anyExchange()); + return filter; + } + + private ServerAuthenticationEntryPoint customJwtAuthenticationEntryPoint() { + return (exchange, ex) -> { + ErrorCode error = ErrorCode.EMPTY_PRINCIPAL; + ApiResponseDto errorResponse = ApiResponseDto.fail(error); + + exchange.getResponse().setStatusCode(error.getHttpStatus()); + exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + + try { + byte[] bytes = objectMapper.writeValueAsBytes(errorResponse); + DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); + return exchange.getResponse().writeWith(Mono.just(buffer)); + } catch (JsonProcessingException e) { + return Mono.error(e); + } + }; } + private ServerAccessDeniedHandler customAccessDeniedHandler() { + return (exchange, ex) -> { + ErrorCode error = ErrorCode.FORBIDDEN; + ApiResponseDto errorResponse = ApiResponseDto.fail(error); + + exchange.getResponse().setStatusCode(error.getHttpStatus()); + exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + + try { + byte[] bytes = objectMapper.writeValueAsBytes(errorResponse); + DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); + return exchange.getResponse().writeWith(Mono.just(buffer)); + } catch (JsonProcessingException e) { + return Mono.error(e); + } + }; + } } diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/config/WebConfig.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/config/WebConfig.java index d9438e3f..fcf0c9ef 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/config/WebConfig.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/config/WebConfig.java @@ -1,11 +1,11 @@ package com.jootalkpia.auth_server.config; import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.reactive.config.CorsRegistry; +import org.springframework.web.reactive.config.WebFluxConfigurer; @Configuration -public class WebConfig implements WebMvcConfigurer { +public class WebConfig implements WebFluxConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/exception/GlobalExceptionHandler.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/exception/GlobalExceptionHandler.java index f59f18cf..0b1aa6a1 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/exception/GlobalExceptionHandler.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/exception/GlobalExceptionHandler.java @@ -2,108 +2,101 @@ import com.jootalkpia.auth_server.response.ApiResponseDto; import com.jootalkpia.auth_server.response.ErrorCode; -import lombok.RequiredArgsConstructor; +import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; +import org.springframework.core.codec.DecodingException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.web.HttpRequestMethodNotSupportedException; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.MissingRequestHeaderException; -import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.ErrorResponseException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.method.annotation.HandlerMethodValidationException; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.bind.support.WebExchangeBindException; +import org.springframework.web.server.MissingRequestValueException; +import reactor.core.publisher.Mono; @Slf4j @RestControllerAdvice -@RequiredArgsConstructor public class GlobalExceptionHandler { - @ExceptionHandler(value = {MethodArgumentNotValidException.class}) - public ResponseEntity> handlerMethodArgumentNotValidException(Exception e) { - log.error( - "handlerMethodArgumentNotValidException() in GlobalExceptionHandler throw MethodArgumentNotValidException : {}", - e.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponseDto.fail(ErrorCode.BAD_REQUEST)); + /** + * @Valid 또는 @Validated에서 바인딩 실패 시 발생 + */ + @ExceptionHandler(WebExchangeBindException.class) + public Mono>> handleWebExchangeBindException(WebExchangeBindException e) { + log.warn("📛 WebExchangeBindException: {}", e.getMessage()); + ApiResponseDto response = ApiResponseDto.fail(ErrorCode.BAD_REQUEST); + return Mono.just(ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(response)); } - @ExceptionHandler(value = {MethodArgumentTypeMismatchException.class}) - public ResponseEntity> handlerMethodArgumentTypeMismatchException(Exception e) { - log.error( - "handlerMethodArgumentTypeMismatchException() in GlobalExceptionHandler throw MethodArgumentTypeMismatchException : {}", - e.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponseDto.fail(ErrorCode.BAD_REQUEST)); + /** + * PathVariable, RequestParam 타입 불일치 + */ + @ExceptionHandler(ConstraintViolationException.class) + public Mono>> handleConstraintViolationException(ConstraintViolationException e) { + log.warn("📛 ConstraintViolationException: {}", e.getMessage()); + ApiResponseDto response = ApiResponseDto.fail(ErrorCode.BAD_REQUEST); + return Mono.just(ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(response)); } - @ExceptionHandler(value = {HandlerMethodValidationException.class}) - public ResponseEntity> handlerHandlerMethodValidationException(Exception e) { - log.error( - "handlerHandlerMethodValidationException() in GlobalExceptionHandler throw HandlerMethodValidationException : {}", - e.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponseDto.fail(ErrorCode.BAD_REQUEST)); + /** + * JSON 파싱 실패 등 메시지 매핑 오류 + */ + @ExceptionHandler(DecodingException.class) + public Mono>> handleDecodingException(DecodingException e) { + log.warn("📛 DecodingException: {}", e.getMessage()); + ApiResponseDto response = ApiResponseDto.fail(ErrorCode.BAD_REQUEST); + return Mono.just(ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(response)); } - @ExceptionHandler(value = {MissingRequestHeaderException.class}) - public ResponseEntity> handlerMissingRequestHeaderException(Exception e) { - log.error( - "handlerMissingRequestHeaderException() in GlobalExceptionHandler throw MissingRequestHeaderException : {}", - e.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponseDto.fail(ErrorCode.MISSING_REQUIRED_HEADER)); - } - - @ExceptionHandler(value = {MissingServletRequestParameterException.class}) - public ResponseEntity> handlerMissingServletRequestParameterException(Exception e) { - log.error( - "handlerMissingServletRequestParameterException() in GlobalExceptionHandler throw MissingServletRequestParameterException : {}", - e.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponseDto.fail(ErrorCode.MISSING_REQUIRED_PARAMETER)); - } - - @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity> handleMessageNotReadableException(HttpMessageNotReadableException e) { - log.error( - "handleMessageNotReadableException() in GlobalExceptionHandler throw HttpMessageNotReadableException : {}", - e.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponseDto.fail(ErrorCode.BAD_REQUEST)); - } - - @ExceptionHandler(value = {NoHandlerFoundException.class}) - public ResponseEntity> handleNoPageFoundException(Exception e) { - log.error("handleNoPageFoundException() in GlobalExceptionHandler throw NoHandlerFoundException : {}", - e.getMessage()); - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponseDto.fail(ErrorCode.NOT_FOUND_END_POINT)); - } - - @ExceptionHandler(value = {HttpRequestMethodNotSupportedException.class}) - public ResponseEntity> handleMethodNotSupportedException(Exception e) { - log.error( - "handleMethodNotSupportedException() in GlobalExceptionHandler throw HttpRequestMethodNotSupportedException : {}", - e.getMessage()); - return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED) - .body(ApiResponseDto.fail(ErrorCode.METHOD_NOT_ALLOWED)); + /** + * WebFlux에서 일반적인 잘못된 요청 처리 + */ + @ExceptionHandler(ErrorResponseException.class) + public Mono>> handleErrorResponseException(ErrorResponseException e) { + log.warn("📛 ErrorResponseException: {}", e.getMessage()); + ApiResponseDto response = ApiResponseDto.fail(ErrorCode.BAD_REQUEST); + return Mono.just(ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(response)); } + /** + * 사용자 정의 예외 처리 + */ @ExceptionHandler(CustomException.class) - public ResponseEntity> handleCustomException(CustomException e) { - log.error("handleException() in GlobalExceptionHandler throw BusinessException : {}", e.getMessage()); - return ResponseEntity.status(e.getHttpStatus()) - .body(ApiResponseDto.fail(e.getErrorCode())); + public Mono>> handleCustomException(CustomException e) { + log.error("❌ CustomException: {}", e.getMessage()); + ApiResponseDto response = ApiResponseDto.fail(e.getErrorCode()); + log.error("❌ CustomException: {}", response); + return Mono.just(ResponseEntity + .status(e.getHttpStatus()) + .body(response)); } + /** + * 처리되지 않은 기타 모든 예외 + */ @ExceptionHandler(Exception.class) - public ResponseEntity> handlerException(Exception e) { - log.error("handlerException() in GlobalExceptionHandler throw Exception : {} {}", e.getClass(), e.getMessage()); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponseDto.fail(ErrorCode.INTERNAL_SERVER_ERROR)); + public Mono>> handleException(Exception e) { + log.error("❌ Unhandled Exception: {} - {}", e.getClass(), e.getMessage(), e); + ApiResponseDto response = ApiResponseDto.fail(ErrorCode.INTERNAL_SERVER_ERROR); + return Mono.just(ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(response)); + } + + @ExceptionHandler(MissingRequestValueException.class) + public Mono>> handleMissingRequestValue(MissingRequestValueException e) { + log.warn("📛 MissingRequestValueException: {}", e.getMessage()); + ApiResponseDto response = ApiResponseDto.fail(ErrorCode.MISSING_REQUIRED_PARAMETER); + return Mono.just(ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(response)); } -} \ No newline at end of file +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/exception/ResponseStatusSetterAdvice.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/exception/ResponseStatusSetterAdvice.java deleted file mode 100644 index a24d35fc..00000000 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/exception/ResponseStatusSetterAdvice.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.jootalkpia.auth_server.exception; - -import com.jootalkpia.auth_server.response.ApiResponseDto; -import org.springframework.core.MethodParameter; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; - -@RestControllerAdvice -public class ResponseStatusSetterAdvice implements ResponseBodyAdvice> { - - @Override - public boolean supports(MethodParameter returnType, Class converterType) { - return returnType.getParameterType() == ApiResponseDto.class; - } - - @Override - public ApiResponseDto beforeBodyWrite( - ApiResponseDto body, - MethodParameter returnType, - MediaType selectedContentType, - Class selectedConverterType, - ServerHttpRequest request, - ServerHttpResponse response - ) { - HttpStatus status = body.httpStatus(); - response.setStatusCode(status); - - return body; - } -} \ No newline at end of file diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/JwtAuthenticationFilter.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/JwtAuthenticationFilter.java index e0b5bbf1..bf65fc44 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/JwtAuthenticationFilter.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/JwtAuthenticationFilter.java @@ -1,80 +1,70 @@ package com.jootalkpia.auth_server.jwt; -import static com.jootalkpia.auth_server.jwt.JwtValidationType.EXPIRED_JWT_TOKEN; -import static com.jootalkpia.auth_server.jwt.JwtValidationType.VALID_JWT; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.jootalkpia.auth_server.exception.CustomException; -import com.jootalkpia.auth_server.response.ApiResponseDto; import com.jootalkpia.auth_server.response.ErrorCode; import com.jootalkpia.auth_server.security.UserAuthentication; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; -import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; +import java.nio.charset.StandardCharsets; @Component @RequiredArgsConstructor -public class JwtAuthenticationFilter extends OncePerRequestFilter { +public class JwtAuthenticationFilter implements WebFilter { private final JwtTokenProvider jwtTokenProvider; @Override - protected void doFilterInternal(@NonNull HttpServletRequest request, - @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain) throws ServletException, IOException { - try { - final String token = getJwtFromRequest(request); - - if (token != null && jwtTokenProvider.validateToken(token) == VALID_JWT) { - Long userId = jwtTokenProvider.getUserFromJwt(token); + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + String token = extractToken(exchange); - UserAuthentication authentication = new UserAuthentication(userId.toString(),null, null); - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authentication); - } else if (jwtTokenProvider.validateToken(token) == EXPIRED_JWT_TOKEN) { - // 토큰이 만료된 경우 CustomException 던짐 - throw new CustomException(ErrorCode.ACCESS_TOKEN_EXPIRED); + if (token != null) { + switch (jwtTokenProvider.validateToken(token)) { + case VALID_JWT -> { + Long userId = jwtTokenProvider.getUserFromJwt(token); + UserAuthentication authentication = new UserAuthentication(userId.toString(), null, null); + return chain.filter(exchange) + .contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication)); + } + case EXPIRED_JWT_TOKEN -> { + return handleException(exchange, ErrorCode.ACCESS_TOKEN_EXPIRED); + } + default -> { + return handleException(exchange, ErrorCode.EMPTY_PRINCIPAL); + } } - - // 다음 필터로 요청 전달 - filterChain.doFilter(request, response); - - } catch (CustomException e) { - // CustomException을 잡아서 클라이언트에게 응답 - handleException(response, e.getErrorCode()); } + return chain.filter(exchange); } - // 예외 처리 및 응답 - private void handleException(HttpServletResponse response, ErrorCode errorCode) throws IOException { - response.setStatus(errorCode.getHttpStatus().value()); - response.setContentType("application/json; charset=UTF-8"); - - // ObjectMapper는 예외 처리시에만 생성하여 메모리 최적화 - ObjectMapper objectMapper = new ObjectMapper(); - - // 실패 응답 생성 - ApiResponseDto apiResponse = ApiResponseDto.fail(errorCode); - - // 응답을 JSON 형식으로 변환 후 출력 - response.getWriter().write(objectMapper.writeValueAsString(apiResponse)); + private String extractToken(ServerWebExchange exchange) { + String bearer = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) { + return bearer.substring(7); + } + return null; } - // JWT를 요청 헤더에서 추출 - private String getJwtFromRequest(HttpServletRequest request) { - String bearerToken = request.getHeader("Authorization"); - if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring("Bearer ".length()); + private Mono handleException(ServerWebExchange exchange, ErrorCode errorCode) { + exchange.getResponse().setStatusCode(errorCode.getHttpStatus()); + exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + + String body = String.format(""" + { + "code": "%s", + "message": "%s" } - return null; + """, errorCode.getCode(), errorCode.getMessage()); + + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + return exchange.getResponse() + .writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(bytes))); } } + diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/JwtReactiveAuthenticationManager.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/JwtReactiveAuthenticationManager.java new file mode 100644 index 00000000..5fd402b5 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/JwtReactiveAuthenticationManager.java @@ -0,0 +1,27 @@ +package com.jootalkpia.auth_server.jwt; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import reactor.core.publisher.Mono; + +import java.util.Collections; + +@RequiredArgsConstructor +public class JwtReactiveAuthenticationManager implements ReactiveAuthenticationManager { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + public Mono authenticate(Authentication authentication) { + String token = authentication.getCredentials().toString(); + + if (jwtTokenProvider.validateToken(token) == JwtValidationType.VALID_JWT) { + Long userId = jwtTokenProvider.getUserFromJwt(token); + return Mono.just(new UsernamePasswordAuthenticationToken(userId.toString(), null, Collections.emptyList())); + } + + return Mono.empty(); // 인증 실패 + } +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/JwtServerAuthenticationConverter.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/JwtServerAuthenticationConverter.java new file mode 100644 index 00000000..7e5da689 --- /dev/null +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/JwtServerAuthenticationConverter.java @@ -0,0 +1,24 @@ +package com.jootalkpia.auth_server.jwt; + +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +public class JwtServerAuthenticationConverter implements ServerAuthenticationConverter { + + @Override + public Mono convert(ServerWebExchange exchange) { + String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + + if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + return Mono.just(new UsernamePasswordAuthenticationToken(null, token)); + } + + return Mono.empty(); // 토큰 없음 + } +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/TokenRepository.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/TokenRepository.java index ad6f5bb9..282397bd 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/TokenRepository.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/TokenRepository.java @@ -5,8 +5,5 @@ import org.springframework.data.repository.CrudRepository; public interface TokenRepository extends CrudRepository { - Optional findByRefreshToken(final String refreshToken); - - Optional findById(final Long id); -} \ No newline at end of file +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/TokenService.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/TokenService.java index 4b4db466..cb1c1578 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/TokenService.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/jwt/TokenService.java @@ -5,7 +5,7 @@ import com.jootalkpia.auth_server.response.ErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import reactor.core.publisher.Mono; @RequiredArgsConstructor @Service @@ -13,18 +13,16 @@ public class TokenService { private final TokenRepository tokenRepository; - @Transactional - public void saveRefreshToken(final Long userId, final String refreshToken) { - tokenRepository.save( - Token.of(userId, refreshToken) - ); + public Mono saveRefreshToken(final Long userId, final String refreshToken) { + return Mono.fromRunnable(() -> tokenRepository.save(Token.of(userId, refreshToken))); } - public Long findIdByRefreshToken(final String refreshToken) { - Token token = tokenRepository.findByRefreshToken(refreshToken) - .orElseThrow( - () -> new CustomException(ErrorCode.REFRESH_TOKEN_NOT_FOUND) - ); - return token.getId(); + public Mono findIdByRefreshToken(final String refreshToken) { + return Mono.defer(() -> + tokenRepository.findByRefreshToken(refreshToken) + .map(Token::getId) + .map(Mono::just) + .orElseGet(() -> Mono.error(new CustomException(ErrorCode.REFRESH_TOKEN_NOT_FOUND))) + ); } -} \ No newline at end of file +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/redis/Token.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/redis/Token.java index 34481cb0..bf1b4788 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/redis/Token.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/redis/Token.java @@ -7,7 +7,7 @@ import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.index.Indexed; -@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 24 * 1000L * 14) +@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 24 * 14) @AllArgsConstructor @Getter @Builder diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/response/ErrorCode.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/response/ErrorCode.java index 0b88f9ef..c4a15ff7 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/response/ErrorCode.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/response/ErrorCode.java @@ -25,6 +25,7 @@ public enum ErrorCode { //403 Forbidden + FORBIDDEN("A40300", HttpStatus.FORBIDDEN, "접근이 거부되었습니다."), // 404 Not Found NOT_FOUND_END_POINT("A40400", HttpStatus.NOT_FOUND, "존재하지 않는 API입니다."), diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/security/CustomAccessDeniedHandler.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/security/CustomAccessDeniedHandler.java index 9fac574a..0150cd6b 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/security/CustomAccessDeniedHandler.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/security/CustomAccessDeniedHandler.java @@ -1,26 +1,42 @@ package com.jootalkpia.auth_server.security; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; +import com.jootalkpia.auth_server.response.ApiResponseDto; +import com.jootalkpia.auth_server.response.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.core.io.buffer.DataBuffer; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; @Component -public class CustomAccessDeniedHandler implements AccessDeniedHandler { +@RequiredArgsConstructor +public class CustomAccessDeniedHandler implements ServerAccessDeniedHandler { + + private final ObjectMapper objectMapper; @Override - public void handle(HttpServletRequest request, HttpServletResponse response, - AccessDeniedException accessDeniedException) throws IOException, ServletException { - setResponse(response); - } + public Mono handle(ServerWebExchange exchange, AccessDeniedException denied) { + exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); + exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + + ApiResponseDto errorResponse = ApiResponseDto.fail(ErrorCode.FORBIDDEN); + + byte[] responseBytes; + try { + responseBytes = objectMapper.writeValueAsBytes(errorResponse); + } catch (JsonProcessingException e) { + responseBytes = "{\"message\":\"Access Denied\"}".getBytes(StandardCharsets.UTF_8); + } - private void setResponse(HttpServletResponse response) throws IOException { - response.setStatus(HttpServletResponse.SC_FORBIDDEN); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); + DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(responseBytes); + return exchange.getResponse().writeWith(Mono.just(buffer)); } } diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/security/CustomJwtAuthenticationEntryPoint.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/security/CustomJwtAuthenticationEntryPoint.java index 39efd221..d9dff696 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/security/CustomJwtAuthenticationEntryPoint.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/security/CustomJwtAuthenticationEntryPoint.java @@ -1,21 +1,18 @@ package com.jootalkpia.auth_server.security; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.server.ServerAuthenticationEntryPoint; import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; @Component -public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint { - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) { - setResponse(response); - } +public class CustomJwtAuthenticationEntryPoint implements ServerAuthenticationEntryPoint { - private void setResponse(HttpServletResponse response) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + @Override + public Mono commence(ServerWebExchange exchange, AuthenticationException ex) { + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + return exchange.getResponse().setComplete(); } - } diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/AuthController.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/AuthController.java index 8b06de82..031f98e9 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/AuthController.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/AuthController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; @RestController @RequiredArgsConstructor @@ -15,10 +16,12 @@ public class AuthController { private final JwtTokenProvider jwtTokenProvider; private final PassportGenerator passportGenerator; - @PostMapping("/auth/validate") - public Passport validateJwt(@RequestHeader("Authorization") String token) { - Long userId = jwtTokenProvider.getUserFromJwt(token.replace("Bearer ", "")); - return passportGenerator.generatePassport(userId); + public Mono validateJwt(@RequestHeader("Authorization") String token) { + return Mono.fromCallable(() -> { + String jwt = token.replace("Bearer ", ""); + Long userId = jwtTokenProvider.getUserFromJwt(jwt); + return passportGenerator.generatePassport(userId); + }); } -} +} \ No newline at end of file diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/UserController.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/UserController.java index 06f4faef..bb513f39 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/UserController.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/UserController.java @@ -18,50 +18,52 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; @Slf4j @RestController @RequiredArgsConstructor -public class UserController implements UserControllerDocs { +public class UserController implements UserControllerDocs { private final UserService userService; @GetMapping("/api/v1/user/me") - public ResponseEntity getUserInfo(@CurrentUser UserInfo userInfo) { + public Mono> getUserInfo(@CurrentUser UserInfo userInfo) { log.info("📢 [Controller] Received UserInfo: {}", userInfo); - if (userInfo == null) { log.error("[Controller] UserInfo is NULL!"); - return ResponseEntity.badRequest().build(); + return Mono.just(ResponseEntity.badRequest().build()); } - - return ResponseEntity.ok().body(userInfo); + return Mono.just(ResponseEntity.ok(userInfo)); } @Override @PostMapping("api/v1/user/login") - public ResponseEntity login( + public Mono> login( @RequestParam final String authorizationCode, @RequestBody final UserLoginRequest loginRequest ) { - return ResponseEntity.ok().body(userService.create(authorizationCode, loginRequest)); + return userService.create(authorizationCode, loginRequest) + .map(ResponseEntity::ok); } @Override @GetMapping("api/v1/user/token-refresh") - public ResponseEntity refreshToken( + public Mono> refreshToken( @RequestParam final String token ) { - return ResponseEntity.ok().body(userService.refreshToken(token)); + return userService.refreshToken(token) + .map(ResponseEntity::ok); } @Override @PatchMapping("api/v1/user/profile") - public ResponseEntity updateNickname ( + public Mono> updateNickname ( @RequestBody final UpdateNicknameRequest request, Principal principal ) { - Long userId = Long.valueOf(principal.getName());//JootalkpiaAuthenticationContext.getUserInfo().userId(); - return ResponseEntity.ok().body(userService.updateNickname(request.nickname(), userId)); + Long userId = Long.valueOf(principal.getName()); + return userService.updateNickname(request.nickname(), userId) + .map(ResponseEntity::ok); } } diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/UserControllerDocs.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/UserControllerDocs.java index dc34eb17..33436428 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/UserControllerDocs.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/controller/UserControllerDocs.java @@ -14,6 +14,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; +import reactor.core.publisher.Mono; @Tag(name = "User", description = "User 관련 API") public interface UserControllerDocs { @@ -30,7 +31,7 @@ public interface UserControllerDocs { @ApiResponse(responseCode = "A40104", description = "해당 유저의 리프레시 토큰이 존재하지 않습니다.") } ) - ResponseEntity login( + Mono> login( @RequestParam final String authorizationCode, @RequestBody final UserLoginRequest loginRequest ); @@ -44,7 +45,7 @@ ResponseEntity login( @ApiResponse(responseCode = "A50000", description = "서버 내부 오류입니다.") } ) - ResponseEntity refreshToken( + Mono> refreshToken( @RequestParam final String refreshToken ); @@ -55,7 +56,7 @@ ResponseEntity refreshToken( @ApiResponse(responseCode = "400", description = "이미 존재하는 닉네임입니다."), } ) - ResponseEntity updateNickname ( + Mono> updateNickname ( @RequestBody final UpdateNicknameRequest request, Principal principal ); diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/BaseTimeEntity.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/BaseTimeEntity.java index 7d499e63..cfec900a 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/BaseTimeEntity.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/BaseTimeEntity.java @@ -1,21 +1,25 @@ package com.jootalkpia.auth_server.user.domain; -import jakarta.persistence.EntityListeners; -import jakarta.persistence.MappedSuperclass; -import java.time.LocalDateTime; import lombok.Getter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.time.LocalDateTime; @Getter -@MappedSuperclass -@EntityListeners(AuditingEntityListener.class) public abstract class BaseTimeEntity { @CreatedDate private LocalDateTime createdAt; @LastModifiedDate - protected LocalDateTime updatedAt; + private LocalDateTime updatedAt; + + public void markCreatedNow() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + public void markUpdatedNow() { + this.updatedAt = LocalDateTime.now(); + } } diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/User.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/User.java index 66b3a23b..ad7fc87e 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/User.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/domain/User.java @@ -1,42 +1,35 @@ package com.jootalkpia.auth_server.user.domain; +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Table; +import org.springframework.data.relational.core.mapping.Column; +import java.time.LocalDateTime; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import jakarta.persistence.Table; -import jakarta.persistence.Id; - -@Entity @Getter @Table(name = "users") -@Builder @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder public class User extends BaseTimeEntity { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) private Long userId; private Long socialId; private String email; - @Enumerated(EnumType.STRING) + @Column("platform") private Platform platform; private String nickname; private String profileImage; + private LocalDateTime createdAt; + + private LocalDateTime updatedAt; public static User of( final Long socialId, @@ -51,10 +44,13 @@ public static User of( .platform(platform) .nickname(socialNickname) .profileImage(profileImage) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) .build(); } public void updateNickname(final String newNickname) { this.nickname = newNickname; + this.updatedAt = LocalDateTime.now(); } } diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/repository/UserRepository.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/repository/UserRepository.java index ff4d33a5..33a146e0 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/repository/UserRepository.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/repository/UserRepository.java @@ -4,23 +4,14 @@ import com.jootalkpia.auth_server.response.ErrorCode; import com.jootalkpia.auth_server.user.domain.Platform; import com.jootalkpia.auth_server.user.domain.User; -import feign.Param; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import reactor.core.publisher.Mono; -public interface UserRepository extends JpaRepository, UserRepositoryCustom { +public interface UserRepository extends ReactiveCrudRepository { - @Query("SELECT u FROM User u WHERE u.socialId = :socialId AND u.platform = :platform") - Optional findUserByPlatformAndSocialId(@Param("socialId") Long socialId, - @Param("platform") Platform platform); + Mono findByUserId(Long userId); // JPA가 아님 → 직접 커스텀 쿼리 필요 시 @Query 필요 - Optional findByUserId(Long userId); + Mono existsByNickname(String nickname); - default User findByUserIdOrThrow(Long id) { - return findByUserId(id) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - } - - boolean existsByNickname(String nickname); -} \ No newline at end of file + Mono findBySocialIdAndPlatform(Long socialId, Platform platform); +} diff --git a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/service/UserService.java b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/service/UserService.java index 18c13242..edd2f29b 100644 --- a/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/service/UserService.java +++ b/src/backend/auth_server/src/main/java/com/jootalkpia/auth_server/user/service/UserService.java @@ -1,16 +1,14 @@ package com.jootalkpia.auth_server.user.service; -import static com.jootalkpia.auth_server.jwt.JwtValidationType.EXPIRED_JWT_TOKEN; - import com.jootalkpia.auth_server.client.dto.UserInfoResponse; import com.jootalkpia.auth_server.client.dto.UserLoginRequest; import com.jootalkpia.auth_server.client.kakao.KakaoSocialService; import com.jootalkpia.auth_server.exception.CustomException; import com.jootalkpia.auth_server.jwt.JwtTokenProvider; +import com.jootalkpia.auth_server.jwt.JwtValidationType; import com.jootalkpia.auth_server.jwt.TokenService; import com.jootalkpia.auth_server.response.ErrorCode; import com.jootalkpia.auth_server.security.UserAuthentication; -import com.jootalkpia.auth_server.user.domain.Platform; import com.jootalkpia.auth_server.user.domain.User; import com.jootalkpia.auth_server.user.dto.response.GetAccessTokenResponse; import com.jootalkpia.auth_server.user.dto.response.LoginResponse; @@ -18,10 +16,10 @@ import com.jootalkpia.auth_server.user.dto.response.UpdateNicknameResponse; import com.jootalkpia.auth_server.user.dto.response.UserDto; import com.jootalkpia.auth_server.user.repository.UserRepository; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; @Service @RequiredArgsConstructor @@ -34,123 +32,98 @@ public class UserService { @Value("${default_image}") private String defaultImage; - public LoginResponse create( - final String authorizationCode, - final UserLoginRequest loginRequest - ) { - User user = getUser(getUserInfoResponse(authorizationCode, loginRequest)); - UserDto userDto = UserDto.of(user.getUserId(), user.getNickname(),user.getProfileImage()); - TokenDto tokenDto = getTokenDto(user); - - return LoginResponse.of(userDto, tokenDto); + public Mono create(String authorizationCode, UserLoginRequest loginRequest) { + return getUserInfoResponse(authorizationCode, loginRequest) // 이미 Mono 반환 + .flatMap(this::getOrCreateUser) + .flatMap(user -> getTokenDto(user) + .map(tokenDto -> { + UserDto userDto = UserDto.of(user.getUserId(), user.getNickname(), user.getProfileImage()); + return LoginResponse.of(userDto, tokenDto); + })); } - public UserInfoResponse getUserInfoResponse( - final String authorizationCode, - final UserLoginRequest loginRequest - ) { - switch (loginRequest.platform()) { - case KAKAO: - return kakaoSocialService.login(authorizationCode, loginRequest); - default: - throw new CustomException(ErrorCode.PLATFORM_BAD_REQUEST); - } + private Mono getOrCreateUser(UserInfoResponse userInfo) { + return userRepository.findBySocialIdAndPlatform(userInfo.socialId(), userInfo.platform()) + .switchIfEmpty(createUser(userInfo)); } - public User createUser(final UserInfoResponse userResponse) { - User user = User.of( - userResponse.socialId(), - userResponse.email(), - userResponse.platform(), - userResponse.socialNickname()+"#"+userResponse.socialId(), + private Mono createUser(UserInfoResponse userInfo) { + User newUser = User.of( + userInfo.socialId(), + userInfo.email(), + userInfo.platform(), + userInfo.socialNickname() + "#" + userInfo.socialId(), defaultImage ); - return userRepository.save(user); - } - - public User getBySocialId( - final Long socialId, - final Platform platform - ) { - User user = userRepository.findUserByPlatformAndSocialId(socialId, platform).orElseThrow( - () -> new CustomException(ErrorCode.USER_NOT_FOUND) - ); - return user; - } - - public TokenDto getTokenByUserId( - final Long id - ) { - // 사용자 정보 가져오기 - userRepository.findByUserIdOrThrow(id); - - UserAuthentication userAuthentication = new UserAuthentication(id.toString(), null, null); - - String refreshToken = jwtTokenProvider.issueRefreshToken(userAuthentication); - tokenService.saveRefreshToken(id, refreshToken); - return TokenDto.of( - jwtTokenProvider.issueAccessToken(userAuthentication), - refreshToken - ); + return userRepository.save(newUser); } - public GetAccessTokenResponse refreshToken( - final String refreshToken - ) { - if (jwtTokenProvider.validateToken(refreshToken) == EXPIRED_JWT_TOKEN) { - // 리프레시 토큰이 만료된 경우 - throw new CustomException(ErrorCode.REFRESH_TOKEN_EXPIRED); + public Mono refreshToken(String refreshToken) { + if (!isValidJwt(jwtTokenProvider.validateToken(refreshToken))) { + return Mono.error(new CustomException(ErrorCode.REFRESH_TOKEN_EXPIRED)); } Long userId = jwtTokenProvider.getUserFromJwt(refreshToken); - if (!userId.equals(tokenService.findIdByRefreshToken(refreshToken))) { - throw new CustomException(ErrorCode.TOKEN_INCORRECT_ERROR); - } - - // 사용자 정보 가져오기 - userRepository.findByUserIdOrThrow(userId); - - UserAuthentication userAuthentication = new UserAuthentication(userId.toString(),null, null); - return GetAccessTokenResponse.of( - jwtTokenProvider.issueAccessToken(userAuthentication) - ); + return tokenService.findIdByRefreshToken(refreshToken) + .flatMap(idFromToken -> { + if (!userId.equals(idFromToken)) { + return Mono.error(new CustomException(ErrorCode.TOKEN_INCORRECT_ERROR)); + } + + UserAuthentication auth = new UserAuthentication(userId.toString(), null, null); + return userRepository.findByUserId(userId) + .switchIfEmpty(Mono.error(new CustomException(ErrorCode.USER_NOT_FOUND))) + .map(user -> GetAccessTokenResponse.of(jwtTokenProvider.issueAccessToken(auth))); + }); } - @Transactional - public UpdateNicknameResponse updateNickname( - String nickname, - Long userId - ) { - User user = userRepository.findByUserIdOrThrow(userId); - - if (userRepository.existsByNickname(nickname)) { - throw new CustomException(ErrorCode.DUPLICATION_NICKNAME); - } + public boolean isValidJwt(JwtValidationType type) { + return type == JwtValidationType.VALID_JWT; + } - user.updateNickname(nickname); + public Mono updateNickname(String nickname, Long userId) { + return userRepository.findByUserId(userId) + .switchIfEmpty(Mono.error(new CustomException(ErrorCode.USER_NOT_FOUND))) + .flatMap(user -> userRepository.existsByNickname(nickname) + .flatMap(exists -> { + if (Boolean.TRUE.equals(exists)) { + return Mono.error(new CustomException(ErrorCode.DUPLICATION_NICKNAME)); + } + user.updateNickname(nickname); + return userRepository.save(user) + .map(updated -> UpdateNicknameResponse.of( + updated.getUserId(), + updated.getNickname(), + updated.getProfileImage() + )); + })); + } - return UpdateNicknameResponse.of(user.getUserId(),user.getNickname(),user.getProfileImage()); + public Mono getTokenByUserId(Long userId) { + return userRepository.findByUserId(userId) + .switchIfEmpty(Mono.error(new CustomException(ErrorCode.USER_NOT_FOUND))) + .flatMap(user -> { + UserAuthentication auth = new UserAuthentication(userId.toString(), null, null); + String refreshToken = jwtTokenProvider.issueRefreshToken(auth); + tokenService.saveRefreshToken(userId, refreshToken); + + return Mono.just(TokenDto.of( + jwtTokenProvider.issueAccessToken(auth), + refreshToken + )); + }); } - private TokenDto getTokenDto( - final User user - ) { + private Mono getTokenDto(User user) { return getTokenByUserId(user.getUserId()); } - private User getUser(final UserInfoResponse userResponse) { - if (isExistingUser(userResponse.socialId(), userResponse.platform())) { - return getBySocialId(userResponse.socialId(), userResponse.platform()); - } else { - return createUser(userResponse); - } + public Mono getUserInfoResponse(String authorizationCode, UserLoginRequest loginRequest) { + return switch (loginRequest.platform()) { + case KAKAO -> kakaoSocialService.login(authorizationCode, loginRequest); + default -> Mono.error(new CustomException(ErrorCode.PLATFORM_BAD_REQUEST)); + }; } - private boolean isExistingUser( - final Long socialId, - final Platform platform - ) { - return userRepository.findUserByPlatformAndSocialId(socialId, platform).isPresent(); - } -} \ No newline at end of file +} diff --git a/src/backend/auth_server/src/main/resources/application.yml b/src/backend/auth_server/src/main/resources/application.yml index e535f8c9..51b367e7 100644 --- a/src/backend/auth_server/src/main/resources/application.yml +++ b/src/backend/auth_server/src/main/resources/application.yml @@ -1,27 +1,19 @@ server: - port: ${AUTH_PORT} + port: ${AUTH_PORT} spring: - data: - redis: - host: ${REDIS_HOST} - port: ${REDIS_PORT} - + main: + web-application-type: reactive - datasource: - driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME} + r2dbc: + url: r2dbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME} username: ${DB_USER} password: ${DB_PASSWORD} - jpa: - show-sql: true - hibernate: - ddl-auto: none - properties: - hibernate: - format_sql: true - show_sql: true + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} jwt: secret: ${JWT_SECRET} @@ -30,3 +22,7 @@ kakao: clientId: ${KAKAO_CLIENTID} default_image: ${DEFAULT_IMAGE_S3} + +springdoc: + swagger-ui: + path: /swagger-ui.html diff --git a/src/backend/auth_server/src/test/java/com/jootalkpia/auth_server/AuthServerApplicationTests.java b/src/backend/auth_server/src/test/java/com/jootalkpia/auth_server/AuthServerApplicationTests.java deleted file mode 100644 index 9203e396..00000000 --- a/src/backend/auth_server/src/test/java/com/jootalkpia/auth_server/AuthServerApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.jootalkpia.auth_server; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class AuthServerApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/backend/common_module/build.gradle b/src/backend/common_module/build.gradle index b020c7a9..90587784 100644 --- a/src/backend/common_module/build.gradle +++ b/src/backend/common_module/build.gradle @@ -27,7 +27,7 @@ dependencies { compileOnly 'jakarta.annotation:jakarta.annotation-api:1.3.5' - implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-webflux' //mono implementation 'io.projectreactor:reactor-core' diff --git a/src/backend/common_module/src/main/java/com/jootalkpia/config/WebFluxConfig.java b/src/backend/common_module/src/main/java/com/jootalkpia/config/WebFluxConfig.java new file mode 100644 index 00000000..ed8e4770 --- /dev/null +++ b/src/backend/common_module/src/main/java/com/jootalkpia/config/WebFluxConfig.java @@ -0,0 +1,21 @@ +package com.jootalkpia.config; + +import com.jootalkpia.passport.anotation.CurrentUserArgumentResolver; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.config.WebFluxConfigurer; +import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; + +@Configuration +public class WebFluxConfig implements WebFluxConfigurer { + + private final CurrentUserArgumentResolver currentUserArgumentResolver; + + public WebFluxConfig(CurrentUserArgumentResolver currentUserArgumentResolver) { + this.currentUserArgumentResolver = currentUserArgumentResolver; + } + + @Override + public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { + configurer.addCustomResolver(currentUserArgumentResolver); + } +} diff --git a/src/backend/common_module/src/main/java/com/jootalkpia/config/WebMvcConfig.java b/src/backend/common_module/src/main/java/com/jootalkpia/config/WebMvcConfig.java deleted file mode 100644 index 8c8cd9e6..00000000 --- a/src/backend/common_module/src/main/java/com/jootalkpia/config/WebMvcConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.jootalkpia.config; - -import com.jootalkpia.passport.anotation.CurrentUserArgumentResolver; -import java.util.List; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class WebMvcConfig implements WebMvcConfigurer { - - private final CurrentUserArgumentResolver currentUserArgumentResolver; - - public WebMvcConfig(CurrentUserArgumentResolver currentUserArgumentResolver) { - this.currentUserArgumentResolver = currentUserArgumentResolver; - } - - @Override - public void addArgumentResolvers(List resolvers) { - resolvers.add(currentUserArgumentResolver); - } -} diff --git a/src/backend/common_module/src/main/java/com/jootalkpia/passport/anotation/CurrentUserArgumentResolver.java b/src/backend/common_module/src/main/java/com/jootalkpia/passport/anotation/CurrentUserArgumentResolver.java index e3891f3a..4187ffa2 100644 --- a/src/backend/common_module/src/main/java/com/jootalkpia/passport/anotation/CurrentUserArgumentResolver.java +++ b/src/backend/common_module/src/main/java/com/jootalkpia/passport/anotation/CurrentUserArgumentResolver.java @@ -2,19 +2,17 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.jootalkpia.passport.component.UserInfo; -import lombok.extern.slf4j.Slf4j; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.reactive.BindingContext; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; -@Slf4j @Component public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver { - private final ObjectMapper objectMapper; // JSON 파싱을 위한 ObjectMapper 추가 + private final ObjectMapper objectMapper; public CurrentUserArgumentResolver(ObjectMapper objectMapper) { this.objectMapper = objectMapper; @@ -22,27 +20,26 @@ public CurrentUserArgumentResolver(ObjectMapper objectMapper) { @Override public boolean supportsParameter(MethodParameter parameter) { - return parameter.getParameterAnnotation(CurrentUser.class) != null && - parameter.getParameterType().equals(UserInfo.class); + return parameter.hasParameterAnnotation(CurrentUser.class) + && parameter.getParameterType().equals(UserInfo.class); } @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { - String userJson = webRequest.getHeader("X-Passport-User"); + public Mono resolveArgument(MethodParameter parameter, + BindingContext bindingContext, + ServerWebExchange exchange) { + + String userJson = exchange.getRequest().getHeaders().getFirst("X-Passport-User"); if (userJson == null) { - throw new RuntimeException("Missing X-Passport-User header"); + return Mono.empty(); } try { - ObjectMapper objectMapper = new ObjectMapper(); UserInfo userInfo = objectMapper.readValue(userJson, UserInfo.class); - return userInfo; + return Mono.just(userInfo); } catch (Exception e) { - throw new RuntimeException("Invalid X-Passport-User header format", e); + return Mono.error(new RuntimeException("Invalid X-Passport-User header", e)); } } - - } diff --git a/src/backend/stock_server/src/main/java/com/jootalkpia/stock_server/support/config/WebConfig.java b/src/backend/stock_server/src/main/java/com/jootalkpia/stock_server/support/config/WebConfig.java index 71dc0f3b..88d8152a 100644 --- a/src/backend/stock_server/src/main/java/com/jootalkpia/stock_server/support/config/WebConfig.java +++ b/src/backend/stock_server/src/main/java/com/jootalkpia/stock_server/support/config/WebConfig.java @@ -13,4 +13,4 @@ public void addCorsMappings(CorsRegistry registry) { .allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS") .allowedHeaders("*"); } -} +} \ No newline at end of file