diff --git a/backend/src/main/java/moadong/global/util/SecurePasswordGenerator.java b/backend/src/main/java/moadong/global/util/SecurePasswordGenerator.java new file mode 100644 index 000000000..16e35d73a --- /dev/null +++ b/backend/src/main/java/moadong/global/util/SecurePasswordGenerator.java @@ -0,0 +1,49 @@ +package moadong.global.util; + +import org.springframework.stereotype.Component; + +import java.security.SecureRandom; + +@Component +public class SecurePasswordGenerator { + + // 알파벳 대소문자와 특수문자 배열 + private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private static final String NUMBERS = "0123456789"; + private static final String SPECIAL = "!@#$%^"; + + private static final String ALL = ALPHABET + NUMBERS + SPECIAL; + + private final SecureRandom secureRandom = new SecureRandom(); + + //영어, 숫자, 특수문자 각각 최소 1개씩 + public String generate(int length) { + if (length < 8) { + throw new IllegalArgumentException("Length must be at least 8");} + + StringBuilder password = new StringBuilder(length); + + password.append(ALPHABET.charAt(secureRandom.nextInt(ALPHABET.length()))); + password.append(NUMBERS.charAt(secureRandom.nextInt(NUMBERS.length()))); + password.append(SPECIAL.charAt(secureRandom.nextInt(SPECIAL.length()))); + + for (int i = 3; i < length; i++) { + int index = secureRandom.nextInt(ALL.length()); + password.append(ALL.charAt(index)); + } + + //뒤섞기 + return shuffleString(password.toString(), secureRandom); + } + + private static String shuffleString(String password, SecureRandom secureRandom) { + char[] characters = password.toCharArray(); + for (int i = characters.length - 1; i > 0; i--) { + int j = secureRandom.nextInt(i + 1); + char tmp = characters[i]; + characters[i] = characters[j]; + characters[j] = tmp; + } + return new String(characters); + } +} diff --git a/backend/src/main/java/moadong/user/controller/UserController.java b/backend/src/main/java/moadong/user/controller/UserController.java index c2dd55de7..56c416442 100644 --- a/backend/src/main/java/moadong/user/controller/UserController.java +++ b/backend/src/main/java/moadong/user/controller/UserController.java @@ -6,7 +6,6 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; import moadong.global.payload.Response; -import moadong.global.util.JwtProvider; import moadong.user.annotation.CurrentUser; import moadong.user.payload.CustomUserDetails; import moadong.user.payload.request.UserLoginRequest; @@ -15,6 +14,7 @@ import moadong.user.payload.response.FindUserClubResponse; import moadong.user.payload.response.LoginResponse; import moadong.user.payload.response.RefreshResponse; +import moadong.user.payload.response.TempPasswordResponse; import moadong.user.service.UserCommandService; import moadong.user.view.UserSwaggerView; import org.springframework.http.ResponseCookie; @@ -93,6 +93,25 @@ public ResponseEntity update(@CurrentUser CustomUserDetails user, return Response.ok("success update"); } + @PostMapping("/reset") + @Operation(summary = "사용자 비밀번호 초기화", description = "사용자 비밀번호를 초기화합니다.") + @PreAuthorize("isAuthenticated()") + @SecurityRequirement(name = "BearerAuth") + public ResponseEntity reset(@CurrentUser CustomUserDetails user, + HttpServletResponse response) { + TempPasswordResponse tempPwdResponse = userCommandService.reset(user.getUserId()); + + ResponseCookie cookie = ResponseCookie.from("refresh_token", "") + .path("/") + .maxAge(0) + .httpOnly(true) + .sameSite("None") + .secure(true) + .build(); + response.addHeader("Set-Cookie", cookie.toString()); + return Response.ok(tempPwdResponse); + } + @PostMapping("/find/club") @Operation(summary = "사용자 동아리 조회", description = "사용자의 동아리를 조회합니다.") @PreAuthorize("isAuthenticated()") diff --git a/backend/src/main/java/moadong/user/entity/User.java b/backend/src/main/java/moadong/user/entity/User.java index a5f2ca22d..388bad2fc 100644 --- a/backend/src/main/java/moadong/user/entity/User.java +++ b/backend/src/main/java/moadong/user/entity/User.java @@ -74,9 +74,12 @@ public String getUsername() { } public void updateUserProfile(UserUpdateRequest userUpdateRequest) { - this.userId = userUpdateRequest.userId(); this.password = userUpdateRequest.password(); } + + public void resetPassword(String encodedPassword) { //초기화된 비밀번호 업데이트 + this.password = encodedPassword; + } public void updateRefreshToken(RefreshToken refreshToken) { this.refreshToken = refreshToken; } diff --git a/backend/src/main/java/moadong/user/payload/request/UserUpdateRequest.java b/backend/src/main/java/moadong/user/payload/request/UserUpdateRequest.java index f08a6fad5..a2212df4b 100644 --- a/backend/src/main/java/moadong/user/payload/request/UserUpdateRequest.java +++ b/backend/src/main/java/moadong/user/payload/request/UserUpdateRequest.java @@ -6,15 +6,12 @@ import org.springframework.security.crypto.password.PasswordEncoder; public record UserUpdateRequest( - @NotNull - @UserId - String userId, @NotNull @Password String password ) { public UserUpdateRequest encryptPassword(PasswordEncoder passwordEncoder){ - return new UserUpdateRequest(userId, passwordEncoder.encode(this.password)); + return new UserUpdateRequest(passwordEncoder.encode(this.password)); } } diff --git a/backend/src/main/java/moadong/user/payload/response/TempPasswordResponse.java b/backend/src/main/java/moadong/user/payload/response/TempPasswordResponse.java new file mode 100644 index 000000000..3fee60fed --- /dev/null +++ b/backend/src/main/java/moadong/user/payload/response/TempPasswordResponse.java @@ -0,0 +1,10 @@ +package moadong.user.payload.response; + +import jakarta.validation.constraints.NotNull; +import moadong.global.annotation.Password; + +public record TempPasswordResponse( + @NotNull + @Password + String tempPassword +){ } diff --git a/backend/src/main/java/moadong/user/service/UserCommandService.java b/backend/src/main/java/moadong/user/service/UserCommandService.java index adab85964..278af60b3 100644 --- a/backend/src/main/java/moadong/user/service/UserCommandService.java +++ b/backend/src/main/java/moadong/user/service/UserCommandService.java @@ -9,6 +9,7 @@ import moadong.global.exception.ErrorCode; import moadong.global.exception.RestApiException; import moadong.global.util.JwtProvider; +import moadong.global.util.SecurePasswordGenerator; import moadong.user.entity.RefreshToken; import moadong.user.entity.User; import moadong.user.payload.CustomUserDetails; @@ -17,6 +18,7 @@ import moadong.user.payload.request.UserUpdateRequest; import moadong.user.payload.response.LoginResponse; import moadong.user.payload.response.RefreshResponse; +import moadong.user.payload.response.TempPasswordResponse; import moadong.user.repository.UserRepository; import moadong.user.util.CookieMaker; import org.springframework.http.ResponseCookie; @@ -36,6 +38,7 @@ public class UserCommandService { private final PasswordEncoder passwordEncoder; private final ClubRepository clubRepository; private final CookieMaker cookieMaker; + private final SecurePasswordGenerator securePasswordGenerator; public User registerUser(UserRegisterRequest userRegisterRequest) { try { @@ -120,6 +123,23 @@ public void update(String userId, response.addHeader("Set-Cookie", cookie.toString()); } + public TempPasswordResponse reset(String userId) { + User user = userRepository.findUserByUserId(userId) + .orElseThrow(() -> new RestApiException(ErrorCode.USER_NOT_EXIST)); + + //랜덤 임시 비밀번호 생성 + TempPasswordResponse tempPwdResponse = new TempPasswordResponse( + securePasswordGenerator.generate(8)); + + //암호화 + user.resetPassword(passwordEncoder.encode(tempPwdResponse.tempPassword())); + + user.updateRefreshToken(null); + userRepository.save(user); + + return tempPwdResponse; + } + public String findClubIdByUserId(String userID) { Club club = clubRepository.findClubByUserId(userID) .orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND)); @@ -130,4 +150,5 @@ private void createClub(String userId) { Club club = new Club(userId); clubRepository.save(club); } + }