2023. 1. 19. 00:00ㆍASPNET/ASPNET 7
이제 UnitTest 를 만들었으니 기존 코드들에서 재사용 가능한 부분들을 refactoring 해보자
또한 design 적으로 수정할 부분이 있다면 이 부분들도 refactoring 해보자
UnitTest 를 만들기 전에는 이러한 수정들이 다른 코드들에 영향을 줄 수 있으므로 수정하기 꺼려진다.
Test 를 해보면 10 중 8~9 는 분명히 문제가 생길 것이고 미쳐 생각하지 못한 부분들에서
알수 없는 오류들이 나올 수 있다. UnitTest 는 이러한 문제들을 찾는데 도움이 될 것이다.
ApiServerContext 와 IConfiguration 은 대부분의 Handler 에서 사용하게 될것 같으니 공통 class 에 넣자
또한 apiLogger 도 추가하자. 향후에 사용할 가능성이 많다.
Features/Shared/AbstractBaseHandler.cs
using ApiServer.Shared.Interfaces;
using Infrastructure;
namespace ApiServer.Features.Shared;
public abstract class AbstractBaseHandler
{
protected readonly ApiServerContext _context;
protected readonly IConfiguration _configuration;
protected readonly IApiLogger _apiLogger;
public AbstractBaseHandler(ApiServerContext context, IConfiguration configuration, IApiLogger apiLogger)
{
_context = context;
_configuration = configuration;
_apiLogger = apiLogger;
}
}
이제 각각의 Handler 들을 AbstractBaseHandler 를 상속하도록 수정하자.
Features/User/Register.cs
using ApiServer.Features.Shared;
using ApiServer.Helper;
using ApiServer.Shared.Interfaces;
using Infrastructure;
using MediatR;
namespace ApiServer.Features.User;
public class Register
{
...
public class CommandHandler : AbstractBaseHandler, IRequestHandler<Command, Response>
{
public CommandHandler(ApiServerContext context, IConfiguration configuration, IApiLogger apiLogger) : base(context, configuration, apiLogger)
{
}
...
}
}
Features/User/Login.cs
using ApiServer.Features.Shared;
using ApiServer.Shared.Interfaces;
using ApiServer.Shared.Models;
using Infrastructure;
using MediatR;
namespace ApiServer.Features.User;
public class Login
{
...
public class CommandHandler : AbstractBaseHandler, IRequestHandler<Command, Response>
{
private readonly ITokenGenerator _tokenGenerator;
// ITokenGenerator 를 Constructor injection
public CommandHandler(ITokenGenerator tokenGenerator, ApiServerContext context, IConfiguration configuration, IApiLogger apiLogger)
: base(context, configuration, apiLogger)
=> _tokenGenerator = tokenGenerator;
...
}
}
Features/User/Logout.cs
using ApiServer.Features.Shared;
using ApiServer.Shared.Interfaces;
using Infrastructure;
using MediatR;
namespace ApiServer.Features.User;
public class Logout
{
...
public class CommandHandler : AbstractBaseHandler, IRequestHandler<Command, Response>
{
private readonly ITokenGenerator _tokenGenerator;
// ITokenGenerator 를 Constructor injection
public CommandHandler(ITokenGenerator tokenGenerator, ApiServerContext context, IConfiguration configuration, IApiLogger apiLogger)
: base(context, configuration, apiLogger)
{
_tokenGenerator = tokenGenerator;
}
...
}
}
Features/User/Refresh.cs
using ApiServer.Features.Shared;
using ApiServer.Shared.Interfaces;
using ApiServer.Shared.Models;
using Infrastructure;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace ApiServer.Features.User;
public class Refresh
{
...
public class CommandHandler : AbstractBaseHandler, IRequestHandler<Command, Response>
{
private readonly ITokenGenerator _tokenGenerator;
public CommandHandler(ITokenGenerator tokenGenerator, ApiServerContext context, IConfiguration configuration, IApiLogger apiLogger)
: base(context, configuration, apiLogger)
=> _tokenGenerator = tokenGenerator;
...
}
}
이제 각 Feature 들의 Response 를 확인해 보자
모두 bool Result 와 Error? Error 을 가지고 있다. (중복은 제거하자!!) Refactoring 해보자
Features/Shared/BaseResponse.cs
namespace ApiServer.Features.Shared;
public abstract class BaseResponse
{
public bool Result { get; set; }
public Error? Error { get; set; }
}
public class Error
{
public string Code { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
Features/User/Register.cs
...
public class Response:BaseResponse
{
}
...
// Error class 삭제
Features/User/Login.cs
...
public class Response:BaseResponse
{
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
}
...
// Error class 삭제
Features/User/Logout.cs
...
public class Response:BaseResponse
{
}
...
// Error class 삭제
Features/User/Refresh.cs
...
public class Response:BaseResponse
{
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
}
...
// Error class 삭제
이제 각 handler 들에 있는 db 관련 method 들을 확인한 후 일반화가 가능하다면 refactoring 해보겠다.
Refresh 만 refactoring 해보겠다. 다음은 refactoring 을 거친 코드이다.
Features/Shared/AbstractBaseHandler.cs
using ApiServer.Shared.Interfaces;
using Infrastructure;
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
namespace ApiServer.Features.Shared;
public abstract class AbstractBaseHandler
{
protected readonly ApiServerContext _context;
protected readonly IConfiguration _configuration;
protected readonly IApiLogger _apiLogger;
public AbstractBaseHandler(ApiServerContext context, IConfiguration configuration, IApiLogger apiLogger)
{
_context = context;
_configuration = configuration;
_apiLogger = apiLogger;
}
protected Infrastructure.Models.Login? GetLogin(Expression<Func<Infrastructure.Models.Login, bool>> predicate)
=> _context.Logins?.Where(predicate).Include(l => l.User).SingleOrDefault();
protected void AddLogin(string id, string accessToken, string refreshToken, int refreshExpirationMinute)
{
var refreshTokenExpired = DateTime.UtcNow.AddMinutes(refreshExpirationMinute);
var findUser = _context.Users.Where(u => u.Id == id).SingleOrDefault();
if (findUser == null) throw new Exception("Can not find user");
var login = new Infrastructure.Models.Login
{
User = findUser,
UseFlag = true,
AccessToken = accessToken,
RefreshToken = refreshToken,
RefreshTokenExpired = refreshTokenExpired
};
_context.Logins.Add(login);
}
}
Features/User/Refresh.cs
using ApiServer.Features.Shared;
using ApiServer.Shared.Interfaces;
using ApiServer.Shared.Models;
using Infrastructure;
using MediatR;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace ApiServer.Features.User;
public class Refresh
{
public class Command : IRequest<Response>
{
public string AccessToken { get; set; } = string.Empty;
public string RefreshToken { get; set; } = string.Empty;
}
public class Response : BaseResponse
{
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
}
public class CommandHandler : AbstractBaseHandler, IRequestHandler<Command, Response>
{
private readonly ITokenGenerator _tokenGenerator;
public CommandHandler(ITokenGenerator tokenGenerator,
ApiServerContext context,
IConfiguration configuration,
IApiLogger apiLogger)
: base(context, configuration, apiLogger)
=> _tokenGenerator = tokenGenerator;
public async Task<Response> Handle(Command request, CancellationToken cancellationToken)
{
var response = new Response { Result = false };
var tokenPayload = _configuration.GetSection("TokenManagement").Get<TokenManagement>();
if (tokenPayload == null) throw new NullReferenceException(nameof(tokenPayload));
ClaimsPrincipal principal = ValidateToken(request.AccessToken,
tokenPayload.Audience,
tokenPayload.Issuer,
tokenPayload.Secret);
var login = GetLoginInfo(request.AccessToken, request.RefreshToken, principal);
// expiring
login.UseFlag = false;
var (AccessToken, RefreshToken) = ReCreateToken(login.User.Id);
var refreshExpirationMinute = tokenPayload?.RefreshExpiration ?? 1440;
AddLogin(login.User.Id, AccessToken, RefreshToken, refreshExpirationMinute);
await _context.SaveChangesAsync(cancellationToken);
response.Result = true;
response.AccessToken = AccessToken;
response.RefreshToken = RefreshToken;
return response;
}
private Infrastructure.Models.Login GetLoginInfo
(string accessToken, string refreshToken, ClaimsPrincipal principal)
{
var identity = (principal.Identity as ClaimsIdentity);
var nameClaim = identity?.
Claims.
SingleOrDefault(c => c.Type == "Name");
var login = GetLogin(l => l.AccessToken == accessToken
&& l.RefreshToken == refreshToken
&& l.RefreshTokenExpired > DateTime.UtcNow);
if (login == null) throw new Exception("Invalid refreshToken");
if (!login.User.Id.Equals(nameClaim?.Value, StringComparison.OrdinalIgnoreCase))
throw new Exception("Invalid refreshToken");
return login;
}
private static ClaimsPrincipal ValidateToken
(string accessToken, string audience, string issuer, string secretKey)
{
var handler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidAudience = audience,
ValidIssuer = issuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey)),
ValidateLifetime = false
};
return handler.ValidateToken(accessToken, validationParameters, out _);
}
private (string AccessToken, string RefreshToken) ReCreateToken(string id)
{
// expiring
var newAccessToken = _tokenGenerator.Create(id);
var newRefreshToken = _tokenGenerator.CreateRefreshToken();
return (newAccessToken, newRefreshToken);
}
}
}
관련영상
'ASPNET > ASPNET 7' 카테고리의 다른 글
인증서버 - API Key Management (3) | 2023.08.07 |
---|---|
인증서버 - JWT 서명 및 payload 암호화 (JWE,JWS) (4) | 2023.07.31 |
Rest API Template 만들기 - EP 09 (UnitTest) (0) | 2023.01.16 |
Rest API Template 만들기 - EP 08 (DB 를 통해 AccessToken 및 RefreshToken 관리) (0) | 2023.01.12 |
Rest API Template 만들기 - EP 07 (EF Core - Migration and Update) (0) | 2023.01.09 |