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]
|
[Authorize]
|
||||||
[SwaggerOperation(
|
[SwaggerOperation(
|
||||||
Summary = "비밀번호 변경",
|
Summary = "비밀번호 변경",
|
||||||
Description = "현재 로그인된 관리자의 비밀번호를 변경합니다.")]
|
Description = "현재 로그인된 관리자의 비밀번호를 변경합니다. 변경 성공 시 모든 세션이 무효화되며 재로그인이 필요합니다. " +
|
||||||
[SwaggerResponse(200, "비밀번호 변경 성공")]
|
"비밀번호 정책: 8자 이상, 영문 대/소문자·숫자·특수문자 각 1자 이상, 현재 비밀번호와 동일 불가.")]
|
||||||
[SwaggerResponse(400, "현재 비밀번호 불일치")]
|
[SwaggerResponse(200, "비밀번호 변경 성공", typeof(ApiResponse<ChangePasswordResponseDto>))]
|
||||||
|
[SwaggerResponse(400, "현재 비밀번호 불일치 / 정책 위반 / 동일 비밀번호 재사용")]
|
||||||
[SwaggerResponse(401, "인증되지 않은 요청")]
|
[SwaggerResponse(401, "인증되지 않은 요청")]
|
||||||
public async Task<IActionResult> ChangePasswordAsync([FromBody] ChangePasswordRequestDto request)
|
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))
|
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
||||||
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
||||||
|
|
||||||
await _authService.ChangePasswordAsync(adminId, request);
|
var result = await _authService.ChangePasswordAsync(adminId, request);
|
||||||
return Ok(ApiResponse.Success());
|
return Ok(ApiResponse<ChangePasswordResponseDto>.Success(result));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,9 @@ public class ChangePasswordRequestDto
|
||||||
|
|
||||||
[Required(ErrorMessage = "새 비밀번호를 입력해주세요.")]
|
[Required(ErrorMessage = "새 비밀번호를 입력해주세요.")]
|
||||||
[MinLength(8, ErrorMessage = "비밀번호는 8자 이상이어야 합니다.")]
|
[MinLength(8, ErrorMessage = "비밀번호는 8자 이상이어야 합니다.")]
|
||||||
|
[MaxLength(64, ErrorMessage = "비밀번호는 64자 이하여야 합니다.")]
|
||||||
|
[RegularExpression(
|
||||||
|
@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':""\\|,.<>\/?]).{8,}$",
|
||||||
|
ErrorMessage = "비밀번호는 영문 대/소문자, 숫자, 특수문자를 각각 1자 이상 포함해야 합니다.")]
|
||||||
public string NewPassword { get; set; } = string.Empty;
|
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<LoginResponseDto> LoginAsync(LoginRequestDto request);
|
||||||
Task<TokenRefreshResponseDto> RefreshTokenAsync(TokenRefreshRequestDto request);
|
Task<TokenRefreshResponseDto> RefreshTokenAsync(TokenRefreshRequestDto request);
|
||||||
Task LogoutAsync(long adminId, string accessToken);
|
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<EmailCheckResponseDto> CheckEmailAsync(EmailCheckRequestDto request);
|
||||||
Task<EmailVerifyResponseDto> VerifyEmailAsync(EmailVerifyRequestDto request);
|
Task<EmailVerifyResponseDto> VerifyEmailAsync(EmailVerifyRequestDto request);
|
||||||
Task<EmailResendResponseDto> ResendVerificationAsync(EmailResendRequestDto 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. 관리자 조회
|
// 1. 관리자 조회
|
||||||
var admin = await _adminRepository.GetByIdAsync(adminId);
|
var admin = await _adminRepository.GetByIdAsync(adminId);
|
||||||
|
|
@ -336,19 +336,37 @@ public class AuthService : IAuthService
|
||||||
400);
|
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);
|
admin.Password = BCrypt.Net.BCrypt.HashPassword(request.NewPassword);
|
||||||
|
|
||||||
// 4. 강제변경 플래그 해제
|
// 5. 강제변경 플래그 해제
|
||||||
if (admin.MustChangePassword)
|
if (admin.MustChangePassword)
|
||||||
{
|
{
|
||||||
admin.MustChangePassword = false;
|
admin.MustChangePassword = false;
|
||||||
admin.TempPasswordIssuedAt = null;
|
admin.TempPasswordIssuedAt = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6. 세션 무효화 — Refresh Token 삭제
|
||||||
|
admin.RefreshToken = null;
|
||||||
|
admin.RefreshTokenExpiresAt = null;
|
||||||
|
|
||||||
_adminRepository.Update(admin);
|
_adminRepository.Update(admin);
|
||||||
await _unitOfWork.SaveChangesAsync();
|
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)
|
public async Task<EmailCheckResponseDto> CheckEmailAsync(EmailCheckRequestDto request)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ public static class ErrorCodes
|
||||||
// === Account (2) ===
|
// === Account (2) ===
|
||||||
public const string PasswordValidationFailed = "121";
|
public const string PasswordValidationFailed = "121";
|
||||||
public const string ResetTokenError = "122";
|
public const string ResetTokenError = "122";
|
||||||
|
public const string PasswordPolicyViolation = "123";
|
||||||
|
public const string PasswordReuseForbidden = "124";
|
||||||
|
|
||||||
// === Service (3) ===
|
// === Service (3) ===
|
||||||
public const string DecryptionFailed = "131";
|
public const string DecryptionFailed = "131";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user