From 1291deabe9b728c86b945531180b5e298e45bbaa Mon Sep 17 00:00:00 2001 From: yunhacandy Date: Tue, 13 Aug 2024 15:32:15 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 충돌 방지를 위해 비밀번호 초기화 기능 관련 네이밍 변경 - 기존 비밀번호와 새로운 비밀번호 입력 시, 비밀번호가 수정되게 기능 구현 완료 --- .../auth/controller/AuthController.java | 23 +++++++++++---- ...sponse.java => ResetPasswordResponse.java} | 2 +- .../growingpain/auth/service/AuthService.java | 29 +++++++++++++++++-- 3 files changed, 45 insertions(+), 9 deletions(-) rename src/main/java/cotato/growingpain/auth/dto/response/{ChangePasswordResponse.java => ResetPasswordResponse.java} (65%) diff --git a/src/main/java/cotato/growingpain/auth/controller/AuthController.java b/src/main/java/cotato/growingpain/auth/controller/AuthController.java index 4b70892..d8e25ae 100644 --- a/src/main/java/cotato/growingpain/auth/controller/AuthController.java +++ b/src/main/java/cotato/growingpain/auth/controller/AuthController.java @@ -1,9 +1,11 @@ package cotato.growingpain.auth.controller; +import cotato.growingpain.auth.dto.request.ChangePasswordRequest; import cotato.growingpain.auth.dto.request.CompleteSignupRequest; import cotato.growingpain.auth.dto.request.LoginRequest; import cotato.growingpain.auth.dto.request.LogoutRequest; -import cotato.growingpain.auth.dto.response.ChangePasswordResponse; +import cotato.growingpain.auth.dto.request.ResetPasswordRequest; +import cotato.growingpain.auth.dto.response.ResetPasswordResponse; import cotato.growingpain.auth.service.AuthService; import cotato.growingpain.common.Response; import cotato.growingpain.security.jwt.Token; @@ -19,6 +21,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; @@ -74,11 +77,21 @@ public Response logout(@RequestBody LogoutRequest request) { } @Operation(summary = "비밀번호 초기화", description = "비밀번호 찾기 및 초기화를 위한 메소드") - @ApiResponse(content = @Content(schema = @Schema(implementation = ChangePasswordResponse.class))) - @PostMapping("/change-password") + @ApiResponse(content = @Content(schema = @Schema(implementation = ResetPasswordResponse.class))) + @PostMapping("/reset-password") @ResponseStatus(HttpStatus.OK) - public Response changePassword(@RequestBody cotato.growingpain.auth.dto.request.ChangePasswordRequest request){ + public Response resetPassword(@RequestBody ResetPasswordRequest request){ log.info("[비밀번호 초기화 컨트롤러]: {}", request.email()); - return Response.createSuccess("비밀번호 초기화 완료",authService.changePassword(request)); + return Response.createSuccess("비밀번호 초기화 완료",authService.resetPassword(request)); + } + + @Operation(summary = "비밀번호 변경", description = "로그인된 사용자가 비밀번호를 변경하기 위한 메소드") + @ApiResponse(content = @Content(schema = @Schema(implementation = Response.class))) + @PostMapping("/change-password") + @ResponseStatus(HttpStatus.OK) + public Response changePassword(@RequestBody @Valid ChangePasswordRequest request, + @AuthenticationPrincipal Long memberId) { + authService.changePassword(request, memberId); + return Response.createSuccessWithNoData("비밀번호 변경 완료"); } } \ No newline at end of file diff --git a/src/main/java/cotato/growingpain/auth/dto/response/ChangePasswordResponse.java b/src/main/java/cotato/growingpain/auth/dto/response/ResetPasswordResponse.java similarity index 65% rename from src/main/java/cotato/growingpain/auth/dto/response/ChangePasswordResponse.java rename to src/main/java/cotato/growingpain/auth/dto/response/ResetPasswordResponse.java index 950a76c..4d2bc94 100644 --- a/src/main/java/cotato/growingpain/auth/dto/response/ChangePasswordResponse.java +++ b/src/main/java/cotato/growingpain/auth/dto/response/ResetPasswordResponse.java @@ -1,6 +1,6 @@ package cotato.growingpain.auth.dto.response; -public record ChangePasswordResponse( +public record ResetPasswordResponse( String password ) { } \ No newline at end of file diff --git a/src/main/java/cotato/growingpain/auth/service/AuthService.java b/src/main/java/cotato/growingpain/auth/service/AuthService.java index f15fe16..794186e 100644 --- a/src/main/java/cotato/growingpain/auth/service/AuthService.java +++ b/src/main/java/cotato/growingpain/auth/service/AuthService.java @@ -5,7 +5,8 @@ import cotato.growingpain.auth.dto.request.CompleteSignupRequest; import cotato.growingpain.auth.dto.request.LoginRequest; import cotato.growingpain.auth.dto.request.LogoutRequest; -import cotato.growingpain.auth.dto.response.ChangePasswordResponse; +import cotato.growingpain.auth.dto.request.ResetPasswordRequest; +import cotato.growingpain.auth.dto.response.ResetPasswordResponse; import cotato.growingpain.auth.repository.BlackListRepository; import cotato.growingpain.common.exception.AppException; import cotato.growingpain.common.exception.ErrorCode; @@ -152,7 +153,7 @@ public void logout(LogoutRequest request) { } @Transactional - public ChangePasswordResponse changePassword(ChangePasswordRequest request){ + public ResetPasswordResponse resetPassword(ResetPasswordRequest request){ Member member = memberRepository.findByEmail(request.email()) .orElseThrow(() -> new AppException(ErrorCode.EMAIL_NOT_FOUND)); @@ -170,7 +171,29 @@ public ChangePasswordResponse changePassword(ChangePasswordRequest request){ log.info("비밀번호 업데이트 완료: {}", member.getEmail()); - return new ChangePasswordResponse(tempPassword); + return new ResetPasswordResponse(tempPassword); + } + + @Transactional + public void changePassword(ChangePasswordRequest request, Long memberId) { + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new AppException(ErrorCode.MEMBER_NOT_FOUND)); + + // 기존 비밀번호 검증 + if (!bCryptPasswordEncoder.matches(request.currentPassword(), member.getPassword())) { + throw new AppException(ErrorCode.INVALID_PASSWORD); + } + + // 새 비밀번호 패턴 검증 + validateService.checkPasswordPattern(request.newPassword()); + + // 새 비밀번호로 업데이트 + String encodedNewPassword = bCryptPasswordEncoder.encode(request.newPassword()); + member.updatePassword(encodedNewPassword); + memberRepository.save(member); + + log.info("비밀번호 번경 완료: {}", memberId); } private String generateTemporaryPassword() { From 2599f70604fe855bd90d6329cf8e5ec33cbfe83c Mon Sep 17 00:00:00 2001 From: yunhacandy Date: Tue, 13 Aug 2024 15:32:26 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 충돌 방지를 위해 비밀번호 초기화 기능 관련 네이밍 변경 - 기존 비밀번호와 새로운 비밀번호 입력 시, 비밀번호가 수정되게 기능 구현 완료 --- .../auth/dto/request/ChangePasswordRequest.java | 9 ++++++++- .../auth/dto/request/ResetPasswordRequest.java | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/main/java/cotato/growingpain/auth/dto/request/ResetPasswordRequest.java diff --git a/src/main/java/cotato/growingpain/auth/dto/request/ChangePasswordRequest.java b/src/main/java/cotato/growingpain/auth/dto/request/ChangePasswordRequest.java index f5df0d9..1436ddc 100644 --- a/src/main/java/cotato/growingpain/auth/dto/request/ChangePasswordRequest.java +++ b/src/main/java/cotato/growingpain/auth/dto/request/ChangePasswordRequest.java @@ -1,6 +1,13 @@ package cotato.growingpain.auth.dto.request; +import jakarta.validation.constraints.NotBlank; + public record ChangePasswordRequest( - String email + + @NotBlank(message = "기존 비밀번호는 필수 입력 항목입니다.") + String currentPassword, + + @NotBlank(message = "새 비밀번호는 필수 입력 항목입니다.") + String newPassword ) { } \ No newline at end of file diff --git a/src/main/java/cotato/growingpain/auth/dto/request/ResetPasswordRequest.java b/src/main/java/cotato/growingpain/auth/dto/request/ResetPasswordRequest.java new file mode 100644 index 0000000..a1e5297 --- /dev/null +++ b/src/main/java/cotato/growingpain/auth/dto/request/ResetPasswordRequest.java @@ -0,0 +1,6 @@ +package cotato.growingpain.auth.dto.request; + +public record ResetPasswordRequest( + String email +) { +} \ No newline at end of file From 2988ad7c19618b342968be1f7f213c4d40ad6b16 Mon Sep 17 00:00:00 2001 From: yunhacandy Date: Tue, 13 Aug 2024 16:18:12 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20Auth=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MemberRole이 Member일때 서비스 이용 가능하게 SecurityConfig 코드 수정 - AuthService에서 RefreshTokenEntity 관련 코드 삽입 및 수정 --- .../growingpain/auth/service/AuthService.java | 54 +++++++++++++------ .../growingpain/config/SecurityConfig.java | 4 +- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/main/java/cotato/growingpain/auth/service/AuthService.java b/src/main/java/cotato/growingpain/auth/service/AuthService.java index 794186e..b1b07fa 100644 --- a/src/main/java/cotato/growingpain/auth/service/AuthService.java +++ b/src/main/java/cotato/growingpain/auth/service/AuthService.java @@ -50,15 +50,19 @@ public Token createLoginInfo(LoginRequest request) { // 기존 회원이 존재하면 로그인 처리 Member member = existingMember.get(); if (!bCryptPasswordEncoder.matches(request.password(), member.getPassword())) { - throw new IllegalArgumentException("이메일 또는 비밀번호가 올바르지 않습니다."); + throw new AppException(ErrorCode.INVALID_PASSWORD); } - if (member.getMemberRole() == MemberRole.PENDING) { - return jwtTokenProvider.createToken(member.getId(), request.email(), MemberRole.PENDING.getDescription()); - } + String role = (member.getMemberRole() == MemberRole.PENDING) + ? MemberRole.PENDING.getDescription() + : MemberRole.MEMBER.getDescription(); + + Token token = jwtTokenProvider.createToken(member.getId(), member.getEmail(), role); - // 토큰 생성 및 반환 - return jwtTokenProvider.createToken(member.getId(), request.email(), MemberRole.MEMBER.getDescription()); + // RefreshTokenEntity 저장 또는 업데이트 + saveOrUpdateRefreshToken(member.getEmail(), token.getRefreshToken()); + + return token; } else { // 신규 회원일 경우 회원가입 처리 @@ -67,15 +71,19 @@ public Token createLoginInfo(LoginRequest request) { log.info("[회원 가입 서비스]: {}", request.email()); - Member newMember = Member.builder() + Member member = Member.builder() .password(bCryptPasswordEncoder.encode(request.password())) .email(request.email()) .memberRole(MemberRole.PENDING) .build(); - memberRepository.save(newMember); + memberRepository.save(member); // 회원가입 성공 후 토큰 생성 및 반환 - return jwtTokenProvider.createToken(newMember.getId(), request.email(),MemberRole.PENDING.getDescription()); + Token token = jwtTokenProvider.createToken(member.getId(), member.getEmail(), MemberRole.PENDING.getDescription()); + + saveOrUpdateRefreshToken(member.getEmail(), token.getRefreshToken()); + + return token; } } @@ -96,7 +104,12 @@ public Token completeSignup(CompleteSignupRequest request, String accessToken) { member.updateRole(MemberRole.MEMBER); memberRepository.save(member); - return jwtTokenProvider.createToken(member.getId(), member.getEmail(), MemberRole.MEMBER.getDescription()); + Token token = jwtTokenProvider.createToken(member.getId(), member.getEmail(), MemberRole.MEMBER.getDescription()); + + // RefreshTokenEntity 저장 + saveOrUpdateRefreshToken(member.getEmail(), token.getRefreshToken()); + + return token; } log.info("memberRole = {}", member.getMemberRole()); return null; @@ -127,25 +140,36 @@ public ReissueResponse tokenReissue(ReissueRequest request) { throw new AppException(ErrorCode.TOKEN_EXPIRED); } - if (!request.equals(request)) { + if (!findToken.getRefreshToken().equals(request.refreshToken())) { log.warn("[쿠키로 들어온 토큰과 DB의 토큰이 일치하지 않음.]"); throw new AppException(ErrorCode.REFRESH_TOKEN_NOT_EXIST); } Token token = jwtTokenProvider.createToken(memberId, email, role); - findToken.updateRefreshToken(token.getRefreshToken()); - refreshTokenRepository.save(findToken); log.info("재발급 된 액세스 토큰: {}", token.getAccessToken()); log.info("재발급 된 refresh 토큰: {}", token.getRefreshToken()); + + // RefreshTokenEntity 업데이트 + saveOrUpdateRefreshToken(email, token.getRefreshToken()); return ReissueResponse.from(token.getAccessToken(),token.getRefreshToken()); } + private void saveOrUpdateRefreshToken(String email, String refreshToken) { + RefreshTokenEntity refreshTokenEntity = refreshTokenRepository.findById(email) + .orElse(RefreshTokenEntity.builder().email(email).build()); + + refreshTokenEntity.updateRefreshToken(refreshToken); + refreshTokenRepository.save(refreshTokenEntity); + } + @Transactional public void logout(LogoutRequest request) { - String memberId = jwtTokenProvider.getEmail(request.refreshToken()); - RefreshTokenEntity existRefreshToken = refreshTokenRepository.findById(memberId) + String email = jwtTokenProvider.getEmail(request.refreshToken()); + + RefreshTokenEntity existRefreshToken = refreshTokenRepository.findById(email) .orElseThrow(() -> new AppException(ErrorCode.REFRESH_TOKEN_NOT_EXIST)); + setBlackList(request.refreshToken()); log.info("[로그아웃 된 리프레시 토큰 블랙리스트 처리]"); refreshTokenRepository.delete(existRefreshToken); diff --git a/src/main/java/cotato/growingpain/config/SecurityConfig.java b/src/main/java/cotato/growingpain/config/SecurityConfig.java index 750ce29..c37414a 100644 --- a/src/main/java/cotato/growingpain/config/SecurityConfig.java +++ b/src/main/java/cotato/growingpain/config/SecurityConfig.java @@ -7,7 +7,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -59,8 +58,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { UsernamePasswordAuthenticationFilter.class) .authorizeHttpRequests(request -> request .requestMatchers(WHITE_LIST).permitAll() - .requestMatchers(HttpMethod.GET, REQUIRED_AUTHENTICATE).authenticated() - .requestMatchers(HttpMethod.GET).permitAll() + .requestMatchers(REQUIRED_AUTHENTICATE).hasAuthority("ROLE_MEMBER") .anyRequest().authenticated() ); return http.build();