From 3ba065af46ba43ddb2b7a396c8370a6c55481f92 Mon Sep 17 00:00:00 2001 From: Shjung Date: Fri, 29 Mar 2024 23:19:17 +0900 Subject: [PATCH 1/4] =?UTF-8?q?:sparkles:=20feat=20:=20=20custom=20annotat?= =?UTF-8?q?ion=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/overcomingroom/bellbell/resolver/Login.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/com/overcomingroom/bellbell/resolver/Login.java diff --git a/src/main/java/com/overcomingroom/bellbell/resolver/Login.java b/src/main/java/com/overcomingroom/bellbell/resolver/Login.java new file mode 100644 index 0000000..482030b --- /dev/null +++ b/src/main/java/com/overcomingroom/bellbell/resolver/Login.java @@ -0,0 +1,11 @@ +package com.overcomingroom.bellbell.resolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Login { +} From 7ccf7a35db7d11404d76e43793c6bb473f8a130e Mon Sep 17 00:00:00 2001 From: Shjung Date: Fri, 29 Mar 2024 23:20:29 +0900 Subject: [PATCH 2/4] =?UTF-8?q?:sparkles:=20feat=20:=20resolver=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EA=B7=B8=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20Interceptor=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interceptor/AuthorizationInterceptor.java | 38 +++++++++++++++---- .../bellbell/resolver/LoginUser.java | 15 ++++++++ .../bellbell/resolver/LoginUserContext.java | 23 +++++++++++ .../bellbell/resolver/LoginUserResolver.java | 28 ++++++++++++++ 4 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/overcomingroom/bellbell/resolver/LoginUser.java create mode 100644 src/main/java/com/overcomingroom/bellbell/resolver/LoginUserContext.java create mode 100644 src/main/java/com/overcomingroom/bellbell/resolver/LoginUserResolver.java diff --git a/src/main/java/com/overcomingroom/bellbell/interceptor/AuthorizationInterceptor.java b/src/main/java/com/overcomingroom/bellbell/interceptor/AuthorizationInterceptor.java index 22b28a7..9cd2b2d 100644 --- a/src/main/java/com/overcomingroom/bellbell/interceptor/AuthorizationInterceptor.java +++ b/src/main/java/com/overcomingroom/bellbell/interceptor/AuthorizationInterceptor.java @@ -1,31 +1,55 @@ package com.overcomingroom.bellbell.interceptor; +import com.overcomingroom.bellbell.exception.CustomException; +import com.overcomingroom.bellbell.exception.ErrorCode; +import com.overcomingroom.bellbell.resolver.LoginUserContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @Component +@Slf4j @RequiredArgsConstructor public class AuthorizationInterceptor implements HandlerInterceptor { - private static final ThreadLocal tokenHolder = new ThreadLocal<>(); + private final LoginUserContext loginUserContext; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 헤더에서 토큰 추출 - String token = extractToken(request); - tokenHolder.set(token); - return true; - } + String accessToken = extractToken(request); + + log.info("interceptor accessToken = {}", accessToken); + // 토큰 검증 + if (accessToken == null) { + throw new CustomException(ErrorCode.JWT_VALUE_IS_EMPTY); + } + + // 로그인 된 유저 토큰 저장 + registerLoginUser(accessToken); - public static String getAccessToken() { - return tokenHolder.get(); + return HandlerInterceptor.super.preHandle(request, response, handler); } private String extractToken(HttpServletRequest request) { return request.getHeader("Authorization"); } + + public void registerLoginUser(String accessToken) { + loginUserContext.save(accessToken); + } + + public void releaseLoginUser() { + loginUserContext.remove(); + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + releaseLoginUser(); + HandlerInterceptor.super.afterCompletion(request, response, handler, ex); + } } diff --git a/src/main/java/com/overcomingroom/bellbell/resolver/LoginUser.java b/src/main/java/com/overcomingroom/bellbell/resolver/LoginUser.java new file mode 100644 index 0000000..2659436 --- /dev/null +++ b/src/main/java/com/overcomingroom/bellbell/resolver/LoginUser.java @@ -0,0 +1,15 @@ +package com.overcomingroom.bellbell.resolver; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LoginUser { + + private String accessToken; + + public static LoginUser of(String accessToken) { + return new LoginUser(accessToken.substring(7)); + } +} diff --git a/src/main/java/com/overcomingroom/bellbell/resolver/LoginUserContext.java b/src/main/java/com/overcomingroom/bellbell/resolver/LoginUserContext.java new file mode 100644 index 0000000..4eec493 --- /dev/null +++ b/src/main/java/com/overcomingroom/bellbell/resolver/LoginUserContext.java @@ -0,0 +1,23 @@ +package com.overcomingroom.bellbell.resolver; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class LoginUserContext { + private static final ThreadLocal loginUserContext = new ThreadLocal<>(); + + public LoginUser getLoginUser() { + return loginUserContext.get(); + } + + public void save(String accessToken){ + loginUserContext.set(LoginUser.of(accessToken)); + } + + public void remove(){ + loginUserContext.remove(); + } + +} diff --git a/src/main/java/com/overcomingroom/bellbell/resolver/LoginUserResolver.java b/src/main/java/com/overcomingroom/bellbell/resolver/LoginUserResolver.java new file mode 100644 index 0000000..f911c4f --- /dev/null +++ b/src/main/java/com/overcomingroom/bellbell/resolver/LoginUserResolver.java @@ -0,0 +1,28 @@ +package com.overcomingroom.bellbell.resolver; + +import lombok.RequiredArgsConstructor; +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; + +@Component +@RequiredArgsConstructor +public class LoginUserResolver implements HandlerMethodArgumentResolver { + + private final LoginUserContext loginUserContext; + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Login.class) && parameter.getParameterType().equals(LoginUser.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + return loginUserContext.getLoginUser(); + } +} From 6893b1fca6cd39d1ffb7fb4af5bf379b898265f8 Mon Sep 17 00:00:00 2001 From: Shjung Date: Fri, 29 Mar 2024 23:20:48 +0900 Subject: [PATCH 3/4] =?UTF-8?q?:sparkles:=20feat=20:=20resolver=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bellbell/config/CorsConfig.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/overcomingroom/bellbell/config/CorsConfig.java b/src/main/java/com/overcomingroom/bellbell/config/CorsConfig.java index c82b30e..c614147 100644 --- a/src/main/java/com/overcomingroom/bellbell/config/CorsConfig.java +++ b/src/main/java/com/overcomingroom/bellbell/config/CorsConfig.java @@ -1,12 +1,16 @@ package com.overcomingroom.bellbell.config; import com.overcomingroom.bellbell.interceptor.AuthorizationInterceptor; +import com.overcomingroom.bellbell.resolver.LoginUserResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.List; + /** * CORS(Cross-Origin Resource Sharing) 설정을 구성하는 클래스입니다. */ @@ -15,6 +19,7 @@ public class CorsConfig implements WebMvcConfigurer { private final AuthorizationInterceptor authorizationInterceptor; + private final LoginUserResolver loginUserResolver; /** * CORS(Cross-Origin Resource Sharing) 설정을 추가합니다. @@ -33,6 +38,14 @@ public void addCorsMappings(CorsRegistry registry) { @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(this.authorizationInterceptor); + registry.addInterceptor(this.authorizationInterceptor) + .addPathPatterns("/**") // 모든 요청은 인터셉터를 거치도록 함. + .excludePathPatterns("/v1/login") // 로그인 시 AccessToken이 없기 때문에 해당 요청만 제외 + .excludePathPatterns("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html"); // 스웨거 예외 처리 + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(loginUserResolver); } } From 7be8f459bce80fb5f86e9929f334aa52a7daeac6 Mon Sep 17 00:00:00 2001 From: Shjung Date: Fri, 29 Mar 2024 23:21:47 +0900 Subject: [PATCH 4/4] =?UTF-8?q?:recycle:=20refactor=20:=20custom=20annotat?= =?UTF-8?q?ion=20=EC=9D=84=20=EC=9D=B4=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20controller=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/controller/MemberController.java | 48 +++--- .../UserNotificationController.java | 140 +++++++++--------- .../weather/controller/WeatherController.java | 42 ++---- 3 files changed, 112 insertions(+), 118 deletions(-) diff --git a/src/main/java/com/overcomingroom/bellbell/member/domain/controller/MemberController.java b/src/main/java/com/overcomingroom/bellbell/member/domain/controller/MemberController.java index 5341645..f0f4f29 100644 --- a/src/main/java/com/overcomingroom/bellbell/member/domain/controller/MemberController.java +++ b/src/main/java/com/overcomingroom/bellbell/member/domain/controller/MemberController.java @@ -1,12 +1,15 @@ package com.overcomingroom.bellbell.member.domain.controller; import com.overcomingroom.bellbell.member.domain.service.MemberService; +import com.overcomingroom.bellbell.resolver.Login; +import com.overcomingroom.bellbell.resolver.LoginUser; import com.overcomingroom.bellbell.response.ResResult; import com.overcomingroom.bellbell.response.ResponseCode; +import io.swagger.v3.oas.annotations.Parameter; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; /** @@ -14,28 +17,29 @@ */ @RestController @RequiredArgsConstructor +@Slf4j public class MemberController { - private final MemberService memberService; + private final MemberService memberService; - /** - * 멤버 정보를 반환하는 메서드입니다. - * - * @param accessToken 클라이언트를 통해 전달받은 액세스 토큰 - * @return 멤버 정보 - */ - @GetMapping("/v1/member") - public ResponseEntity getUserInfo( - @RequestHeader("Authorization") String accessToken - ) { - ResponseCode responseCode = ResponseCode.MEMBER_INFO_GET_SUCCESSFUL; - return ResponseEntity.ok( - ResResult.builder() - .responseCode(responseCode) - .code(responseCode.getCode()) - .message(responseCode.getMessage()) - .data(memberService.getMemberInfo(accessToken.substring(7))) - .build() - ); - } + /** + * 멤버 정보를 반환하는 메서드입니다. + * + * @param accessToken 클라이언트를 통해 전달받은 액세스 토큰 + * @return 멤버 정보 + */ + @GetMapping("/v1/member") + public ResponseEntity getUserInfo(@Parameter(hidden = true) @Login LoginUser loginUser) { + + ResponseCode responseCode = ResponseCode.MEMBER_INFO_GET_SUCCESSFUL; + + return ResponseEntity.ok( + ResResult.builder() + .responseCode(responseCode) + .code(responseCode.getCode()) + .message(responseCode.getMessage()) + .data(memberService.getMemberInfo(loginUser.getAccessToken())) + .build() + ); + } } \ No newline at end of file diff --git a/src/main/java/com/overcomingroom/bellbell/usernotification/controller/UserNotificationController.java b/src/main/java/com/overcomingroom/bellbell/usernotification/controller/UserNotificationController.java index dc264de..ad3e92d 100644 --- a/src/main/java/com/overcomingroom/bellbell/usernotification/controller/UserNotificationController.java +++ b/src/main/java/com/overcomingroom/bellbell/usernotification/controller/UserNotificationController.java @@ -1,88 +1,90 @@ package com.overcomingroom.bellbell.usernotification.controller; +import com.overcomingroom.bellbell.resolver.Login; +import com.overcomingroom.bellbell.resolver.LoginUser; import com.overcomingroom.bellbell.response.ResResult; import com.overcomingroom.bellbell.response.ResponseCode; import com.overcomingroom.bellbell.usernotification.domain.dto.UserNotificationRequestDto; import com.overcomingroom.bellbell.usernotification.service.UserNotificationService; +import io.swagger.v3.oas.annotations.Parameter; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @RequestMapping("/v1/notifications") public class UserNotificationController { - private final UserNotificationService userNotificationService; + private final UserNotificationService userNotificationService; - /** - * 사용자 알림 생성 요청을 처리합니다. - * - * @param accessToken 사용자의 액세스 토큰 - * @param dto 요청 데이터 전송 객체 - * @return ResponseEntity 객체 - */ - @PostMapping("/create") - public ResponseEntity createUserNotification( - @RequestHeader("Authorization") String accessToken, - final @RequestBody @Valid UserNotificationRequestDto dto) { - ResponseCode responseCode = userNotificationService.createUserNotification( - accessToken.substring(7), dto); - return ResponseEntity.ok( - ResResult.builder() - .responseCode(responseCode) - .code(responseCode.getCode()) - .message(responseCode.getMessage()) - .build() - ); - } + /** + * 사용자 알림 생성 요청을 처리합니다. + * + * @param accessToken 사용자의 액세스 토큰 + * @param dto 요청 데이터 전송 객체 + * @return ResponseEntity 객체 + */ + @PostMapping("/create") + public ResponseEntity createUserNotification( + @Parameter(hidden = true) @Login LoginUser loginUser, + final @RequestBody @Valid UserNotificationRequestDto dto) { - /** - * 사용자의 알림 목록을 반환합니다. - * - * @param accessToken 사용자의 액세스 토큰 - * @return ResponseEntity 객체 - */ - @GetMapping - public ResponseEntity getUserNotifications( - @RequestHeader("Authorization") String accessToken) { - ResponseCode responseCode = ResponseCode.USER_NOTIFICATIONS_GET_SUCCESSFUL; - return ResponseEntity.ok( - ResResult.builder() - .responseCode(responseCode) - .code(responseCode.getCode()) - .message(responseCode.getMessage()) - .data(userNotificationService.getUserNotifications(accessToken.substring(7))) - .build() - ); - } + ResponseCode responseCode = userNotificationService.createUserNotification( + loginUser.getAccessToken(), dto); - /** - * 사용자의 특정 알림을 삭제합니다. - * - * @param accessToken 사용자의 액세스 토큰 - * @param notificationId 삭제할 알림의 ID - * @return ResponseEntity 객체 - */ - @DeleteMapping("/delete/{notificationId}") - public ResponseEntity deleteUserNotification( - @RequestHeader("Authorization") String accessToken, @PathVariable Long notificationId) { - ResponseCode responseCode = userNotificationService.deleteUserNotification( - accessToken.substring(7), notificationId); - return ResponseEntity.ok( - ResResult.builder() - .responseCode(responseCode) - .code(responseCode.getCode()) - .message(responseCode.getMessage()) - .build() - ); - } + return ResponseEntity.ok( + ResResult.builder() + .responseCode(responseCode) + .code(responseCode.getCode()) + .message(responseCode.getMessage()) + .build() + ); + } + + /** + * 사용자의 알림 목록을 반환합니다. + * + * @param accessToken 사용자의 액세스 토큰 + * @return ResponseEntity 객체 + */ + @GetMapping + public ResponseEntity getUserNotifications(@Parameter(hidden = true) @Login LoginUser loginUser) { + + ResponseCode responseCode = ResponseCode.USER_NOTIFICATIONS_GET_SUCCESSFUL; + + return ResponseEntity.ok( + ResResult.builder() + .responseCode(responseCode) + .code(responseCode.getCode()) + .message(responseCode.getMessage()) + .data(userNotificationService.getUserNotifications(loginUser.getAccessToken())) + .build() + ); + } + + /** + * 사용자의 특정 알림을 삭제합니다. + * + * @param accessToken 사용자의 액세스 토큰 + * @param notificationId 삭제할 알림의 ID + * @return ResponseEntity 객체 + */ + @DeleteMapping("/delete/{notificationId}") + public ResponseEntity deleteUserNotification( + @Parameter(hidden = true) @Login LoginUser loginUser, + @PathVariable Long notificationId) { + + ResponseCode responseCode = userNotificationService.deleteUserNotification( + loginUser.getAccessToken(), notificationId); + + return ResponseEntity.ok( + ResResult.builder() + .responseCode(responseCode) + .code(responseCode.getCode()) + .message(responseCode.getMessage()) + .build() + ); + } } diff --git a/src/main/java/com/overcomingroom/bellbell/weather/controller/WeatherController.java b/src/main/java/com/overcomingroom/bellbell/weather/controller/WeatherController.java index 8660ccb..cc12fb0 100644 --- a/src/main/java/com/overcomingroom/bellbell/weather/controller/WeatherController.java +++ b/src/main/java/com/overcomingroom/bellbell/weather/controller/WeatherController.java @@ -1,12 +1,12 @@ package com.overcomingroom.bellbell.weather.controller; -import com.overcomingroom.bellbell.exception.CustomException; -import com.overcomingroom.bellbell.exception.ErrorCode; -import com.overcomingroom.bellbell.interceptor.AuthorizationInterceptor; +import com.overcomingroom.bellbell.resolver.Login; +import com.overcomingroom.bellbell.resolver.LoginUser; import com.overcomingroom.bellbell.response.ResResult; import com.overcomingroom.bellbell.response.ResponseCode; import com.overcomingroom.bellbell.weather.domain.dto.WeatherInfoDto; import com.overcomingroom.bellbell.weather.service.WeatherService; +import io.swagger.v3.oas.annotations.Parameter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -21,8 +21,8 @@ public class WeatherController { private final WeatherService weatherService; @GetMapping("/weatherAndClothes") - public ResponseEntity weatherAndClothesInfo() { - String accessToken = AuthorizationInterceptor.getAccessToken(); + public ResponseEntity weatherAndClothesInfo(@Parameter(hidden = true) @Login LoginUser loginUser) { + ResponseCode responseCode = ResponseCode.WEATHER_INFO_GET_SUCCESSFUL; return ResponseEntity.ok( @@ -30,24 +30,17 @@ public ResponseEntity weatherAndClothesInfo() { .responseCode(responseCode) .code(responseCode.getCode()) .message(responseCode.getMessage()) - .data(weatherService.weatherAndClothesInfo(accessToken.substring(7))) + .data(weatherService.weatherAndClothesInfo(loginUser.getAccessToken())) .build()); } @PostMapping("/weather") public ResponseEntity activateWeather( + @Parameter(hidden = true) @Login LoginUser loginUser, @RequestBody WeatherInfoDto weatherInfoDto ) { - String accessToken = AuthorizationInterceptor.getAccessToken(); - - // 토큰이 없는 경우 예외 처리 - if (accessToken == null) { - throw new CustomException(ErrorCode.JWT_VALUE_IS_EMPTY); - } - log.info("토큰 = {}", accessToken); - weatherService.activeWeather(accessToken.substring(7), - weatherInfoDto); + weatherService.activeWeather(loginUser.getAccessToken(), weatherInfoDto); ResponseCode responseCode = ResponseCode.WEATHER_ACTIVATE_SUCCESSFUL; return ResponseEntity.ok( @@ -59,22 +52,17 @@ public ResponseEntity activateWeather( } @GetMapping("/weather") - public ResponseEntity weatherInfo() { - String accessToken = AuthorizationInterceptor.getAccessToken(); - // 토큰이 없는 경우 예외 처리 - if (accessToken == null) { - throw new CustomException(ErrorCode.JWT_VALUE_IS_EMPTY); - } + public ResponseEntity weatherInfo(@Parameter(hidden = true) @Login LoginUser loginUser) { ResponseCode responseCode = ResponseCode.WEATHER_INFO_GET_SUCCESSFUL; return ResponseEntity.ok( - ResResult.builder() - .responseCode(responseCode) - .code(responseCode.getCode()) - .message(responseCode.getMessage()) - .data(weatherService.getWeatherInfo(accessToken.substring(7))) - .build()); + ResResult.builder() + .responseCode(responseCode) + .code(responseCode.getCode()) + .message(responseCode.getMessage()) + .data(weatherService.getWeatherInfo(loginUser.getAccessToken())) + .build()); } }