diff --git a/backend/src/main/java/ddangkong/aop/cookie/DeleteCookie.java b/backend/src/main/java/ddangkong/aop/cookie/DeleteCookie.java new file mode 100644 index 000000000..ca9bc6d40 --- /dev/null +++ b/backend/src/main/java/ddangkong/aop/cookie/DeleteCookie.java @@ -0,0 +1,11 @@ +package ddangkong.aop.cookie; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.CLASS) +public @interface DeleteCookie { +} diff --git a/backend/src/main/java/ddangkong/controller/room/EncryptionUtils.java b/backend/src/main/java/ddangkong/aop/cookie/EncryptionUtils.java similarity index 98% rename from backend/src/main/java/ddangkong/controller/room/EncryptionUtils.java rename to backend/src/main/java/ddangkong/aop/cookie/EncryptionUtils.java index cd78ac033..155ab80a0 100644 --- a/backend/src/main/java/ddangkong/controller/room/EncryptionUtils.java +++ b/backend/src/main/java/ddangkong/aop/cookie/EncryptionUtils.java @@ -1,4 +1,4 @@ -package ddangkong.controller.room; +package ddangkong.aop.cookie; import ddangkong.exception.room.CipherException; import ddangkong.exception.room.InvalidCookieException; diff --git a/backend/src/main/java/ddangkong/aop/cookie/IssueCookie.java b/backend/src/main/java/ddangkong/aop/cookie/IssueCookie.java new file mode 100644 index 000000000..60d201c2d --- /dev/null +++ b/backend/src/main/java/ddangkong/aop/cookie/IssueCookie.java @@ -0,0 +1,11 @@ +package ddangkong.aop.cookie; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.CLASS) +public @interface IssueCookie { +} diff --git a/backend/src/main/java/ddangkong/aop/cookie/MemberId.java b/backend/src/main/java/ddangkong/aop/cookie/MemberId.java new file mode 100644 index 000000000..5b9d6c702 --- /dev/null +++ b/backend/src/main/java/ddangkong/aop/cookie/MemberId.java @@ -0,0 +1,11 @@ +package ddangkong.aop.cookie; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface MemberId { +} diff --git a/backend/src/main/java/ddangkong/aop/cookie/RoomMemberCookieAspect.java b/backend/src/main/java/ddangkong/aop/cookie/RoomMemberCookieAspect.java new file mode 100644 index 000000000..88d88cf9d --- /dev/null +++ b/backend/src/main/java/ddangkong/aop/cookie/RoomMemberCookieAspect.java @@ -0,0 +1,65 @@ +package ddangkong.aop.cookie; + +import ddangkong.facade.room.dto.RoomJoinResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Slf4j +@Aspect +@Component +public class RoomMemberCookieAspect { + + private final RoomMemberCookieEncryptor roomMemberCookieEncryptor; + + public RoomMemberCookieAspect(RoomMemberCookieEncryptor roomMemberCookieEncryptor) { + this.roomMemberCookieEncryptor = roomMemberCookieEncryptor; + } + + @Pointcut("@annotation(ddangkong.aop.cookie.IssueCookie)") + public void issueCookie() { + } + + @Pointcut("@annotation(ddangkong.aop.cookie.DeleteCookie)") + public void deleteCookie() { + } + + @AfterReturning(value = "issueCookie()", returning = "roomJoinResponse") + public void handleIssueCookie(RoomJoinResponse roomJoinResponse) { + HttpServletRequest request = getHttpServletRequest(); + HttpServletResponse response = getHttpServletResponse(); + String origin = request.getHeader(HttpHeaders.ORIGIN); + + ResponseCookie encodedCookie = roomMemberCookieEncryptor.getEncodedCookie(roomJoinResponse.member().memberId(), origin); + response.addHeader(HttpHeaders.SET_COOKIE, encodedCookie.toString()); + } + + @After("deleteCookie()") + public void handleDeleteCookie() { + HttpServletRequest request = getHttpServletRequest(); + HttpServletResponse response = getHttpServletResponse(); + String origin = request.getHeader(HttpHeaders.ORIGIN); + + ResponseCookie deleteCookie = roomMemberCookieEncryptor.deleteCookie(origin); + response.addHeader(HttpHeaders.SET_COOKIE, deleteCookie.toString()); + } + + private HttpServletRequest getHttpServletRequest() { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + return servletRequestAttributes.getRequest(); + } + + private HttpServletResponse getHttpServletResponse() { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + return servletRequestAttributes.getResponse(); + } +} diff --git a/backend/src/main/java/ddangkong/controller/room/RoomMemberCookieEncryptor.java b/backend/src/main/java/ddangkong/aop/cookie/RoomMemberCookieEncryptor.java similarity index 98% rename from backend/src/main/java/ddangkong/controller/room/RoomMemberCookieEncryptor.java rename to backend/src/main/java/ddangkong/aop/cookie/RoomMemberCookieEncryptor.java index f2377e348..25b5692f0 100644 --- a/backend/src/main/java/ddangkong/controller/room/RoomMemberCookieEncryptor.java +++ b/backend/src/main/java/ddangkong/aop/cookie/RoomMemberCookieEncryptor.java @@ -1,4 +1,4 @@ -package ddangkong.controller.room; +package ddangkong.aop.cookie; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.server.Cookie.SameSite; diff --git a/backend/src/main/java/ddangkong/config/CookieConfig.java b/backend/src/main/java/ddangkong/config/CookieConfig.java new file mode 100644 index 000000000..ec6e1adf2 --- /dev/null +++ b/backend/src/main/java/ddangkong/config/CookieConfig.java @@ -0,0 +1,20 @@ +package ddangkong.config; + +import ddangkong.resolver.RoomMemberArgumentResolver; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class CookieConfig implements WebMvcConfigurer { + + private final RoomMemberArgumentResolver roomMemberArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(roomMemberArgumentResolver); + } +} diff --git a/backend/src/main/java/ddangkong/controller/room/RoomController.java b/backend/src/main/java/ddangkong/controller/room/RoomController.java index 905394374..a43f113c9 100644 --- a/backend/src/main/java/ddangkong/controller/room/RoomController.java +++ b/backend/src/main/java/ddangkong/controller/room/RoomController.java @@ -1,5 +1,8 @@ package ddangkong.controller.room; +import ddangkong.aop.cookie.DeleteCookie; +import ddangkong.aop.cookie.IssueCookie; +import ddangkong.aop.cookie.MemberId; import ddangkong.aop.logging.Polling; import ddangkong.facade.room.RoomFacade; import ddangkong.facade.room.dto.InitialRoomResponse; @@ -10,17 +13,12 @@ import ddangkong.facade.room.dto.RoomSettingRequest; import ddangkong.facade.room.dto.RoomStatusResponse; import ddangkong.facade.room.dto.RoundFinishedResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseCookie; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; @@ -39,21 +37,17 @@ public class RoomController { private final RoomFacade roomFacade; - private final RoomMemberCookieEncryptor roomMemberCookieEncryptor; + @IssueCookie @ResponseStatus(HttpStatus.CREATED) @PostMapping("/balances/rooms") - public RoomJoinResponse createRoom(@Valid @RequestBody RoomJoinRequest request, - HttpServletRequest httpRequest, - HttpServletResponse httpResponse) { - RoomJoinResponse roomJoinResponse = roomFacade.createRoom(request.nickname()); - setEncryptCookie(httpRequest, httpResponse, roomJoinResponse.member().memberId()); - return roomJoinResponse; + public RoomJoinResponse createRoom(@Valid @RequestBody RoomJoinRequest request) { + return roomFacade.createRoom(request.nickname()); } @GetMapping("/balances/rooms/member") - public RoomMemberResponse getRoomMemberInfo(@CookieValue(name = "${cookie.rejoin-key}") String cookieValue) { - return roomFacade.getRoomMemberInfo(roomMemberCookieEncryptor.getDecodedCookieValue(cookieValue)); + public RoomMemberResponse getRoomMemberInfo(@MemberId Long memberId) { + return roomFacade.getRoomMemberInfo(memberId); } @Polling @@ -69,25 +63,20 @@ public void updateRoomSetting(@PathVariable @Positive Long roomId, roomFacade.updateRoomSetting(roomId, request); } + @IssueCookie @ResponseStatus(HttpStatus.CREATED) @PostMapping("/balances/rooms/{uuid}/members") public RoomJoinResponse joinRoom(@PathVariable String uuid, - @Valid @RequestBody RoomJoinRequest request, - HttpServletRequest httpRequest, - HttpServletResponse httpResponse) { - RoomJoinResponse roomJoinResponse = roomFacade.joinRoom(request.nickname(), uuid); - setEncryptCookie(httpRequest, httpResponse, roomJoinResponse.member().memberId()); - return roomJoinResponse; + @Valid @RequestBody RoomJoinRequest request) { + return roomFacade.joinRoom(request.nickname(), uuid); } + @DeleteCookie @ResponseStatus(HttpStatus.NO_CONTENT) @DeleteMapping("/balances/rooms/{roomId}/members/{memberId}") public void leaveRoom(@PathVariable @Positive Long roomId, - @PathVariable @Positive Long memberId, - HttpServletRequest request, - HttpServletResponse response) { + @PathVariable @Positive Long memberId) { roomFacade.leaveRoom(roomId, memberId); - deleteCookie(request, response); } @ResponseStatus(HttpStatus.NO_CONTENT) @@ -125,19 +114,4 @@ public RoomStatusResponse getRoomStatus(@NotBlank @PathVariable String uuid) { public InitialRoomResponse isInitialRoom(@PathVariable @Positive Long roomId) { return roomFacade.isInitialRoom(roomId); } - - private void setEncryptCookie(HttpServletRequest request, - HttpServletResponse response, - Object cookieValue) { - String origin = request.getHeader(HttpHeaders.ORIGIN); - ResponseCookie encodedCookie = roomMemberCookieEncryptor.getEncodedCookie(cookieValue, origin); - response.addHeader(HttpHeaders.SET_COOKIE, encodedCookie.toString()); - } - - private void deleteCookie(HttpServletRequest request, - HttpServletResponse response) { - String origin = request.getHeader(HttpHeaders.ORIGIN); - ResponseCookie deleteCookie = roomMemberCookieEncryptor.deleteCookie(origin); - response.addHeader(HttpHeaders.SET_COOKIE, deleteCookie.toString()); - } } diff --git a/backend/src/main/java/ddangkong/resolver/RoomMemberArgumentResolver.java b/backend/src/main/java/ddangkong/resolver/RoomMemberArgumentResolver.java new file mode 100644 index 000000000..8b3d3bf98 --- /dev/null +++ b/backend/src/main/java/ddangkong/resolver/RoomMemberArgumentResolver.java @@ -0,0 +1,52 @@ +package ddangkong.resolver; + +import ddangkong.aop.cookie.MemberId; +import ddangkong.aop.cookie.RoomMemberCookieEncryptor; +import ddangkong.exception.room.NotFoundCookieException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; +import org.springframework.beans.factory.annotation.Value; +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 +public class RoomMemberArgumentResolver implements HandlerMethodArgumentResolver { + + private final RoomMemberCookieEncryptor roomMemberCookieEncryptor; + + private final String cookieKey; + + public RoomMemberArgumentResolver(RoomMemberCookieEncryptor roomMemberCookieEncryptor, @Value("${cookie.rejoin-key}") String cookieKey) { + this.roomMemberCookieEncryptor = roomMemberCookieEncryptor; + this.cookieKey = cookieKey; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean isLongType = parameter.getParameterType().equals(Long.class); + boolean hasParameterAnnotation = parameter.hasParameterAnnotation(MemberId.class); + return isLongType && hasParameterAnnotation; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + throw new NotFoundCookieException(); + } + Cookie cookie = Arrays.stream(request.getCookies()) + .filter(curCookie -> curCookie.getName().equals(cookieKey)) + .findAny() + .orElseThrow(NotFoundCookieException::new); + + String cookieValue = cookie.getValue(); + return roomMemberCookieEncryptor.getDecodedCookieValue(cookieValue); + } +} diff --git a/backend/src/test/java/ddangkong/controller/room/EncryptionUtilsTest.java b/backend/src/test/java/ddangkong/controller/room/EncryptionUtilsTest.java index e029cfc5c..c38b204a8 100644 --- a/backend/src/test/java/ddangkong/controller/room/EncryptionUtilsTest.java +++ b/backend/src/test/java/ddangkong/controller/room/EncryptionUtilsTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import ddangkong.aop.cookie.EncryptionUtils; import ddangkong.controller.BaseControllerTest; import ddangkong.exception.room.InvalidCookieException; import org.junit.jupiter.api.Nested; diff --git a/backend/src/test/java/ddangkong/controller/room/RoomMemberCookieEncryptorTest.java b/backend/src/test/java/ddangkong/controller/room/RoomMemberCookieEncryptorTest.java index e5cfa457e..9db3d4bb3 100644 --- a/backend/src/test/java/ddangkong/controller/room/RoomMemberCookieEncryptorTest.java +++ b/backend/src/test/java/ddangkong/controller/room/RoomMemberCookieEncryptorTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import ddangkong.aop.cookie.RoomMemberCookieEncryptor; import ddangkong.controller.BaseControllerTest; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/ddangkong/documentation/BaseDocumentationTest.java b/backend/src/test/java/ddangkong/documentation/BaseDocumentationTest.java index 903f7c13a..f732db789 100644 --- a/backend/src/test/java/ddangkong/documentation/BaseDocumentationTest.java +++ b/backend/src/test/java/ddangkong/documentation/BaseDocumentationTest.java @@ -7,9 +7,13 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import com.fasterxml.jackson.databind.ObjectMapper; +import ddangkong.aop.cookie.EncryptionUtils; +import ddangkong.aop.cookie.RoomMemberCookieAspect; +import ddangkong.aop.cookie.RoomMemberCookieEncryptor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; @@ -18,6 +22,7 @@ import org.springframework.web.context.WebApplicationContext; @ExtendWith(RestDocumentationExtension.class) +@Import(value = {RoomMemberCookieEncryptor.class, EncryptionUtils.class, RoomMemberCookieAspect.class}) public abstract class BaseDocumentationTest { @Autowired diff --git a/backend/src/test/java/ddangkong/documentation/room/RoomDocumentationTest.java b/backend/src/test/java/ddangkong/documentation/room/RoomDocumentationTest.java index 7e25cbc13..b279e40fc 100644 --- a/backend/src/test/java/ddangkong/documentation/room/RoomDocumentationTest.java +++ b/backend/src/test/java/ddangkong/documentation/room/RoomDocumentationTest.java @@ -26,8 +26,6 @@ import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import ddangkong.controller.room.RoomMemberCookieEncryptor; -import ddangkong.controller.room.EncryptionUtils; import ddangkong.controller.room.RoomController; import ddangkong.documentation.BaseDocumentationTest; import ddangkong.domain.balance.content.Category; @@ -50,11 +48,11 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.http.MediaType; -@WebMvcTest(value = RoomController.class) -@Import(value = {RoomMemberCookieEncryptor.class, EncryptionUtils.class}) +@WebMvcTest(RoomController.class) +@EnableAspectJAutoProxy class RoomDocumentationTest extends BaseDocumentationTest { @MockBean