diff --git a/backend/src/docs/asciidoc/room.adoc b/backend/src/docs/asciidoc/room.adoc index 43d8fe7d7..c45037af3 100644 --- a/backend/src/docs/asciidoc/room.adoc +++ b/backend/src/docs/asciidoc/room.adoc @@ -58,27 +58,27 @@ include::{snippets}/room/join/response-cookies.adoc[] ''' -=== 방 재참여 +=== 사용자 정보 조회 ==== curl -include::{snippets}/room/rejoin/curl-request.adoc[] +include::{snippets}/room/member/curl-request.adoc[] ==== request -include::{snippets}/room/rejoin/http-request.adoc[] +include::{snippets}/room/member/http-request.adoc[] request cookies -include::{snippets}/room/rejoin/request-cookies.adoc[] +include::{snippets}/room/member/request-cookies.adoc[] ==== response -include::{snippets}/room/rejoin/http-response.adoc[] +include::{snippets}/room/member/http-response.adoc[] response fields -include::{snippets}/room/join/response-fields.adoc[] +include::{snippets}/room/member/response-fields.adoc[] ''' diff --git a/backend/src/main/java/ddangkong/config/CorsConfig.java b/backend/src/main/java/ddangkong/config/CorsConfig.java index 2cbba1faf..0065883ab 100644 --- a/backend/src/main/java/ddangkong/config/CorsConfig.java +++ b/backend/src/main/java/ddangkong/config/CorsConfig.java @@ -9,19 +9,20 @@ @Configuration public class CorsConfig implements WebMvcConfigurer { - private final String corsOrigin; + private final String[] corsOrigin; - public CorsConfig(@Value("${cors.origin}") String corsOrigin) { + public CorsConfig(@Value("${cors.origin}") String[] corsOrigin) { this.corsOrigin = corsOrigin; } @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins(corsOrigin) + .allowedOriginPatterns(corsOrigin) .allowedMethods( HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.PATCH.name(), HttpMethod.DELETE.name() ) + .allowCredentials(true) .allowedHeaders("*"); } } diff --git a/backend/src/main/java/ddangkong/controller/room/RejoinCookieEncryptor.java b/backend/src/main/java/ddangkong/controller/room/RejoinCookieEncryptor.java deleted file mode 100644 index ba7da2731..000000000 --- a/backend/src/main/java/ddangkong/controller/room/RejoinCookieEncryptor.java +++ /dev/null @@ -1,27 +0,0 @@ -package ddangkong.controller.room; - -import jakarta.servlet.http.Cookie; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -@Component -public class RejoinCookieEncryptor { - - private final EncryptionUtils encryptionUtils; - - private final String rejoinKey; - - public RejoinCookieEncryptor(EncryptionUtils encryptionUtils, @Value("${cookie.rejoin-key}") String rejoinKey) { - this.encryptionUtils = encryptionUtils; - this.rejoinKey = rejoinKey; - } - - public Cookie getEncodedCookie(Object value) { - String encrypt = encryptionUtils.encrypt(String.valueOf(value)); - return new Cookie(rejoinKey, encrypt); - } - - public Long getDecodedCookieValue(String cookieValue) { - return Long.parseLong(encryptionUtils.decrypt(cookieValue)); - } -} diff --git a/backend/src/main/java/ddangkong/controller/room/RoomController.java b/backend/src/main/java/ddangkong/controller/room/RoomController.java index c03067eb1..76e31a17f 100644 --- a/backend/src/main/java/ddangkong/controller/room/RoomController.java +++ b/backend/src/main/java/ddangkong/controller/room/RoomController.java @@ -6,16 +6,19 @@ import ddangkong.facade.room.dto.RoomInfoResponse; import ddangkong.facade.room.dto.RoomJoinRequest; import ddangkong.facade.room.dto.RoomJoinResponse; +import ddangkong.facade.room.dto.RoomMemberResponse; import ddangkong.facade.room.dto.RoomSettingRequest; import ddangkong.facade.room.dto.RoomStatusResponse; import ddangkong.facade.room.dto.RoundFinishedResponse; -import jakarta.servlet.http.Cookie; +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; @@ -36,19 +39,21 @@ public class RoomController { private final RoomFacade roomFacade; - private final RejoinCookieEncryptor rejoinCookieEncryptor; + private final RoomMemberCookieEncryptor roomMemberCookieEncryptor; @ResponseStatus(HttpStatus.CREATED) @PostMapping("/balances/rooms") - public RoomJoinResponse createRoom(@Valid @RequestBody RoomJoinRequest request, HttpServletResponse response) { + public RoomJoinResponse createRoom(@Valid @RequestBody RoomJoinRequest request, + HttpServletRequest httpRequest, + HttpServletResponse httpResponse) { RoomJoinResponse roomJoinResponse = roomFacade.createRoom(request.nickname()); - setEncryptCookie(response, roomJoinResponse.member().memberId()); + setEncryptCookie(httpRequest, httpResponse, roomJoinResponse.member().memberId()); return roomJoinResponse; } - @GetMapping("/balances/rooms/rejoin") - public RoomJoinResponse rejoinRoom(@CookieValue(name = "${cookie.rejoin-key}") String cookieValue) { - return roomFacade.rejoinRoom(rejoinCookieEncryptor.getDecodedCookieValue(cookieValue)); + @GetMapping("/balances/rooms/member") + public RoomMemberResponse getRoomMemberInfo(@CookieValue(name = "${cookie.rejoin-key}") String cookieValue) { + return roomFacade.getRoomMemberInfo(roomMemberCookieEncryptor.getDecodedCookieValue(cookieValue)); } @Polling @@ -68,9 +73,10 @@ public void updateRoomSetting(@PathVariable @Positive Long roomId, @PostMapping("/balances/rooms/{uuid}/members") public RoomJoinResponse joinRoom(@PathVariable String uuid, @Valid @RequestBody RoomJoinRequest request, - HttpServletResponse response) { + HttpServletRequest httpRequest, + HttpServletResponse httpResponse) { RoomJoinResponse roomJoinResponse = roomFacade.joinRoom(request.nickname(), uuid); - setEncryptCookie(response, roomJoinResponse.member().memberId()); + setEncryptCookie(httpRequest, httpResponse, roomJoinResponse.member().memberId()); return roomJoinResponse; } @@ -117,8 +123,11 @@ public InitialRoomResponse isInitialRoom(@PathVariable @Positive Long roomId) { return roomFacade.isInitialRoom(roomId); } - private void setEncryptCookie(HttpServletResponse response, Object cookieValue) { - Cookie encodedCookie = rejoinCookieEncryptor.getEncodedCookie(cookieValue); - response.addCookie(encodedCookie); + 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()); } } diff --git a/backend/src/main/java/ddangkong/controller/room/RoomMemberCookieEncryptor.java b/backend/src/main/java/ddangkong/controller/room/RoomMemberCookieEncryptor.java new file mode 100644 index 000000000..e313870ea --- /dev/null +++ b/backend/src/main/java/ddangkong/controller/room/RoomMemberCookieEncryptor.java @@ -0,0 +1,44 @@ +package ddangkong.controller.room; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.server.Cookie.SameSite; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; + +@Component +public class RoomMemberCookieEncryptor { + + private static final String DEFAULT_PATH = "/api/balances/rooms"; + private static final String LOCALHOST = "http://localhost"; + + private final EncryptionUtils encryptionUtils; + + private final String rejoinKey; + + public RoomMemberCookieEncryptor(EncryptionUtils encryptionUtils, @Value("${cookie.rejoin-key}") String rejoinKey) { + this.encryptionUtils = encryptionUtils; + this.rejoinKey = rejoinKey; + } + + public ResponseCookie getEncodedCookie(Object value, String origin) { + String encrypt = encryptionUtils.encrypt(String.valueOf(value)); + return ResponseCookie.from(rejoinKey, encrypt) + .httpOnly(true) + .secure(true) + .path(DEFAULT_PATH) + .sameSite(getSameSiteOption(origin)) + .build(); + } + + private String getSameSiteOption(String origin) { + if (origin != null && origin.startsWith(LOCALHOST)) { + return SameSite.NONE.attributeValue(); + } + return SameSite.LAX.attributeValue(); + } + + public Long getDecodedCookieValue(String cookieValue) { + return Long.parseLong(encryptionUtils.decrypt(cookieValue)); + } +} diff --git a/backend/src/main/java/ddangkong/facade/room/RoomFacade.java b/backend/src/main/java/ddangkong/facade/room/RoomFacade.java index f0464d4e0..a4ce9fa18 100644 --- a/backend/src/main/java/ddangkong/facade/room/RoomFacade.java +++ b/backend/src/main/java/ddangkong/facade/room/RoomFacade.java @@ -8,6 +8,7 @@ import ddangkong.facade.room.dto.InitialRoomResponse; import ddangkong.facade.room.dto.RoomInfoResponse; import ddangkong.facade.room.dto.RoomJoinResponse; +import ddangkong.facade.room.dto.RoomMemberResponse; import ddangkong.facade.room.dto.RoomSettingRequest; import ddangkong.facade.room.dto.RoomStatusResponse; import ddangkong.facade.room.dto.RoundFinishedResponse; @@ -53,10 +54,10 @@ public RoomJoinResponse joinRoom(String nickname, String uuid) { } @Transactional(readOnly = true) - public RoomJoinResponse rejoinRoom(Long memberId) { + public RoomMemberResponse getRoomMemberInfo(Long memberId) { Member member = memberService.getMemberById(memberId); Room room = member.getRoom(); - return new RoomJoinResponse(room.getId(), room.getUuid(), new MemberResponse(member)); + return new RoomMemberResponse(room.getId(), room.getUuid(), new MemberResponse(member)); } @Transactional diff --git a/backend/src/main/java/ddangkong/facade/room/dto/RoomMemberResponse.java b/backend/src/main/java/ddangkong/facade/room/dto/RoomMemberResponse.java new file mode 100644 index 000000000..9552c307f --- /dev/null +++ b/backend/src/main/java/ddangkong/facade/room/dto/RoomMemberResponse.java @@ -0,0 +1,10 @@ +package ddangkong.facade.room.dto; + +import ddangkong.facade.room.member.dto.MemberResponse; + +public record RoomMemberResponse( + Long roomId, + String roomUuid, + MemberResponse member +) { +} diff --git a/backend/src/test/java/ddangkong/controller/room/RoomControllerTest.java b/backend/src/test/java/ddangkong/controller/room/RoomControllerTest.java index 4d18b6b1c..2be367756 100644 --- a/backend/src/test/java/ddangkong/controller/room/RoomControllerTest.java +++ b/backend/src/test/java/ddangkong/controller/room/RoomControllerTest.java @@ -349,7 +349,7 @@ class 쿠키 { } @Test - void 쿠키를_통해_방에_재참여_할_수_있다() { + void 쿠키를_통해_사용자_정보를_조회_할_수_있다() { // given RoomJoinRequest body = new RoomJoinRequest("참가자"); String cookie = RestAssured.given().log().all() @@ -362,7 +362,7 @@ class 쿠키 { RoomJoinResponse roomJoinResponse = RestAssured.given().log().all() .contentType(ContentType.JSON) .cookie("test_cookie", cookie) - .when().get("/api/balances/rooms/rejoin") + .when().get("/api/balances/rooms/member") .then().contentType(ContentType.JSON).log().all() .statusCode(200) .extract().as(RoomJoinResponse.class); diff --git a/backend/src/test/java/ddangkong/controller/room/RoomMemberCookieEncryptorTest.java b/backend/src/test/java/ddangkong/controller/room/RoomMemberCookieEncryptorTest.java new file mode 100644 index 000000000..e5cfa457e --- /dev/null +++ b/backend/src/test/java/ddangkong/controller/room/RoomMemberCookieEncryptorTest.java @@ -0,0 +1,45 @@ +package ddangkong.controller.room; + +import static org.assertj.core.api.Assertions.assertThat; + +import ddangkong.controller.BaseControllerTest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseCookie; + +class RoomMemberCookieEncryptorTest extends BaseControllerTest { + + @Autowired + private RoomMemberCookieEncryptor roomMemberCookieEncryptor; + + @Nested + class 방_멤버_쿠키_암호화 { + + @Test + void 로컬_환경인_경우_SameSite는_None_이다() { + // given + String value = "ThisIsMySecretKe"; + String origin = "http://localhost:3306/api"; + + // when + ResponseCookie encodedCookie = roomMemberCookieEncryptor.getEncodedCookie(value, origin); + + // then + assertThat(encodedCookie.getSameSite()).isEqualTo("None"); + } + + @Test + void 로컬_환경이_아닌_경우_SameSite는_Lax_이다() { + // given + String value = "ThisIsMySecretKe"; + String origin = "ddangkong.kr"; + + // when + ResponseCookie encodedCookie = roomMemberCookieEncryptor.getEncodedCookie(value, origin); + + // then + assertThat(encodedCookie.getSameSite()).isEqualTo("Lax"); + } + } +} diff --git a/backend/src/test/java/ddangkong/documentation/room/RoomDocumentationTest.java b/backend/src/test/java/ddangkong/documentation/room/RoomDocumentationTest.java index 3695c983b..97249127c 100644 --- a/backend/src/test/java/ddangkong/documentation/room/RoomDocumentationTest.java +++ b/backend/src/test/java/ddangkong/documentation/room/RoomDocumentationTest.java @@ -26,7 +26,7 @@ import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import ddangkong.controller.room.RejoinCookieEncryptor; +import ddangkong.controller.room.RoomMemberCookieEncryptor; import ddangkong.controller.room.EncryptionUtils; import ddangkong.controller.room.RoomController; import ddangkong.documentation.BaseDocumentationTest; @@ -37,6 +37,7 @@ import ddangkong.facade.room.dto.RoomInfoResponse; import ddangkong.facade.room.dto.RoomJoinRequest; import ddangkong.facade.room.dto.RoomJoinResponse; +import ddangkong.facade.room.dto.RoomMemberResponse; import ddangkong.facade.room.dto.RoomSettingRequest; import ddangkong.facade.room.dto.RoomSettingResponse; import ddangkong.facade.room.dto.RoomStatusResponse; @@ -53,7 +54,7 @@ import org.springframework.http.MediaType; @WebMvcTest(value = RoomController.class) -@Import(value = {RejoinCookieEncryptor.class, EncryptionUtils.class}) +@Import(value = {RoomMemberCookieEncryptor.class, EncryptionUtils.class}) class RoomDocumentationTest extends BaseDocumentationTest { @MockBean @@ -218,16 +219,16 @@ class 방_참여 { } @Nested - class 방_재참여 { + class 사용자_정보_조회 { - private static final String ENDPOINT = "/api/balances/rooms/rejoin"; + private static final String ENDPOINT = "/api/balances/rooms/member"; @Test - void 방에_재참여하여_유저_정보를_조회한다() throws Exception { + void 사용자_정보를_조회한다() throws Exception { // given - RoomJoinResponse response = new RoomJoinResponse(1L, "488fd79f92a34131bf2a628bd58c5d2c", + RoomMemberResponse response = new RoomMemberResponse(1L, "488fd79f92a34131bf2a628bd58c5d2c", new MemberResponse(2L, "타콩", false)); - when(roomFacade.rejoinRoom(anyLong())).thenReturn(response); + when(roomFacade.getRoomMemberInfo(anyLong())).thenReturn(response); //when & then mockMvc.perform(get(ENDPOINT) @@ -235,7 +236,7 @@ class 방_재참여 { .cookie(new Cookie("test_cookie", "oNnHwjSR1G4E5L8Mute61w==")) ) .andExpect(status().isOk()) - .andDo(document("room/rejoin", + .andDo(document("room/member", requestCookies( cookieWithName("test_cookie").description("사용자 인증에 필요한 쿠키(쿠키의 키 값은 UUID로 예측할 수 없는 값이 들어감)") ), diff --git a/backend/src/test/java/ddangkong/facade/room/RoomFacadeTest.java b/backend/src/test/java/ddangkong/facade/room/RoomFacadeTest.java index 2d93e253d..3d3c0bda4 100644 --- a/backend/src/test/java/ddangkong/facade/room/RoomFacadeTest.java +++ b/backend/src/test/java/ddangkong/facade/room/RoomFacadeTest.java @@ -26,6 +26,7 @@ import ddangkong.facade.room.dto.InitialRoomResponse; import ddangkong.facade.room.dto.RoomInfoResponse; import ddangkong.facade.room.dto.RoomJoinResponse; +import ddangkong.facade.room.dto.RoomMemberResponse; import ddangkong.facade.room.dto.RoomSettingRequest; import ddangkong.facade.room.dto.RoomStatusResponse; import ddangkong.facade.room.dto.RoundFinishedResponse; @@ -126,10 +127,10 @@ class 방_참여 { } @Nested - class 방_재참여 { + class 사용자_정보_조회 { @Test - void 이전_방에_재참여한다() { + void 사용자_정보를_조회한다() { // given String nickname = "나는참가자"; String uuid = "uuid4"; @@ -137,7 +138,7 @@ class 방_재참여 { roomFacade.joinRoom(nickname, uuid); // when - RoomJoinResponse actual = roomFacade.rejoinRoom(14L); + RoomMemberResponse actual = roomFacade.getRoomMemberInfo(14L); // then assertAll( @@ -148,12 +149,12 @@ class 방_재참여 { } @Test - void 존재하지_않는_아이디로_방에_재참여할_수_없다() { + void 존재하지_않는_아이디로_사용자_정보를_조회할_수_없다() { // given Long notExistMemberId = 0L; // when & then - assertThatThrownBy(() -> roomFacade.rejoinRoom(notExistMemberId)) + assertThatThrownBy(() -> roomFacade.getRoomMemberInfo(notExistMemberId)) .isExactlyInstanceOf(InvalidMemberIdException.class); } }