improvement: 비밀번호 변경 보안 정책 적용 (#251)
- 비밀번호 정책 서버 검증 강화 (영대/소문자, 숫자, 특수문자 조합, 8~64자) - 동일 비밀번호 재사용 금지 검증 추가 - 비밀번호 변경 후 세션 무효화 (Refresh Token 삭제) - ChangePasswordResponseDto 신규 (re_login_required 힌트) - 에러코드 추가 (PasswordPolicyViolation, PasswordReuseForbidden) - AuthController Swagger 문서 보강 Closes #251
This commit is contained in:
parent
335676a282
commit
f31964c92e
|
|
@ -132,9 +132,10 @@ public class AuthController : ControllerBase
|
|||
[Authorize]
|
||||
[SwaggerOperation(
|
||||
Summary = "비밀번호 변경",
|
||||
Description = "현재 로그인된 관리자의 비밀번호를 변경합니다.")]
|
||||
[SwaggerResponse(200, "비밀번호 변경 성공")]
|
||||
[SwaggerResponse(400, "현재 비밀번호 불일치")]
|
||||
Description = "현재 로그인된 관리자의 비밀번호를 변경합니다. 변경 성공 시 모든 세션이 무효화되며 재로그인이 필요합니다. " +
|
||||
"비밀번호 정책: 8자 이상, 영문 대/소문자·숫자·특수문자 각 1자 이상, 현재 비밀번호와 동일 불가.")]
|
||||
[SwaggerResponse(200, "비밀번호 변경 성공", typeof(ApiResponse<ChangePasswordResponseDto>))]
|
||||
[SwaggerResponse(400, "현재 비밀번호 불일치 / 정책 위반 / 동일 비밀번호 재사용")]
|
||||
[SwaggerResponse(401, "인증되지 않은 요청")]
|
||||
public async Task<IActionResult> ChangePasswordAsync([FromBody] ChangePasswordRequestDto request)
|
||||
{
|
||||
|
|
@ -142,7 +143,7 @@ public class AuthController : ControllerBase
|
|||
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
||||
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
||||
|
||||
await _authService.ChangePasswordAsync(adminId, request);
|
||||
return Ok(ApiResponse.Success());
|
||||
var result = await _authService.ChangePasswordAsync(adminId, request);
|
||||
return Ok(ApiResponse<ChangePasswordResponseDto>.Success(result));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,5 +9,9 @@ public class ChangePasswordRequestDto
|
|||
|
||||
[Required(ErrorMessage = "새 비밀번호를 입력해주세요.")]
|
||||
[MinLength(8, ErrorMessage = "비밀번호는 8자 이상이어야 합니다.")]
|
||||
[MaxLength(64, ErrorMessage = "비밀번호는 64자 이하여야 합니다.")]
|
||||
[RegularExpression(
|
||||
@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':""\\|,.<>\/?]).{8,}$",
|
||||
ErrorMessage = "비밀번호는 영문 대/소문자, 숫자, 특수문자를 각각 1자 이상 포함해야 합니다.")]
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
|
|||
9
SPMS.Application/DTOs/Auth/ChangePasswordResponseDto.cs
Normal file
9
SPMS.Application/DTOs/Auth/ChangePasswordResponseDto.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Auth;
|
||||
|
||||
public class ChangePasswordResponseDto
|
||||
{
|
||||
[JsonPropertyName("re_login_required")]
|
||||
public bool ReLoginRequired { get; set; }
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ public interface IAuthService
|
|||
Task<LoginResponseDto> LoginAsync(LoginRequestDto request);
|
||||
Task<TokenRefreshResponseDto> RefreshTokenAsync(TokenRefreshRequestDto request);
|
||||
Task LogoutAsync(long adminId, string accessToken);
|
||||
Task ChangePasswordAsync(long adminId, ChangePasswordRequestDto request);
|
||||
Task<ChangePasswordResponseDto> ChangePasswordAsync(long adminId, ChangePasswordRequestDto request);
|
||||
Task<EmailCheckResponseDto> CheckEmailAsync(EmailCheckRequestDto request);
|
||||
Task<EmailVerifyResponseDto> VerifyEmailAsync(EmailVerifyRequestDto request);
|
||||
Task<EmailResendResponseDto> ResendVerificationAsync(EmailResendRequestDto request);
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ public class AuthService : IAuthService
|
|||
}
|
||||
}
|
||||
|
||||
public async Task ChangePasswordAsync(long adminId, ChangePasswordRequestDto request)
|
||||
public async Task<ChangePasswordResponseDto> ChangePasswordAsync(long adminId, ChangePasswordRequestDto request)
|
||||
{
|
||||
// 1. 관리자 조회
|
||||
var admin = await _adminRepository.GetByIdAsync(adminId);
|
||||
|
|
@ -336,19 +336,37 @@ public class AuthService : IAuthService
|
|||
400);
|
||||
}
|
||||
|
||||
// 3. 새 비밀번호 해싱 및 저장
|
||||
// 3. 동일 비밀번호 재사용 금지
|
||||
if (BCrypt.Net.BCrypt.Verify(request.NewPassword, admin.Password))
|
||||
{
|
||||
throw new SpmsException(
|
||||
ErrorCodes.PasswordReuseForbidden,
|
||||
"현재 비밀번호와 동일한 비밀번호는 사용할 수 없습니다.",
|
||||
400);
|
||||
}
|
||||
|
||||
// 4. 새 비밀번호 해싱 및 저장
|
||||
admin.Password = BCrypt.Net.BCrypt.HashPassword(request.NewPassword);
|
||||
|
||||
// 4. 강제변경 플래그 해제
|
||||
// 5. 강제변경 플래그 해제
|
||||
if (admin.MustChangePassword)
|
||||
{
|
||||
admin.MustChangePassword = false;
|
||||
admin.TempPasswordIssuedAt = null;
|
||||
}
|
||||
|
||||
// 6. 세션 무효화 — Refresh Token 삭제
|
||||
admin.RefreshToken = null;
|
||||
admin.RefreshTokenExpiresAt = null;
|
||||
|
||||
_adminRepository.Update(admin);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
_logger.LogInformation("[AUTH] 비밀번호 변경 — AdminId: {AdminId}", adminId);
|
||||
_logger.LogInformation("[AUTH] 비밀번호 변경 + 세션 무효화 — AdminId: {AdminId}", adminId);
|
||||
|
||||
return new ChangePasswordResponseDto
|
||||
{
|
||||
ReLoginRequired = true
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<EmailCheckResponseDto> CheckEmailAsync(EmailCheckRequestDto request)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ public static class ErrorCodes
|
|||
// === Account (2) ===
|
||||
public const string PasswordValidationFailed = "121";
|
||||
public const string ResetTokenError = "122";
|
||||
public const string PasswordPolicyViolation = "123";
|
||||
public const string PasswordReuseForbidden = "124";
|
||||
|
||||
// === Service (3) ===
|
||||
public const string DecryptionFailed = "131";
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user