-
Notifications
You must be signed in to change notification settings - Fork 3
[feature] 관리자 계정의 비밀번호 변경 및 초기화한다 #722
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
94a214b
8b69506
5a0b7bb
16f4874
20dad7e
029435b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+96
to
+113
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 보안: 임시 비밀번호 응답 반환 시 캐시/로그 차단 필요 평문 비밀번호가 프록시/브라우저/서버 로그에 남을 수 있습니다. 최소한 응답 캐시 비활성화와 응답/요청 로깅 마스킹을 적용하세요. 장기적으로는 메일/SMS 전송으로 전환 권장. 패치 예시(캐시 금지 헤더): 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());
+ response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
+ response.setHeader("Pragma", "no-cache");
return Response.ok(tempPwdResponse);
}또한, 액세스 토큰 무효화 전략(토큰 버전, 블랙리스트, 짧은 TTL)도 병행 검토 바랍니다. 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @PostMapping("/find/club") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Operation(summary = "사용자 동아리 조회", description = "사용자의 동아리를 조회합니다.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @PreAuthorize("isAuthenticated()") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+80
to
+82
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 비밀번호 초기화 시 보안 후속처리 누락
예시 스니펫(참고): + private Date passwordChangedAt;
...
public void resetPassword(String encodedPassword) { //초기화된 비밀번호 업데이트
this.password = encodedPassword;
+ this.passwordChangedAt = new Date();
+ this.refreshToken = null;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| public void updateRefreshToken(RefreshToken refreshToken) { | ||||||||||||||||||||||||||||||
| this.refreshToken = refreshToken; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ){ } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)); | ||
|
|
||
|
Comment on lines
+131
to
+133
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent ❓ Verification inconclusive임시 비밀번호 길이 8 → 최소 16자로 상수/설정화하세요. 아래 diff로 생성 길이를 상수로 치환하세요: - TempPasswordResponse tempPwdResponse = new TempPasswordResponse(
- securePasswordGenerator.generate(8));
+ TempPasswordResponse tempPwdResponse = new TempPasswordResponse(
+ securePasswordGenerator.generate(TEMP_PASSWORD_LENGTH));그리고 클래스 상단에 상수 추가: // 클래스 필드 영역
private static final int TEMP_PASSWORD_LENGTH = 16; // 정책에 따라 16~20 권장추가로, 생성기가 대/소문자+숫자+기호를 모두 포함하도록 보장하는지 점검 바랍니다. 임시 비밀번호 길이 매직 넘버 제거 및 상수화 |
||
| //암호화 | ||
| user.resetPassword(passwordEncoder.encode(tempPwdResponse.tempPassword())); | ||
|
|
||
| user.updateRefreshToken(null); | ||
| userRepository.save(user); | ||
|
|
||
|
Comment on lines
+135
to
+139
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 비밀번호 재설정 시 기존 Access Token 무효화 전략 필요 원하시면 User 필드/필터/JWT 클레임 변경까지 포함한 패치안을 드리겠습니다. |
||
| return tempPwdResponse; | ||
| } | ||
|
|
||
|
Comment on lines
+126
to
+142
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent ❓ Verification inconclusive임시 비밀번호를 응답 본문으로 반환하는 것은 고위험 보안 이슈입니다.
원하시면 컨트롤러/필터 레벨 마스킹 및 헤더 설정 패치를 제안하겠습니다. 임시 비밀번호 평문 반환 차단
🤖 Prompt for AI Agents |
||
| 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); | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
쿠키 제거 시 Domain 정합성 확인
로그인 시 설정한
refresh_token의 Domain/Path와 동일해야 삭제됩니다. 현재 Domain 미설정 상태입니다. 발급부와 동일하게 맞추는지 확인 부탁드립니다.아래 스크립트로 발급부 설정을 점검하세요:
🏁 Script executed:
Length of output: 1706
🏁 Script executed:
Length of output: 795
refresh_token 삭제 쿠키에 domain 설정 추가
UserController.java의 로그아웃(63–67행) 및 비밀번호 재설정(104–110행) 삭제 로직에서 ResponseCookie 빌더에
.domain(".moadong.com")을 추가해 주세요.🤖 Prompt for AI Agents