feat: 토큰 갱신 및 로그아웃 API 구현 (#38)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f037977102
commit
336dcf8193
|
|
@ -31,4 +31,36 @@ public class AuthController : ControllerBase
|
|||
var result = await _authService.LoginAsync(request);
|
||||
return Ok(ApiResponse<LoginResponseDto>.Success(result));
|
||||
}
|
||||
|
||||
[HttpPost("token/refresh")]
|
||||
[AllowAnonymous]
|
||||
[SwaggerOperation(
|
||||
Summary = "토큰 갱신",
|
||||
Description = "Refresh Token을 사용하여 새로운 Access Token과 Refresh Token을 발급받습니다.")]
|
||||
[SwaggerResponse(200, "토큰 갱신 성공", typeof(ApiResponse<TokenRefreshResponseDto>))]
|
||||
[SwaggerResponse(401, "유효하지 않거나 만료된 Refresh Token")]
|
||||
public async Task<IActionResult> RefreshTokenAsync([FromBody] TokenRefreshRequestDto request)
|
||||
{
|
||||
var result = await _authService.RefreshTokenAsync(request);
|
||||
return Ok(ApiResponse<TokenRefreshResponseDto>.Success(result));
|
||||
}
|
||||
|
||||
[HttpPost("logout")]
|
||||
[Authorize]
|
||||
[SwaggerOperation(
|
||||
Summary = "로그아웃",
|
||||
Description = "현재 로그인된 관리자의 Refresh Token을 무효화합니다.")]
|
||||
[SwaggerResponse(200, "로그아웃 성공")]
|
||||
[SwaggerResponse(401, "인증되지 않은 요청")]
|
||||
public async Task<IActionResult> LogoutAsync()
|
||||
{
|
||||
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
||||
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
||||
{
|
||||
return Unauthorized(ApiResponse<object>.Fail("101", "인증 정보가 유효하지 않습니다."));
|
||||
}
|
||||
|
||||
await _authService.LogoutAsync(adminId);
|
||||
return Ok(ApiResponse.Success());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
SPMS.Application/DTOs/Auth/TokenRefreshRequestDto.cs
Normal file
11
SPMS.Application/DTOs/Auth/TokenRefreshRequestDto.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Auth;
|
||||
|
||||
public class TokenRefreshRequestDto
|
||||
{
|
||||
[Required(ErrorMessage = "Refresh Token은 필수입니다.")]
|
||||
[JsonPropertyName("refresh_token")]
|
||||
public string RefreshToken { get; set; } = string.Empty;
|
||||
}
|
||||
15
SPMS.Application/DTOs/Auth/TokenRefreshResponseDto.cs
Normal file
15
SPMS.Application/DTOs/Auth/TokenRefreshResponseDto.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.Auth;
|
||||
|
||||
public class TokenRefreshResponseDto
|
||||
{
|
||||
[JsonPropertyName("access_token")]
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("refresh_token")]
|
||||
public string RefreshToken { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("expires_in")]
|
||||
public int ExpiresIn { get; set; }
|
||||
}
|
||||
|
|
@ -5,4 +5,6 @@ namespace SPMS.Application.Interfaces;
|
|||
public interface IAuthService
|
||||
{
|
||||
Task<LoginResponseDto> LoginAsync(LoginRequestDto request);
|
||||
Task<TokenRefreshResponseDto> RefreshTokenAsync(TokenRefreshRequestDto request);
|
||||
Task LogoutAsync(long adminId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,9 @@ public class AuthService : IAuthService
|
|||
admin.Role.ToString());
|
||||
var refreshToken = _jwtService.GenerateRefreshToken();
|
||||
|
||||
// 5. 최종 로그인 시간 업데이트
|
||||
// 5. Refresh Token 및 최종 로그인 시간 저장
|
||||
admin.RefreshToken = refreshToken;
|
||||
admin.RefreshTokenExpiresAt = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiryDays);
|
||||
admin.LastLoginAt = DateTime.UtcNow;
|
||||
_adminRepository.Update(admin);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
|
@ -83,4 +85,65 @@ public class AuthService : IAuthService
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<TokenRefreshResponseDto> RefreshTokenAsync(TokenRefreshRequestDto request)
|
||||
{
|
||||
// 1. Refresh Token으로 관리자 조회
|
||||
var admin = await _adminRepository.GetByRefreshTokenAsync(request.RefreshToken);
|
||||
if (admin is null)
|
||||
{
|
||||
throw new SpmsException(
|
||||
ErrorCodes.Unauthorized,
|
||||
"유효하지 않은 Refresh Token입니다.",
|
||||
401);
|
||||
}
|
||||
|
||||
// 2. Refresh Token 만료 확인
|
||||
if (admin.RefreshTokenExpiresAt < DateTime.UtcNow)
|
||||
{
|
||||
throw new SpmsException(
|
||||
ErrorCodes.Unauthorized,
|
||||
"Refresh Token이 만료되었습니다. 다시 로그인해주세요.",
|
||||
401);
|
||||
}
|
||||
|
||||
// 3. 새 토큰 생성
|
||||
var newAccessToken = _jwtService.GenerateAccessToken(
|
||||
admin.Id,
|
||||
admin.Role.ToString());
|
||||
var newRefreshToken = _jwtService.GenerateRefreshToken();
|
||||
|
||||
// 4. 새 Refresh Token 저장 (Rotation)
|
||||
admin.RefreshToken = newRefreshToken;
|
||||
admin.RefreshTokenExpiresAt = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiryDays);
|
||||
_adminRepository.Update(admin);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
// 5. 응답 반환
|
||||
return new TokenRefreshResponseDto
|
||||
{
|
||||
AccessToken = newAccessToken,
|
||||
RefreshToken = newRefreshToken,
|
||||
ExpiresIn = _jwtSettings.ExpiryMinutes * 60
|
||||
};
|
||||
}
|
||||
|
||||
public async Task LogoutAsync(long adminId)
|
||||
{
|
||||
// 1. 관리자 조회
|
||||
var admin = await _adminRepository.GetByIdAsync(adminId);
|
||||
if (admin is null)
|
||||
{
|
||||
throw new SpmsException(
|
||||
ErrorCodes.NotFound,
|
||||
"관리자를 찾을 수 없습니다.",
|
||||
404);
|
||||
}
|
||||
|
||||
// 2. Refresh Token 무효화
|
||||
admin.RefreshToken = null;
|
||||
admin.RefreshTokenExpiresAt = null;
|
||||
_adminRepository.Update(admin);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ public class Admin : BaseEntity
|
|||
public DateTime? EmailVerifiedAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? LastLoginAt { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
public DateTime? RefreshTokenExpiresAt { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,5 +6,6 @@ public interface IAdminRepository : IRepository<Admin>
|
|||
{
|
||||
Task<Admin?> GetByEmailAsync(string email);
|
||||
Task<Admin?> GetByAdminCodeAsync(string adminCode);
|
||||
Task<Admin?> GetByRefreshTokenAsync(string refreshToken);
|
||||
Task<bool> EmailExistsAsync(string email, long? excludeId = null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ public class AdminRepository : Repository<Admin>, IAdminRepository
|
|||
.FirstOrDefaultAsync(a => a.AdminCode == adminCode && !a.IsDeleted);
|
||||
}
|
||||
|
||||
public async Task<Admin?> GetByRefreshTokenAsync(string refreshToken)
|
||||
{
|
||||
return await _dbSet
|
||||
.FirstOrDefaultAsync(a => a.RefreshToken == refreshToken && !a.IsDeleted);
|
||||
}
|
||||
|
||||
public async Task<bool> EmailExistsAsync(string email, long? excludeId = null)
|
||||
{
|
||||
var query = _dbSet.Where(a => a.Email == email && !a.IsDeleted);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user