From d91478c0a9ce9952b4adb79f7ff224a690d3de01 Mon Sep 17 00:00:00 2001 From: Jo Hyeong-Ik <70360890+ikjo39@users.noreply.github.com> Date: Thu, 26 Sep 2024 21:31:24 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20=EC=B0=B8=EA=B0=80=EC=9E=90=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=95=94=ED=98=B8=ED=99=94=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20(#345)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(build.gradle): 비밀번호 암호화 알고리즘을 위한 의존성 추가 * feat(PasswordEncoderConfig): 비밀번호 암호화 알고리즘 스프링 Bean 등록 * feat(PasswordConverter): `@Converter`를 사용한 비밀번호 암호화 로직 추가 * test(AttendeeEncryptedPasswordFixture): 암호화된 비밀번호 Fixture 추가 * feat(AttendeeService): 비밀번호 암호화 추가로 인한 주요 Service 로직 수정 * refactor(AttendeePassword): 직관적인 메서드명으로 변경 * refactor(AttendeePassword): 불필요한 어노테이션 삭제 * test(AttendeeEncryptedPasswordFixture): 리플렉션 설정 변경 후 패키지 이 * refactor(AttendeePassword): Converter 문제 발생으로 인한 암호화 로직 리팩토링 * refactor(AttendeePassword): 생성자 내 PasswordEncoder 제거 --- backend/build.gradle | 3 + .../kr/momo/config/PasswordEncoderConfig.java | 15 ++++ .../kr/momo/domain/attendee/Attendee.java | 19 +++-- .../domain/attendee/AttendeePassword.java | 23 ++----- .../domain/attendee/AttendeeRawPassword.java | 25 +++++++ .../service/attendee/AttendeeService.java | 16 +++-- .../momo/service/meeting/MeetingService.java | 6 +- .../meeting/dto/MeetingCreateRequest.java | 2 +- .../attendee/AttendeeControllerTest.java | 10 ++- .../meeting/MeetingControllerTest.java | 69 +++++++++++-------- .../schedule/ScheduleControllerTest.java | 14 ++-- .../domain/attendee/AttendeePasswordTest.java | 34 +++++---- .../attendee/AttendeeRawPasswordTest.java | 23 +++++++ .../kr/momo/domain/attendee/AttendeeTest.java | 25 +++++-- .../java/kr/momo/fixture/AttendeeFixture.java | 6 +- .../service/attendee/AttendeeServiceTest.java | 14 ++-- .../meeting/MeetingConfirmServiceTest.java | 7 +- .../service/schedule/ScheduleServiceTest.java | 2 - 18 files changed, 211 insertions(+), 102 deletions(-) create mode 100644 backend/src/main/java/kr/momo/config/PasswordEncoderConfig.java create mode 100644 backend/src/main/java/kr/momo/domain/attendee/AttendeeRawPassword.java create mode 100644 backend/src/test/java/kr/momo/domain/attendee/AttendeeRawPasswordTest.java diff --git a/backend/build.gradle b/backend/build.gradle index b1c5409d3..277b3e737 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -35,6 +35,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' + implementation 'org.springframework.security:spring-security-crypto' + implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1' + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/backend/src/main/java/kr/momo/config/PasswordEncoderConfig.java b/backend/src/main/java/kr/momo/config/PasswordEncoderConfig.java new file mode 100644 index 000000000..deddb540a --- /dev/null +++ b/backend/src/main/java/kr/momo/config/PasswordEncoderConfig.java @@ -0,0 +1,15 @@ +package kr.momo.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordEncoderConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); + } +} diff --git a/backend/src/main/java/kr/momo/domain/attendee/Attendee.java b/backend/src/main/java/kr/momo/domain/attendee/Attendee.java index 6f7025826..37d8adcab 100644 --- a/backend/src/main/java/kr/momo/domain/attendee/Attendee.java +++ b/backend/src/main/java/kr/momo/domain/attendee/Attendee.java @@ -19,6 +19,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; @Table(name = "attendee") @Entity @@ -46,6 +47,10 @@ public class Attendee extends BaseEntity { @Column(nullable = false, length = 10) private Role role; + public Attendee(Meeting meeting, String name, AttendeePassword password, Role role) { + this(meeting, new AttendeeName(name), password, role); + } + public Attendee(Meeting meeting, AttendeeName name, AttendeePassword password, Role role) { this.meeting = meeting; this.name = name; @@ -53,10 +58,6 @@ public Attendee(Meeting meeting, AttendeeName name, AttendeePassword password, R this.role = role; } - public Attendee(Meeting meeting, String name, String password, Role role) { - this(meeting, new AttendeeName(name), new AttendeePassword(password), role); - } - public boolean isHost() { return role.isHost(); } @@ -65,15 +66,11 @@ public boolean isNotHost() { return !isHost(); } - public void verifyPassword(AttendeePassword other) { - this.password.verifyPassword(other); + public void verifyPassword(AttendeeRawPassword rawPassword, PasswordEncoder passwordEncoder) { + password.verifyMatch(rawPassword, passwordEncoder); } public String name() { - return this.name.getName(); - } - - public String password() { - return this.password.getPassword(); + return name.getName(); } } diff --git a/backend/src/main/java/kr/momo/domain/attendee/AttendeePassword.java b/backend/src/main/java/kr/momo/domain/attendee/AttendeePassword.java index 3ae39c713..e9005710b 100644 --- a/backend/src/main/java/kr/momo/domain/attendee/AttendeePassword.java +++ b/backend/src/main/java/kr/momo/domain/attendee/AttendeePassword.java @@ -2,42 +2,27 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; -import java.util.regex.Pattern; import kr.momo.exception.MomoException; import kr.momo.exception.code.AttendeeErrorCode; import lombok.AccessLevel; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; @Embeddable @Getter -@EqualsAndHashCode @NoArgsConstructor(access = AccessLevel.PROTECTED) public class AttendeePassword { - private static final Pattern PASSWORD_PATTERN = Pattern.compile("^\\d{4}+$"); - - @Column(nullable = false, length = 4) + @Column(nullable = false) private String password; public AttendeePassword(String password) { - validatePassword(password); this.password = password; } - private void validatePassword(String password) { - validatePasswordFormat(password); - } - - private void validatePasswordFormat(String password) { - if (!PASSWORD_PATTERN.matcher(password).matches()) { - throw new MomoException(AttendeeErrorCode.INVALID_PASSWORD_FORMAT); - } - } - - public void verifyPassword(AttendeePassword other) { - if (!this.equals(other)) { + public void verifyMatch(AttendeeRawPassword rawPassword, PasswordEncoder passwordEncoder) { + if (!passwordEncoder.matches(rawPassword.password(), password)) { throw new MomoException(AttendeeErrorCode.PASSWORD_MISMATCHED); } } diff --git a/backend/src/main/java/kr/momo/domain/attendee/AttendeeRawPassword.java b/backend/src/main/java/kr/momo/domain/attendee/AttendeeRawPassword.java new file mode 100644 index 000000000..79658f1a1 --- /dev/null +++ b/backend/src/main/java/kr/momo/domain/attendee/AttendeeRawPassword.java @@ -0,0 +1,25 @@ +package kr.momo.domain.attendee; + +import java.util.regex.Pattern; +import kr.momo.exception.MomoException; +import kr.momo.exception.code.AttendeeErrorCode; +import org.springframework.security.crypto.password.PasswordEncoder; + +public record AttendeeRawPassword(String password) { + + private static final Pattern PASSWORD_PATTERN = Pattern.compile("^\\d{4}+$"); + + public AttendeeRawPassword { + validatePassword(password); + } + + private void validatePassword(String password) { + if (password == null || !PASSWORD_PATTERN.matcher(password).matches()) { + throw new MomoException(AttendeeErrorCode.INVALID_PASSWORD_FORMAT); + } + } + + public AttendeePassword encodePassword(PasswordEncoder passwordEncoder) { + return new AttendeePassword(passwordEncoder.encode(password)); + } +} diff --git a/backend/src/main/java/kr/momo/service/attendee/AttendeeService.java b/backend/src/main/java/kr/momo/service/attendee/AttendeeService.java index 4707390c5..0795979e5 100644 --- a/backend/src/main/java/kr/momo/service/attendee/AttendeeService.java +++ b/backend/src/main/java/kr/momo/service/attendee/AttendeeService.java @@ -4,6 +4,7 @@ import kr.momo.domain.attendee.Attendee; import kr.momo.domain.attendee.AttendeeName; import kr.momo.domain.attendee.AttendeePassword; +import kr.momo.domain.attendee.AttendeeRawPassword; import kr.momo.domain.attendee.AttendeeRepository; import kr.momo.domain.attendee.Role; import kr.momo.domain.meeting.Meeting; @@ -14,6 +15,7 @@ import kr.momo.service.attendee.dto.AttendeeLoginResponse; import kr.momo.service.auth.JwtManager; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -24,6 +26,7 @@ public class AttendeeService { private final AttendeeRepository attendeeRepository; private final MeetingRepository meetingRepository; private final JwtManager jwtManager; + private final PasswordEncoder passwordEncoder; @Transactional public AttendeeLoginResponse login(String uuid, AttendeeLoginRequest request) { @@ -31,19 +34,20 @@ public AttendeeLoginResponse login(String uuid, AttendeeLoginRequest request) { .orElseThrow(() -> new MomoException(MeetingErrorCode.INVALID_UUID)); AttendeeName name = new AttendeeName(request.attendeeName()); - AttendeePassword password = new AttendeePassword(request.password()); + AttendeeRawPassword rawPassword = new AttendeeRawPassword(request.password()); return attendeeRepository.findByMeetingAndName(meeting, name) - .map(attendee -> verifyPassword(attendee, password)) - .orElseGet(() -> signup(meeting, name, password)); + .map(attendee -> verifyPassword(attendee, rawPassword)) + .orElseGet(() -> signup(meeting, name, rawPassword)); } - private AttendeeLoginResponse verifyPassword(Attendee attendee, AttendeePassword password) { - attendee.verifyPassword(password); + private AttendeeLoginResponse verifyPassword(Attendee attendee, AttendeeRawPassword rawPassword) { + attendee.verifyPassword(rawPassword, passwordEncoder); return AttendeeLoginResponse.from(jwtManager.generate(attendee.getId()), attendee); } - private AttendeeLoginResponse signup(Meeting meeting, AttendeeName name, AttendeePassword password) { + private AttendeeLoginResponse signup(Meeting meeting, AttendeeName name, AttendeeRawPassword rawPassword) { + AttendeePassword password = rawPassword.encodePassword(passwordEncoder); Attendee attendee = new Attendee(meeting, name, password, Role.GUEST); attendeeRepository.save(attendee); return AttendeeLoginResponse.from(jwtManager.generate(attendee.getId()), attendee); diff --git a/backend/src/main/java/kr/momo/service/meeting/MeetingService.java b/backend/src/main/java/kr/momo/service/meeting/MeetingService.java index 67da08d30..f3f660bc5 100644 --- a/backend/src/main/java/kr/momo/service/meeting/MeetingService.java +++ b/backend/src/main/java/kr/momo/service/meeting/MeetingService.java @@ -5,6 +5,7 @@ import java.time.LocalTime; import java.util.List; import kr.momo.domain.attendee.Attendee; +import kr.momo.domain.attendee.AttendeeRawPassword; import kr.momo.domain.attendee.AttendeeRepository; import kr.momo.domain.attendee.Role; import kr.momo.domain.availabledate.AvailableDateBatchRepository; @@ -22,6 +23,7 @@ import kr.momo.service.meeting.dto.MeetingResponse; import kr.momo.service.meeting.dto.MeetingSharingResponse; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -39,6 +41,7 @@ public class MeetingService { private final AvailableDateRepository availableDateRepository; private final AttendeeRepository attendeeRepository; private final AvailableDateBatchRepository availableDateBatchRepository; + private final PasswordEncoder passwordEncoder; @Transactional public MeetingCreateResponse create(MeetingCreateRequest request) { @@ -82,7 +85,8 @@ private void validateNotPast(AvailableDates meetingDates) { } private Attendee saveHostAttendee(Meeting meeting, String hostName, String hostPassword) { - Attendee attendee = new Attendee(meeting, hostName, hostPassword, Role.HOST); + AttendeeRawPassword rawPassword = new AttendeeRawPassword(hostPassword); + Attendee attendee = new Attendee(meeting, hostName, rawPassword.encodePassword(passwordEncoder), Role.HOST); return attendeeRepository.save(attendee); } diff --git a/backend/src/main/java/kr/momo/service/meeting/dto/MeetingCreateRequest.java b/backend/src/main/java/kr/momo/service/meeting/dto/MeetingCreateRequest.java index 92af0876e..e4f140bb9 100644 --- a/backend/src/main/java/kr/momo/service/meeting/dto/MeetingCreateRequest.java +++ b/backend/src/main/java/kr/momo/service/meeting/dto/MeetingCreateRequest.java @@ -51,7 +51,7 @@ public List toAvailableMeetingDates() { } public LocalTime toMeetingStartTime() { - return LocalTime.parse(meetingStartTime); + return LocalTime.parse(meetingStartTime); } public LocalTime toMeetingEndTime() { diff --git a/backend/src/test/java/kr/momo/controller/attendee/AttendeeControllerTest.java b/backend/src/test/java/kr/momo/controller/attendee/AttendeeControllerTest.java index a810156a5..a736a9b8b 100644 --- a/backend/src/test/java/kr/momo/controller/attendee/AttendeeControllerTest.java +++ b/backend/src/test/java/kr/momo/controller/attendee/AttendeeControllerTest.java @@ -26,6 +26,7 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; +import org.springframework.security.crypto.password.PasswordEncoder; @IsolateDatabase @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @@ -43,6 +44,9 @@ class AttendeeControllerTest { @Autowired private JwtManager jwtManager; + @Autowired + private PasswordEncoder passwordEncoder; + @BeforeEach void setUp() { RestAssured.port = port; @@ -52,9 +56,9 @@ void setUp() { @Test void login() { Meeting meeting = meetingRepository.save(MeetingFixture.COFFEE.create()); - Attendee attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); - - AttendeeLoginRequest request = new AttendeeLoginRequest(attendee.name(), attendee.password()); + AttendeeFixture jazz = AttendeeFixture.HOST_JAZZ; + Attendee attendee = attendeeRepository.save(jazz.create(meeting)); + AttendeeLoginRequest request = new AttendeeLoginRequest(attendee.name(), jazz.getPassword()); Response response = RestAssured.given().log().all() .contentType(ContentType.JSON) diff --git a/backend/src/test/java/kr/momo/controller/meeting/MeetingControllerTest.java b/backend/src/test/java/kr/momo/controller/meeting/MeetingControllerTest.java index 5fd690cfc..56977644c 100644 --- a/backend/src/test/java/kr/momo/controller/meeting/MeetingControllerTest.java +++ b/backend/src/test/java/kr/momo/controller/meeting/MeetingControllerTest.java @@ -236,8 +236,9 @@ void createByDuplicatedName() { @Test void lock() { Meeting meeting = meetingRepository.save(MeetingFixture.DINNER.create()); - Attendee attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); - String token = getToken(attendee, meeting); + AttendeeFixture jazz = AttendeeFixture.HOST_JAZZ; + Attendee attendee = attendeeRepository.save(jazz.create(meeting)); + String token = getToken(jazz.getPassword(), attendee, meeting); RestAssured.given().log().all() .cookie("ACCESS_TOKEN", token) @@ -253,8 +254,9 @@ void lock() { void lockWithInvalidUUID() { String invalidUUID = "INVALID_UUID"; Meeting meeting = meetingRepository.save(MeetingFixture.DINNER.create()); - Attendee attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); - String token = getToken(attendee, meeting); + AttendeeFixture jazz = AttendeeFixture.HOST_JAZZ; + Attendee attendee = attendeeRepository.save(jazz.create(meeting)); + String token = getToken(jazz.getPassword(), attendee, meeting); RestAssured.given().log().all() .cookie("ACCESS_TOKEN", token) @@ -269,8 +271,9 @@ void lockWithInvalidUUID() { @Test void lockWithNoPermission() { Meeting meeting = meetingRepository.save(MeetingFixture.DINNER.create()); - Attendee attendee = attendeeRepository.save(AttendeeFixture.GUEST_PEDRO.create(meeting)); - String token = getToken(attendee, meeting); + AttendeeFixture daon = AttendeeFixture.GUEST_DAON; + Attendee attendee = attendeeRepository.save(daon.create(meeting)); + String token = getToken(daon.getPassword(), attendee, meeting); RestAssured.given().log().all() .cookie("ACCESS_TOKEN", token) @@ -285,8 +288,9 @@ void lockWithNoPermission() { @Test void unlock() { Meeting meeting = meetingRepository.save(MeetingFixture.DINNER.create()); - Attendee attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); - String token = getToken(attendee, meeting); + AttendeeFixture jazz = AttendeeFixture.HOST_JAZZ; + Attendee attendee = attendeeRepository.save(jazz.create(meeting)); + String token = getToken(jazz.getPassword(), attendee, meeting); RestAssured.given().log().all() .cookie("ACCESS_TOKEN", token) @@ -302,8 +306,9 @@ void unlock() { void unlockWithInvalidUUID() { String invalidUUID = "INVALID_UUID"; Meeting meeting = meetingRepository.save(MeetingFixture.DINNER.create()); - Attendee attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); - String token = getToken(attendee, meeting); + AttendeeFixture jazz = AttendeeFixture.HOST_JAZZ; + Attendee attendee = attendeeRepository.save(jazz.create(meeting)); + String token = getToken(jazz.getPassword(), attendee, meeting); RestAssured.given().log().all() .cookie("ACCESS_TOKEN", token) @@ -318,8 +323,9 @@ void unlockWithInvalidUUID() { @Test void unlockWithNoPermission() { Meeting meeting = meetingRepository.save(MeetingFixture.DINNER.create()); - Attendee attendee = attendeeRepository.save(AttendeeFixture.GUEST_PEDRO.create(meeting)); - String token = getToken(attendee, meeting); + AttendeeFixture daon = AttendeeFixture.GUEST_DAON; + Attendee attendee = attendeeRepository.save(daon.create(meeting)); + String token = getToken(daon.getPassword(), attendee, meeting); RestAssured.given().log().all() .cookie("ACCESS_TOKEN", token) @@ -330,8 +336,8 @@ void unlockWithNoPermission() { .statusCode(HttpStatus.FORBIDDEN.value()); } - private String getToken(Attendee attendee, Meeting meeting) { - AttendeeLoginRequest request = new AttendeeLoginRequest(attendee.name(), attendee.password()); + private String getToken(String rawPassword, Attendee attendee, Meeting meeting) { + AttendeeLoginRequest request = new AttendeeLoginRequest(attendee.name(), rawPassword); return RestAssured.given().log().all() .contentType(ContentType.JSON) @@ -346,9 +352,10 @@ private String getToken(Attendee attendee, Meeting meeting) { @Test void confirmSchedule() { Meeting meeting = createLockedMovieMeeting(); - Attendee host = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); AvailableDate tomorrow = availableDateRepository.save(new AvailableDate(LocalDate.now().plusDays(1), meeting)); - String token = getToken(host, meeting); + AttendeeFixture jazz = AttendeeFixture.HOST_JAZZ; + Attendee attendee = attendeeRepository.save(jazz.create(meeting)); + String token = getToken(jazz.getPassword(), attendee, meeting); MeetingConfirmRequest request = getValidFindRequest(tomorrow); RestAssured.given().log().all() @@ -373,8 +380,9 @@ private Meeting createLockedMovieMeeting() { void confirmScheduleNotHost() { Meeting meeting = createLockedMovieMeeting(); AvailableDate tomorrow = availableDateRepository.save(new AvailableDate(LocalDate.now().plusDays(1), meeting)); - Attendee guest = attendeeRepository.save(AttendeeFixture.GUEST_MARK.create(meeting)); - String token = getToken(guest, meeting); + AttendeeFixture guestMark = AttendeeFixture.GUEST_MARK; + Attendee guest = attendeeRepository.save(guestMark.create(meeting)); + String token = getToken(guestMark.getPassword(), guest, meeting); MeetingConfirmRequest request = getValidFindRequest(tomorrow); RestAssured.given().log().all() @@ -391,9 +399,10 @@ void confirmScheduleNotHost() { @Test void confirmScheduleUnlock() { Meeting meeting = meetingRepository.save(MeetingFixture.MOVIE.create()); - Attendee host = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); + AttendeeFixture jazz = AttendeeFixture.HOST_JAZZ; AvailableDate tomorrow = availableDateRepository.save(new AvailableDate(LocalDate.now().plusDays(1), meeting)); - String token = getToken(host, meeting); + Attendee host = attendeeRepository.save(jazz.create(meeting)); + String token = getToken(jazz.getPassword(), host, meeting); MeetingConfirmRequest request = getValidFindRequest(tomorrow); RestAssured.given().log().all() @@ -413,8 +422,9 @@ void confirmInvalidRequest() { AvailableDate availableDate = availableDateRepository.save( new AvailableDate(LocalDate.now().plusDays(1), meeting)); String tomorrow = availableDate.getDate().format(DateTimeFormatter.ISO_DATE); - Attendee guest = attendeeRepository.save(AttendeeFixture.GUEST_MARK.create(meeting)); - String token = getToken(guest, meeting); + AttendeeFixture guestMark = AttendeeFixture.GUEST_MARK; + Attendee guest = attendeeRepository.save(guestMark.create(meeting)); + String token = getToken(guestMark.getPassword(), guest, meeting); MeetingConfirmRequest request = new MeetingConfirmRequest(tomorrow, "3:00", tomorrow, "03:00"); @@ -432,9 +442,10 @@ void confirmInvalidRequest() { @Test void cancelConfirmedMeeting() { Meeting meeting = createLockedMovieMeeting(); - Attendee host = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); confirmedMeetingRepository.save(ConfirmedMeetingFixture.MOVIE.create(meeting)); - String token = getToken(host, meeting); + AttendeeFixture jazz = AttendeeFixture.HOST_JAZZ; + Attendee attendee = attendeeRepository.save(jazz.create(meeting)); + String token = getToken(jazz.getPassword(), attendee, meeting); RestAssured.given().log().all() .cookie("ACCESS_TOKEN", token) @@ -457,8 +468,9 @@ void cancelConfirmedMeeting() { @Test void cancelConfirmedMeetingNonExist() { Meeting meeting = createLockedMovieMeeting(); - Attendee host = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); - String token = getToken(host, meeting); + AttendeeFixture jazz = AttendeeFixture.HOST_JAZZ; + Attendee attendee = attendeeRepository.save(jazz.create(meeting)); + String token = getToken(jazz.getPassword(), attendee, meeting); RestAssured.given().log().all() .cookie("ACCESS_TOKEN", token) @@ -473,9 +485,10 @@ void cancelConfirmedMeetingNonExist() { @Test void cancelConfirmedMeetingNotHost() { Meeting meeting = createLockedMovieMeeting(); - Attendee guest = attendeeRepository.save(AttendeeFixture.GUEST_MARK.create(meeting)); confirmedMeetingRepository.save(ConfirmedMeetingFixture.MOVIE.create(meeting)); - String token = getToken(guest, meeting); + AttendeeFixture guestMark = AttendeeFixture.GUEST_MARK; + Attendee guest = attendeeRepository.save(guestMark.create(meeting)); + String token = getToken(guestMark.getPassword(), guest, meeting); RestAssured.given().log().all() .cookie("ACCESS_TOKEN", token) diff --git a/backend/src/test/java/kr/momo/controller/schedule/ScheduleControllerTest.java b/backend/src/test/java/kr/momo/controller/schedule/ScheduleControllerTest.java index ca705aa03..98762b778 100644 --- a/backend/src/test/java/kr/momo/controller/schedule/ScheduleControllerTest.java +++ b/backend/src/test/java/kr/momo/controller/schedule/ScheduleControllerTest.java @@ -10,7 +10,6 @@ import java.util.List; import kr.momo.domain.attendee.Attendee; import kr.momo.domain.attendee.AttendeeRepository; -import kr.momo.domain.attendee.Role; import kr.momo.domain.availabledate.AvailableDate; import kr.momo.domain.availabledate.AvailableDateRepository; import kr.momo.domain.meeting.Meeting; @@ -18,6 +17,7 @@ import kr.momo.domain.schedule.Schedule; import kr.momo.domain.schedule.ScheduleRepository; import kr.momo.domain.timeslot.Timeslot; +import kr.momo.fixture.AttendeeFixture; import kr.momo.fixture.MeetingFixture; import kr.momo.service.attendee.dto.AttendeeLoginRequest; import kr.momo.service.schedule.dto.DateTimesCreateRequest; @@ -31,6 +31,7 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; +import org.springframework.security.crypto.password.PasswordEncoder; @IsolateDatabase @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @@ -51,7 +52,11 @@ class ScheduleControllerTest { @Autowired private ScheduleRepository scheduleRepository; + @Autowired + private PasswordEncoder passwordEncoder; + private Meeting meeting; + private AttendeeFixture fixture; private Attendee attendee; private AvailableDate today; private AvailableDate tomorrow; @@ -59,8 +64,9 @@ class ScheduleControllerTest { @BeforeEach void setUp() { RestAssured.port = port; + fixture = AttendeeFixture.GUEST_DAON; meeting = meetingRepository.save(MeetingFixture.MOVIE.create()); - attendee = attendeeRepository.save(new Attendee(meeting, "name", "1234", Role.GUEST)); + attendee = attendeeRepository.save(fixture.create(meeting)); today = availableDateRepository.save(new AvailableDate(LocalDate.now(), meeting)); tomorrow = availableDateRepository.save(new AvailableDate(LocalDate.now().plusDays(1), meeting)); } @@ -68,7 +74,7 @@ void setUp() { @DisplayName("참가자가 스케줄을 생성하는데 성공하면 200 상태 코드를 응답한다.") @Test void create() { - AttendeeLoginRequest loginRequest = new AttendeeLoginRequest(attendee.name(), attendee.password()); + AttendeeLoginRequest loginRequest = new AttendeeLoginRequest(attendee.name(), fixture.getPassword()); String token = RestAssured.given().log().all() .contentType(ContentType.JSON) @@ -122,7 +128,7 @@ void findAllSchedules() { @DisplayName("UUID와 참가자 ID로 자신의 스케줄을 조회한다.") @Test void findMySchedule() { - AttendeeLoginRequest loginRequest = new AttendeeLoginRequest(attendee.name(), attendee.password()); + AttendeeLoginRequest loginRequest = new AttendeeLoginRequest(attendee.name(), fixture.getPassword()); createAttendeeSchedule(attendee); diff --git a/backend/src/test/java/kr/momo/domain/attendee/AttendeePasswordTest.java b/backend/src/test/java/kr/momo/domain/attendee/AttendeePasswordTest.java index b54d8963a..4e276d669 100644 --- a/backend/src/test/java/kr/momo/domain/attendee/AttendeePasswordTest.java +++ b/backend/src/test/java/kr/momo/domain/attendee/AttendeePasswordTest.java @@ -5,33 +5,41 @@ import kr.momo.exception.MomoException; import kr.momo.exception.code.AttendeeErrorCode; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; class AttendeePasswordTest { - @DisplayName("참가자 비밀번호가 4글자를 초과하면 예외를 발생시킨다.") - @Test - void throwsExceptionIfAttendeePasswordIsTooLong() { - assertThatThrownBy(() -> new AttendeePassword("invalid_password_length_invalid_password_length")) - .isInstanceOf(MomoException.class) - .hasMessage(AttendeeErrorCode.INVALID_PASSWORD_FORMAT.message()); + private PasswordEncoder passwordEncoder; + + @BeforeEach + void setup() { + passwordEncoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); } - @DisplayName("참가자 비밀번호 객체가 정상 생성된다.") + @DisplayName("비밀번호와 동일한지 검증한다.") @Test - void createAttendeePasswordObjectSuccessfully() { + void verifyMatch() { + String given = "1234"; + AttendeeRawPassword rawPassword = new AttendeeRawPassword(given); + AttendeePassword password = rawPassword.encodePassword(passwordEncoder); + assertThatNoException() - .isThrownBy(() -> new AttendeePassword("1234")); + .isThrownBy(() -> password.verifyMatch(rawPassword, passwordEncoder)); } - @DisplayName("비밀번호가 서로 다르면 예외를 발생시킨다.") + @DisplayName("암호화된 비밀번호와 서로 다르면 예외를 발생시킨다.") @Test void throwsExceptionForMismatchedPasswords() { - AttendeePassword password = new AttendeePassword("1234"); - AttendeePassword other = new AttendeePassword("4321"); + String given = "1234"; + AttendeeRawPassword rawPassword = new AttendeeRawPassword(given); + AttendeeRawPassword other = new AttendeeRawPassword("4321"); + AttendeePassword password = rawPassword.encodePassword(passwordEncoder); - assertThatThrownBy(() -> password.verifyPassword(other)) + assertThatThrownBy(() -> password.verifyMatch(other, passwordEncoder)) .isInstanceOf(MomoException.class) .hasMessage(AttendeeErrorCode.PASSWORD_MISMATCHED.message()); } diff --git a/backend/src/test/java/kr/momo/domain/attendee/AttendeeRawPasswordTest.java b/backend/src/test/java/kr/momo/domain/attendee/AttendeeRawPasswordTest.java new file mode 100644 index 000000000..6809cbafa --- /dev/null +++ b/backend/src/test/java/kr/momo/domain/attendee/AttendeeRawPasswordTest.java @@ -0,0 +1,23 @@ +package kr.momo.domain.attendee; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.momo.exception.MomoException; +import kr.momo.exception.code.AttendeeErrorCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class AttendeeRawPasswordTest { + + @DisplayName("참가자 비밀번호가 숫자가 아니거나 4자를 초과하면 예외를 발생시킨다.") + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"invalid_password_length", "1@13", " ", "mo12"}) + void throwsExceptionIfAttendeePasswordIsTooLong(String given) { + assertThatThrownBy(() -> new AttendeeRawPassword(given)) + .isInstanceOf(MomoException.class) + .hasMessage(AttendeeErrorCode.INVALID_PASSWORD_FORMAT.message()); + } +} diff --git a/backend/src/test/java/kr/momo/domain/attendee/AttendeeTest.java b/backend/src/test/java/kr/momo/domain/attendee/AttendeeTest.java index 1c81e4af1..e985664a1 100644 --- a/backend/src/test/java/kr/momo/domain/attendee/AttendeeTest.java +++ b/backend/src/test/java/kr/momo/domain/attendee/AttendeeTest.java @@ -6,20 +6,32 @@ import kr.momo.domain.meeting.Meeting; import kr.momo.exception.MomoException; import kr.momo.exception.code.AttendeeErrorCode; +import kr.momo.fixture.AttendeeFixture; import kr.momo.fixture.MeetingFixture; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; class AttendeeTest { + private PasswordEncoder passwordEncoder; + + @BeforeEach + void setup() { + passwordEncoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); + } + @DisplayName("참가자의 비밀번호가 일치하지 않으면 예외를 발생시킨다.") @Test void throwsExceptionIfPasswordDoesNotMatch() { Meeting meeting = MeetingFixture.DINNER.create(); - Attendee attendee = new Attendee(meeting, "jazz", "1111", Role.GUEST); - AttendeePassword other = new AttendeePassword("1234"); + AttendeeFixture daon = AttendeeFixture.GUEST_DAON; + Attendee attendee = AttendeeFixture.HOST_JAZZ.create(meeting); + AttendeeRawPassword rawPassword = new AttendeeRawPassword(daon.getPassword()); - assertThatThrownBy(() -> attendee.verifyPassword(other)) + assertThatThrownBy(() -> attendee.verifyPassword(rawPassword, passwordEncoder)) .isInstanceOf(MomoException.class) .hasMessage(AttendeeErrorCode.PASSWORD_MISMATCHED.message()); } @@ -27,11 +39,12 @@ void throwsExceptionIfPasswordDoesNotMatch() { @DisplayName("참가자의 비밀번호가 일치하면 정상 기능한다.") @Test void doesNotThrowExceptionIfPasswordMatches() { + String given = "1234"; + AttendeeRawPassword rawPassword = new AttendeeRawPassword(given); Meeting meeting = MeetingFixture.DINNER.create(); - Attendee attendee = new Attendee(meeting, "jazz", "1111", Role.GUEST); - AttendeePassword other = new AttendeePassword("1111"); + Attendee attendee = AttendeeFixture.HOST_JAZZ.create(meeting); assertThatNoException() - .isThrownBy(() -> attendee.verifyPassword(other)); + .isThrownBy(() -> attendee.verifyPassword(rawPassword, passwordEncoder)); } } diff --git a/backend/src/test/java/kr/momo/fixture/AttendeeFixture.java b/backend/src/test/java/kr/momo/fixture/AttendeeFixture.java index e5983fc49..d7de5380e 100644 --- a/backend/src/test/java/kr/momo/fixture/AttendeeFixture.java +++ b/backend/src/test/java/kr/momo/fixture/AttendeeFixture.java @@ -5,6 +5,8 @@ import kr.momo.domain.attendee.AttendeePassword; import kr.momo.domain.attendee.Role; import kr.momo.domain.meeting.Meeting; +import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; public enum AttendeeFixture { @@ -14,6 +16,8 @@ public enum AttendeeFixture { GUEST_PEDRO("pedro", "4353", Role.GUEST), GUEST_MARK("mark", "1234", Role.GUEST); + private static final PasswordEncoder ENCODER = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); + private final String name; private final String password; private final Role role; @@ -25,7 +29,7 @@ public enum AttendeeFixture { } public Attendee create(Meeting meeting) { - return new Attendee(meeting, new AttendeeName(name), new AttendeePassword(password), role); + return new Attendee(meeting, new AttendeeName(name), new AttendeePassword(ENCODER.encode(password)), role); } public String getName() { diff --git a/backend/src/test/java/kr/momo/service/attendee/AttendeeServiceTest.java b/backend/src/test/java/kr/momo/service/attendee/AttendeeServiceTest.java index 83a4c1ff2..e68286eaa 100644 --- a/backend/src/test/java/kr/momo/service/attendee/AttendeeServiceTest.java +++ b/backend/src/test/java/kr/momo/service/attendee/AttendeeServiceTest.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.security.crypto.password.PasswordEncoder; @IsolateDatabase @SpringBootTest(webEnvironment = WebEnvironment.NONE) @@ -34,6 +35,9 @@ class AttendeeServiceTest { @Autowired private MeetingRepository meetingRepository; + @Autowired + private PasswordEncoder passwordEncoder; + private Meeting meeting; @BeforeEach @@ -44,8 +48,9 @@ void setUp() { @DisplayName("로그인 시 올바르지 않은 uuid로 접근할 경우 예외를 발생시킨다.") @Test void loginThrowsExceptionForInvalidUuid() { - Attendee attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); - AttendeeLoginRequest request = new AttendeeLoginRequest(attendee.name(), attendee.password()); + AttendeeFixture jazz = AttendeeFixture.HOST_JAZZ; + Attendee attendee = attendeeRepository.save(jazz.create(meeting)); + AttendeeLoginRequest request = new AttendeeLoginRequest(attendee.name(), jazz.getPassword()); assertThatThrownBy(() -> attendeeService.login("invalidUUID", request)) .isInstanceOf(MomoException.class) @@ -67,8 +72,9 @@ void createsNewAttendeeIfNameIsNotAlreadyExists() { @DisplayName("로그인 시 동일한 이름이 저장되어 있으면 새로 참가자를 생성하지 않는다.") @Test void doesNotCreateAttendeeIfNameAlreadyExists() { - Attendee attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); - AttendeeLoginRequest request = new AttendeeLoginRequest(attendee.name(), attendee.password()); + AttendeeFixture jazz = AttendeeFixture.HOST_JAZZ; + Attendee attendee = attendeeRepository.save(jazz.create(meeting)); + AttendeeLoginRequest request = new AttendeeLoginRequest(attendee.name(), jazz.getPassword()); attendeeService.login(meeting.getUuid(), request); long initialCount = attendeeRepository.count(); diff --git a/backend/src/test/java/kr/momo/service/meeting/MeetingConfirmServiceTest.java b/backend/src/test/java/kr/momo/service/meeting/MeetingConfirmServiceTest.java index 9230f6bc6..dd8d5a960 100644 --- a/backend/src/test/java/kr/momo/service/meeting/MeetingConfirmServiceTest.java +++ b/backend/src/test/java/kr/momo/service/meeting/MeetingConfirmServiceTest.java @@ -70,8 +70,8 @@ void setUp() { meeting = MeetingFixture.MOVIE.create(); meeting.lock(); meeting = meetingRepository.save(meeting); - attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(this.meeting)); - today = availableDateRepository.save(new AvailableDate(LocalDate.now(), this.meeting)); + attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); + today = availableDateRepository.save(new AvailableDate(LocalDate.now(), meeting)); validRequest = new MeetingConfirmRequest( today.getDate(), meeting.earliestTime(), @@ -218,7 +218,8 @@ void findByUuid() { LocalTime.of(1, 30) ); - MeetingConfirmResponse confirmed = meetingConfirmService.create(meeting.getUuid(), attendee.getId(), validRequest); + MeetingConfirmResponse confirmed = meetingConfirmService.create(meeting.getUuid(), attendee.getId(), + validRequest); ConfirmedMeetingResponse response = meetingConfirmService.findByUuid(meeting.getUuid()); assertAll( diff --git a/backend/src/test/java/kr/momo/service/schedule/ScheduleServiceTest.java b/backend/src/test/java/kr/momo/service/schedule/ScheduleServiceTest.java index da3e0f870..672a1039f 100644 --- a/backend/src/test/java/kr/momo/service/schedule/ScheduleServiceTest.java +++ b/backend/src/test/java/kr/momo/service/schedule/ScheduleServiceTest.java @@ -328,7 +328,6 @@ private List addSchedule( schedules.add(new Schedule(attendee1, date2, Timeslot.TIME_0500)); - // attendee2 schedules.add(new Schedule(attendee2, date1, Timeslot.TIME_0330)); schedules.add(new Schedule(attendee2, date1, Timeslot.TIME_0400)); @@ -343,7 +342,6 @@ private List addSchedule( schedules.add(new Schedule(attendee2, date2, Timeslot.TIME_0230)); schedules.add(new Schedule(attendee2, date2, Timeslot.TIME_0300)); - // attendee3 schedules.add(new Schedule(attendee3, date1, Timeslot.TIME_0130)); schedules.add(new Schedule(attendee3, date1, Timeslot.TIME_0200));