improvement: 인증 보안 정책 — Rate Limit + 시도제한 + 보안 로깅 (#190)
- auth_sensitive 명명 Rate Limit 정책 추가 (20회/15분/IP) - AuthController 3개 + PasswordController 2개 메서드에 EnableRateLimiting 적용 - 로그인 시도 제한 구현 (5회/15분, Redis 카운터, LoginAttemptExceeded 에러코드 활성화) - 비밀번호 찾기/임시 비밀번호 요청 제한 (3회/1시간, silent 반환) - AuthService 보안 이벤트 구조적 로깅 (ILogger 주입) - Swagger 429 응답 문서화 Closes #190
This commit is contained in:
parent
09831ebcbf
commit
42aa04f58e
|
|
@ -1,5 +1,6 @@
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
using SPMS.Application.DTOs.Auth;
|
using SPMS.Application.DTOs.Auth;
|
||||||
using SPMS.Application.Interfaces;
|
using SPMS.Application.Interfaces;
|
||||||
|
|
@ -49,13 +50,15 @@ public class AuthController : ControllerBase
|
||||||
|
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
|
[EnableRateLimiting("auth_sensitive")]
|
||||||
[SwaggerOperation(
|
[SwaggerOperation(
|
||||||
Summary = "관리자 로그인",
|
Summary = "관리자 로그인",
|
||||||
Description = "이메일과 비밀번호로 로그인하여 JWT 토큰을 발급받습니다. " +
|
Description = "이메일과 비밀번호로 로그인하여 JWT 토큰을 발급받습니다. " +
|
||||||
"응답의 nextAction으로 화면 분기: GO_DASHBOARD(대시보드), VERIFY_EMAIL(이메일 인증 필요). " +
|
"응답의 nextAction으로 화면 분기: GO_DASHBOARD(대시보드), VERIFY_EMAIL(이메일 인증 필요), CHANGE_PASSWORD(비밀번호 강제변경). " +
|
||||||
"미인증 유저는 verifySessionId와 emailSent가 함께 반환됩니다.")]
|
"5회 연속 실패 시 15분간 로그인이 차단됩니다.")]
|
||||||
[SwaggerResponse(200, "로그인 성공", typeof(ApiResponse<LoginResponseDto>))]
|
[SwaggerResponse(200, "로그인 성공", typeof(ApiResponse<LoginResponseDto>))]
|
||||||
[SwaggerResponse(401, "로그인 실패")]
|
[SwaggerResponse(401, "로그인 실패")]
|
||||||
|
[SwaggerResponse(429, "로그인 시도 횟수 초과")]
|
||||||
public async Task<IActionResult> LoginAsync([FromBody] LoginRequestDto request)
|
public async Task<IActionResult> LoginAsync([FromBody] LoginRequestDto request)
|
||||||
{
|
{
|
||||||
var result = await _authService.LoginAsync(request);
|
var result = await _authService.LoginAsync(request);
|
||||||
|
|
@ -97,6 +100,7 @@ public class AuthController : ControllerBase
|
||||||
|
|
||||||
[HttpPost("email/verify")]
|
[HttpPost("email/verify")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
|
[EnableRateLimiting("auth_sensitive")]
|
||||||
[SwaggerOperation(
|
[SwaggerOperation(
|
||||||
Summary = "이메일 인증",
|
Summary = "이메일 인증",
|
||||||
Description = "인증 코드로 이메일을 인증합니다. verify_session_id(권장) 또는 email로 대상을 지정합니다. 5회 실패 시 30분간 차단됩니다.")]
|
Description = "인증 코드로 이메일을 인증합니다. verify_session_id(권장) 또는 email로 대상을 지정합니다. 5회 실패 시 30분간 차단됩니다.")]
|
||||||
|
|
@ -111,6 +115,7 @@ public class AuthController : ControllerBase
|
||||||
|
|
||||||
[HttpPost("email/verify/resend")]
|
[HttpPost("email/verify/resend")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
|
[EnableRateLimiting("auth_sensitive")]
|
||||||
[SwaggerOperation(
|
[SwaggerOperation(
|
||||||
Summary = "이메일 인증코드 재전송",
|
Summary = "이메일 인증코드 재전송",
|
||||||
Description = "인증 세션 ID로 인증코드를 재전송합니다. 재전송 간 60초 쿨다운이 적용되며, 인증코드 유효시간은 5분입니다.")]
|
Description = "인증 세션 ID로 인증코드를 재전송합니다. 재전송 간 60초 쿨다운이 적용되며, 인증코드 유효시간은 5분입니다.")]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
using SPMS.Application.DTOs.Account;
|
using SPMS.Application.DTOs.Account;
|
||||||
using SPMS.Application.Interfaces;
|
using SPMS.Application.Interfaces;
|
||||||
|
|
@ -21,6 +22,7 @@ public class PasswordController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("forgot")]
|
[HttpPost("forgot")]
|
||||||
|
[EnableRateLimiting("auth_sensitive")]
|
||||||
[SwaggerOperation(
|
[SwaggerOperation(
|
||||||
Summary = "비밀번호 찾기",
|
Summary = "비밀번호 찾기",
|
||||||
Description = "등록된 이메일로 비밀번호 재설정 토큰을 발송합니다. 보안을 위해 이메일 존재 여부와 관계없이 동일한 응답을 반환합니다.")]
|
Description = "등록된 이메일로 비밀번호 재설정 토큰을 발송합니다. 보안을 위해 이메일 존재 여부와 관계없이 동일한 응답을 반환합니다.")]
|
||||||
|
|
@ -45,6 +47,7 @@ public class PasswordController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("temp")]
|
[HttpPost("temp")]
|
||||||
|
[EnableRateLimiting("auth_sensitive")]
|
||||||
[SwaggerOperation(
|
[SwaggerOperation(
|
||||||
Summary = "임시 비밀번호 발급",
|
Summary = "임시 비밀번호 발급",
|
||||||
Description = "등록된 이메일로 임시 비밀번호를 발송합니다. 보안을 위해 이메일 존재 여부와 관계없이 동일한 응답을 반환합니다.")]
|
Description = "등록된 이메일로 임시 비밀번호를 발송합니다. 보안을 위해 이메일 존재 여부와 관계없이 동일한 응답을 반환합니다.")]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Threading.RateLimiting;
|
using System.Threading.RateLimiting;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using SPMS.API.Extensions;
|
using SPMS.API.Extensions;
|
||||||
using SPMS.Application;
|
using SPMS.Application;
|
||||||
|
|
@ -55,6 +56,11 @@ builder.Services.AddRateLimiter(options =>
|
||||||
ErrorCodes.LimitExceeded, "요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.");
|
ErrorCodes.LimitExceeded, "요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.");
|
||||||
await context.HttpContext.Response.WriteAsJsonAsync(response, cancellationToken);
|
await context.HttpContext.Response.WriteAsJsonAsync(response, cancellationToken);
|
||||||
};
|
};
|
||||||
|
options.AddFixedWindowLimiter("auth_sensitive", opt =>
|
||||||
|
{
|
||||||
|
opt.PermitLimit = 20;
|
||||||
|
opt.Window = TimeSpan.FromMinutes(15);
|
||||||
|
});
|
||||||
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
|
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
|
||||||
RateLimitPartition.GetFixedWindowLimiter(
|
RateLimitPartition.GetFixedWindowLimiter(
|
||||||
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using SPMS.Application.DTOs.Account;
|
using SPMS.Application.DTOs.Account;
|
||||||
using SPMS.Application.DTOs.Auth;
|
using SPMS.Application.DTOs.Auth;
|
||||||
|
|
@ -20,6 +21,7 @@ public class AuthService : IAuthService
|
||||||
private readonly JwtSettings _jwtSettings;
|
private readonly JwtSettings _jwtSettings;
|
||||||
private readonly ITokenStore _tokenStore;
|
private readonly ITokenStore _tokenStore;
|
||||||
private readonly IEmailService _emailService;
|
private readonly IEmailService _emailService;
|
||||||
|
private readonly ILogger<AuthService> _logger;
|
||||||
|
|
||||||
public AuthService(
|
public AuthService(
|
||||||
IAdminRepository adminRepository,
|
IAdminRepository adminRepository,
|
||||||
|
|
@ -27,7 +29,8 @@ public class AuthService : IAuthService
|
||||||
IJwtService jwtService,
|
IJwtService jwtService,
|
||||||
IOptions<JwtSettings> jwtSettings,
|
IOptions<JwtSettings> jwtSettings,
|
||||||
ITokenStore tokenStore,
|
ITokenStore tokenStore,
|
||||||
IEmailService emailService)
|
IEmailService emailService,
|
||||||
|
ILogger<AuthService> logger)
|
||||||
{
|
{
|
||||||
_adminRepository = adminRepository;
|
_adminRepository = adminRepository;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
|
|
@ -35,6 +38,7 @@ public class AuthService : IAuthService
|
||||||
_jwtSettings = jwtSettings.Value;
|
_jwtSettings = jwtSettings.Value;
|
||||||
_tokenStore = tokenStore;
|
_tokenStore = tokenStore;
|
||||||
_emailService = emailService;
|
_emailService = emailService;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SignupResponseDto> SignupAsync(SignupRequestDto request)
|
public async Task<SignupResponseDto> SignupAsync(SignupRequestDto request)
|
||||||
|
|
@ -115,10 +119,22 @@ public class AuthService : IAuthService
|
||||||
|
|
||||||
public async Task<LoginResponseDto> LoginAsync(LoginRequestDto request)
|
public async Task<LoginResponseDto> LoginAsync(LoginRequestDto request)
|
||||||
{
|
{
|
||||||
|
// 0. 로그인 시도 횟수 확인 (5회/15분)
|
||||||
|
var loginAttemptsKey = $"login_attempts:{request.Email}";
|
||||||
|
var loginAttemptsStr = await _tokenStore.GetAsync(loginAttemptsKey);
|
||||||
|
var loginAttempts = int.TryParse(loginAttemptsStr, out var la) ? la : 0;
|
||||||
|
if (loginAttempts >= 5)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[AUTH] 로그인 잠금 — Email: {Email}", request.Email);
|
||||||
|
throw new SpmsException(ErrorCodes.LoginAttemptExceeded, "로그인 시도 횟수를 초과했습니다. 15분 후 다시 시도해주세요.", 429);
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 이메일로 관리자 조회
|
// 1. 이메일로 관리자 조회
|
||||||
var admin = await _adminRepository.GetByEmailAsync(request.Email);
|
var admin = await _adminRepository.GetByEmailAsync(request.Email);
|
||||||
if (admin is null)
|
if (admin is null)
|
||||||
{
|
{
|
||||||
|
await _tokenStore.StoreAsync(loginAttemptsKey, (loginAttempts + 1).ToString(), TimeSpan.FromMinutes(15));
|
||||||
|
_logger.LogWarning("[AUTH] 로그인 실패 — Email: {Email}, Attempts: {Count}/5", request.Email, loginAttempts + 1);
|
||||||
throw new SpmsException(
|
throw new SpmsException(
|
||||||
ErrorCodes.LoginFailed,
|
ErrorCodes.LoginFailed,
|
||||||
"이메일 또는 비밀번호가 일치하지 않습니다.",
|
"이메일 또는 비밀번호가 일치하지 않습니다.",
|
||||||
|
|
@ -128,6 +144,8 @@ public class AuthService : IAuthService
|
||||||
// 2. 삭제된 계정 확인
|
// 2. 삭제된 계정 확인
|
||||||
if (admin.IsDeleted)
|
if (admin.IsDeleted)
|
||||||
{
|
{
|
||||||
|
await _tokenStore.StoreAsync(loginAttemptsKey, (loginAttempts + 1).ToString(), TimeSpan.FromMinutes(15));
|
||||||
|
_logger.LogWarning("[AUTH] 로그인 실패 — Email: {Email}, Attempts: {Count}/5", request.Email, loginAttempts + 1);
|
||||||
throw new SpmsException(
|
throw new SpmsException(
|
||||||
ErrorCodes.LoginFailed,
|
ErrorCodes.LoginFailed,
|
||||||
"이메일 또는 비밀번호가 일치하지 않습니다.",
|
"이메일 또는 비밀번호가 일치하지 않습니다.",
|
||||||
|
|
@ -137,12 +155,17 @@ public class AuthService : IAuthService
|
||||||
// 3. 비밀번호 검증 (BCrypt)
|
// 3. 비밀번호 검증 (BCrypt)
|
||||||
if (!BCrypt.Net.BCrypt.Verify(request.Password, admin.Password))
|
if (!BCrypt.Net.BCrypt.Verify(request.Password, admin.Password))
|
||||||
{
|
{
|
||||||
|
await _tokenStore.StoreAsync(loginAttemptsKey, (loginAttempts + 1).ToString(), TimeSpan.FromMinutes(15));
|
||||||
|
_logger.LogWarning("[AUTH] 로그인 실패 — Email: {Email}, Attempts: {Count}/5", request.Email, loginAttempts + 1);
|
||||||
throw new SpmsException(
|
throw new SpmsException(
|
||||||
ErrorCodes.LoginFailed,
|
ErrorCodes.LoginFailed,
|
||||||
"이메일 또는 비밀번호가 일치하지 않습니다.",
|
"이메일 또는 비밀번호가 일치하지 않습니다.",
|
||||||
401);
|
401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 로그인 성공 — 시도 카운터 초기화
|
||||||
|
await _tokenStore.RemoveAsync(loginAttemptsKey);
|
||||||
|
|
||||||
// 4. 토큰 생성
|
// 4. 토큰 생성
|
||||||
var accessToken = _jwtService.GenerateAccessToken(
|
var accessToken = _jwtService.GenerateAccessToken(
|
||||||
admin.Id,
|
admin.Id,
|
||||||
|
|
@ -198,6 +221,7 @@ public class AuthService : IAuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. 응답 반환
|
// 7. 응답 반환
|
||||||
|
_logger.LogInformation("[AUTH] 로그인 성공 — Email: {Email}", admin.Email);
|
||||||
return new LoginResponseDto
|
return new LoginResponseDto
|
||||||
{
|
{
|
||||||
AccessToken = accessToken,
|
AccessToken = accessToken,
|
||||||
|
|
@ -321,6 +345,7 @@ public class AuthService : IAuthService
|
||||||
|
|
||||||
_adminRepository.Update(admin);
|
_adminRepository.Update(admin);
|
||||||
await _unitOfWork.SaveChangesAsync();
|
await _unitOfWork.SaveChangesAsync();
|
||||||
|
_logger.LogInformation("[AUTH] 비밀번호 변경 — AdminId: {AdminId}", adminId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<EmailCheckResponseDto> CheckEmailAsync(EmailCheckRequestDto request)
|
public async Task<EmailCheckResponseDto> CheckEmailAsync(EmailCheckRequestDto request)
|
||||||
|
|
@ -357,7 +382,10 @@ public class AuthService : IAuthService
|
||||||
var attemptsStr = await _tokenStore.GetAsync(attemptsKey);
|
var attemptsStr = await _tokenStore.GetAsync(attemptsKey);
|
||||||
var attempts = int.TryParse(attemptsStr, out var a) ? a : 0;
|
var attempts = int.TryParse(attemptsStr, out var a) ? a : 0;
|
||||||
if (attempts >= 5)
|
if (attempts >= 5)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[AUTH] 인증 시도 초과 — Email: {Email}", email);
|
||||||
throw new SpmsException(ErrorCodes.VerifyAttemptExceeded, "인증 시도 횟수를 초과했습니다. 30분 후 다시 시도해주세요.", 429);
|
throw new SpmsException(ErrorCodes.VerifyAttemptExceeded, "인증 시도 횟수를 초과했습니다. 30분 후 다시 시도해주세요.", 429);
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 관리자 조회
|
// 3. 관리자 조회
|
||||||
var admin = await _adminRepository.GetByEmailAsync(email);
|
var admin = await _adminRepository.GetByEmailAsync(email);
|
||||||
|
|
@ -436,11 +464,24 @@ public class AuthService : IAuthService
|
||||||
|
|
||||||
public async Task ForgotPasswordAsync(PasswordForgotRequestDto request)
|
public async Task ForgotPasswordAsync(PasswordForgotRequestDto request)
|
||||||
{
|
{
|
||||||
|
// 요청 제한 확인 (3회/1시간)
|
||||||
|
var forgotAttemptsKey = $"forgot_attempts:{request.Email}";
|
||||||
|
var forgotAttemptsStr = await _tokenStore.GetAsync(forgotAttemptsKey);
|
||||||
|
var forgotAttempts = int.TryParse(forgotAttemptsStr, out var fa) ? fa : 0;
|
||||||
|
if (forgotAttempts >= 3)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[AUTH] 비밀번호 찾기 제한 — Email: {Email}", request.Email);
|
||||||
|
return; // 계정 존재 비노출 원칙 유지
|
||||||
|
}
|
||||||
|
|
||||||
var admin = await _adminRepository.GetByEmailAsync(request.Email);
|
var admin = await _adminRepository.GetByEmailAsync(request.Email);
|
||||||
|
|
||||||
// 보안: 이메일 존재 여부와 관계없이 동일한 성공 응답 반환
|
// 보안: 이메일 존재 여부와 관계없이 동일한 성공 응답 반환
|
||||||
if (admin is null)
|
if (admin is null)
|
||||||
|
{
|
||||||
|
await _tokenStore.StoreAsync(forgotAttemptsKey, (forgotAttempts + 1).ToString(), TimeSpan.FromHours(1));
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var resetToken = Guid.NewGuid().ToString("N");
|
var resetToken = Guid.NewGuid().ToString("N");
|
||||||
await _tokenStore.StoreAsync(
|
await _tokenStore.StoreAsync(
|
||||||
|
|
@ -448,6 +489,7 @@ public class AuthService : IAuthService
|
||||||
resetToken,
|
resetToken,
|
||||||
TimeSpan.FromMinutes(30));
|
TimeSpan.FromMinutes(30));
|
||||||
await _emailService.SendPasswordResetTokenAsync(request.Email, resetToken);
|
await _emailService.SendPasswordResetTokenAsync(request.Email, resetToken);
|
||||||
|
await _tokenStore.StoreAsync(forgotAttemptsKey, (forgotAttempts + 1).ToString(), TimeSpan.FromHours(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ResetPasswordAsync(PasswordResetRequestDto request)
|
public async Task ResetPasswordAsync(PasswordResetRequestDto request)
|
||||||
|
|
@ -479,14 +521,30 @@ public class AuthService : IAuthService
|
||||||
|
|
||||||
public async Task IssueTempPasswordAsync(TempPasswordRequestDto request)
|
public async Task IssueTempPasswordAsync(TempPasswordRequestDto request)
|
||||||
{
|
{
|
||||||
|
// 0. 요청 제한 확인 (3회/1시간)
|
||||||
|
var tempAttemptsKey = $"temp_password_attempts:{request.Email}";
|
||||||
|
var tempAttemptsStr = await _tokenStore.GetAsync(tempAttemptsKey);
|
||||||
|
var tempAttempts = int.TryParse(tempAttemptsStr, out var ta) ? ta : 0;
|
||||||
|
if (tempAttempts >= 3)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[AUTH] 임시 비밀번호 제한 — Email: {Email}", request.Email);
|
||||||
|
return; // 계정 존재 비노출 원칙 유지
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 이메일로 관리자 조회 (없으면 조용히 반환 — 계정 존재 비노출)
|
// 1. 이메일로 관리자 조회 (없으면 조용히 반환 — 계정 존재 비노출)
|
||||||
var admin = await _adminRepository.GetByEmailAsync(request.Email);
|
var admin = await _adminRepository.GetByEmailAsync(request.Email);
|
||||||
if (admin is null)
|
if (admin is null)
|
||||||
|
{
|
||||||
|
await _tokenStore.StoreAsync(tempAttemptsKey, (tempAttempts + 1).ToString(), TimeSpan.FromHours(1));
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 삭제된 계정이면 조용히 반환
|
// 2. 삭제된 계정이면 조용히 반환
|
||||||
if (admin.IsDeleted)
|
if (admin.IsDeleted)
|
||||||
|
{
|
||||||
|
await _tokenStore.StoreAsync(tempAttemptsKey, (tempAttempts + 1).ToString(), TimeSpan.FromHours(1));
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 임시 비밀번호 생성 (12자, 영대소+숫자+특수)
|
// 3. 임시 비밀번호 생성 (12자, 영대소+숫자+특수)
|
||||||
var tempPassword = GenerateTempPassword(12);
|
var tempPassword = GenerateTempPassword(12);
|
||||||
|
|
@ -500,6 +558,8 @@ public class AuthService : IAuthService
|
||||||
|
|
||||||
// 5. 메일 발송
|
// 5. 메일 발송
|
||||||
await _emailService.SendTempPasswordAsync(admin.Email, tempPassword);
|
await _emailService.SendTempPasswordAsync(admin.Email, tempPassword);
|
||||||
|
await _tokenStore.StoreAsync(tempAttemptsKey, (tempAttempts + 1).ToString(), TimeSpan.FromHours(1));
|
||||||
|
_logger.LogInformation("[AUTH] 임시 비밀번호 발급 — Email: {Email}", admin.Email);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GenerateTempPassword(int length)
|
private static string GenerateTempPassword(int length)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user