Asp.net webapi와 Angular, Sqlite를 이용한 Jwt Login, Todo 만들기. #1

유튜브 : https://youtu.be/MPINGd-SfQs

GitHub : https://github.com/tigersarang/AngularAspnetJwtTodo


1.  asp.net 생성

  dotnet new webapi -controllers -n TodoWeb

2. nuget package 설치

  

3. Models, DbContext 생성

namespace TodoWeb.Models.Datas
{
    public class TodoCustomer
    {
        public int Id { get; set; }
        public required string UserName { get; set; }
        public required string Email { get; set; }
        public byte[]? PasswordHash { get; set; }
        public byte[]? PasswordSalt { get; set; }
        public IEnumerable<TodoItem>? TodoItems { get; set; }
       
    }
}
namespace TodoWeb.Models.Datas
{
    public class TodoItem
    {
        public int Id { get; set; }
        public required string Title { get; set; }
        public required string Description { get; set; }
        public bool IsCompleted { get; set; }
        public required TodoCustomer TodoCustomer { get; set; }
        public required int TodoCustomerId { get; set; }
    }
}
namespace TodoWeb.Models.Dtos
{
    public class TodoCustomerDto
    {
        public int Id { get; set; }
        public string UserName { get; set; } = default!;
        public string? Email { get; set; } = default!;
        public string? Password { get; set; }
        public string? Token { get; set; }
        public List<TodoItemDto> TodoItems { get; set; } = new();
    }
}
namespace TodoWeb.Models.Dtos
{
    public class TodoItemDto
    {
        public int Id { get; set; }
        public string Title { get; set; } = default!;
        public string Description { get; set; } = default!;
        public bool IsCompleted { get; set; }
        public int TodoCustomerId { get; set; }
    }
}
namespace TodoWeb.Models.Datas
{
    public class TodoDbContext(DbContextOptions options) : DbContext(options)
    {
        public DbSet<TodoCustomer> Customers { get; set; }
        public DbSet<TodoItem> Items { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TodoItem>()
                .HasOne(ti => ti.TodoCustomer)
                .WithMany(tc => tc.TodoItems)
                .HasForeignKey(ti => ti.TodoCustomerId)
                .OnDelete(DeleteBehavior.Cascade)
                ;

            base.OnModelCreating(modelBuilder);
        }
    }
}
 

4. config 설정(appsettings.Development.json)

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
 "ConnectionStrings": {
    "conn" : "Data Source = todo.db"
  },
  "TokenKey": "asdfjksldfjkweuiroweudsf23423098492038DSFSADFSADFDS@!#$@!$#@sdfjsdakflk#455-0-weoisdkdfmndfsajk#@%DFDFXREdsf@!#$@!$#@sdfjsdakflk#455-0-weoisdkdfmndfsajk#@%DFDFXREdsf"
}

5. AutoMapper 설정

namespace TodoWeb.Helpers
{
    public class AutoMapperProfiles : Profile
    {
        public AutoMapperProfiles()
        {
            CreateMap<TodoItem, TodoItemDto>().ReverseMap();
            CreateMap<TodoCustomer, TodoCustomerDto>().ReverseMap();
        }
    }
}


6. Service 생성

namespace TodoWeb.Services

{
    public interface ITokenRepository
    {        
        string CreateToken(TodoCustomer todoCustomer);
    }
}

namespace TodoWeb.Services
{
    public class TokenService(IConfiguration configuration) : ITokenRepository
    {
        public string CreateToken(TodoCustomer todoCustomer)
        {
            var tokenKey = configuration["TokenKey"];
            if (tokenKey.Length < 64) throw new ArgumentException("Token key must be at least 64 characters long.");

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenKey));
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.NameIdentifier, todoCustomer.Id.ToString()),
                new Claim(ClaimTypes.Email, todoCustomer.Email),
                new Claim(ClaimTypes.Name, todoCustomer.UserName)
            };

            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);

            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(claims),
                Expires = DateTime.Now.AddDays(7),
                SigningCredentials = creds
            };

            var tokenHandler = new JwtSecurityTokenHandler();
            var token = tokenHandler.CreateToken(tokenDescriptor);
            return tokenHandler.WriteToken(token);
        }
    }
}

namespace TodoWeb.Services
{
    public interface ITodoCustomerRepository
    {
        Task<TodoCustomerDto> RegisterUser(TodoCustomer customer);
        Task<TodoCustomerDto> LoginUser(TodoCustomerDto todoCustomerDto);        
    }
}

namespace TodoWeb.Services
{
    public class TodoCustomerService(TodoDbContext _todoDbContext, ITokenRepository tokenRepository, IMapper mapper) : ITodoCustomerRepository
    {
        public async Task<TodoCustomerDto> LoginUser(TodoCustomerDto todoCustomerDto)
        {
            var customer = await _todoDbContext.Customers.FirstOrDefaultAsync(c => c.UserName == todoCustomerDto.UserName);

            if (customer == null) return null;

            using var hmac = new HMACSHA512(customer.PasswordSalt);
            var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(todoCustomerDto.Password));

            if (computedHash.SequenceEqual(customer.PasswordHash))
            {
                TodoCustomerDto customerDto = mapper.Map<TodoCustomerDto>(customer);
                customerDto.Token = tokenRepository.CreateToken(customer);

                return customerDto;
            }

            return null;
        }

        public async Task<TodoCustomerDto> RegisterUser(TodoCustomer customer)
        {
            _todoDbContext.Entry(customer).State = EntityState.Added;
            await _todoDbContext.SaveChangesAsync();
             
            return mapper.Map<TodoCustomerDto>(customer);
        }
    }
}

namespace TodoWeb.Services
{
    public interface ITodoRepository
    {
        Task<IEnumerable<TodoItem>> GetAll();
        Task<TodoItem> GetDodo(int id);
        Task<TodoItem> AddTodo(TodoItem todoItem);
        Task<TodoItem> UpdateTodo(TodoItem todoItem);
        Task<bool> DeleteTodo(int id);
    }
}

namespace TodoWeb.Services
{
    public class TodoService(TodoDbContext _todoDbContext) : ITodoRepository
    {
        public async Task<TodoItem> AddTodo(TodoItem todoItem)
        {
            _todoDbContext.Items.Add(todoItem);
            await _todoDbContext.SaveChangesAsync();
            return todoItem;
        }

        public async Task<bool> DeleteTodo(int id)
        {
            var todoItem = await _todoDbContext.Items.FirstOrDefaultAsync(x => x.Id == id);

            if (todoItem == null) return false;

            _todoDbContext.Items.Remove(todoItem);
            await _todoDbContext.SaveChangesAsync();
            return true;
        }

        public async Task<IEnumerable<TodoItem>> GetAll()
        {
            return await _todoDbContext.Items.ToListAsync();
        }

        public async Task<TodoItem> GetDodo(int id)
        {
            return await _todoDbContext.Items.FirstOrDefaultAsync(x => x.Id == id);
        }

        public async Task<TodoItem> UpdateTodo(TodoItem todoItem)
        {
            _todoDbContext.Items.Update(todoItem);
            await _todoDbContext.SaveChangesAsync();
            return todoItem;
        }
    }
}


7. program.cs 수정

    using System.Text;
    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.IdentityModel.Tokens;
    using Microsoft.OpenApi.Models;
    using TodoWeb.Models.Datas;
    using TodoWeb.Services;

    var builder = WebApplication.CreateBuilder(args);

    // Add services to the container.

    builder.Services.AddControllers();

    builder.Services.AddDbContext<TodoDbContext>(options =>
    {
        options.UseSqlite(builder.Configuration.GetConnectionString("conn"));
    });

    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(o =>
            {
                var tokenKey = builder.Configuration["TokenKey"] ?? throw new Exception("TokenKey Error");
                o.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenKey)),
                    ValidateAudience = false,
                    ValidateIssuer = false
                };
            });

    builder.Services.AddCors();
    builder.Services.AddScoped<ITodoCustomerRepository, TodoCustomerService>();
    builder.Services.AddScoped<ITokenRepository, TokenService>();
    builder.Services.AddScoped<ITodoRepository, TodoService>();
    builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

    builder.Services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo
        {
            Title = "Todo API",
            Version = "v1",
            Description = "Todo Api"
        });

        c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
        {
            Name = "Authorization",
            In = ParameterLocation.Header,
            Type = SecuritySchemeType.Http,
            Scheme = "Bearer",
            BearerFormat = "JWT",
            Description = "Description"
        });

        c.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
            {
                new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                    }
                },
                Array.Empty<string>()
            }
        });
    });
           
    // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
    builder.Services.AddOpenApi();

    var app = builder.Build();

    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.MapOpenApi();
        app.UseSwagger();
        app.UseSwaggerUI();    
    }

    app.UseCors(x => x.AllowAnyHeader().AllowAnyMethod().AllowCredentials().WithOrigins("http://localhost:4200"));
    app.UseHttpsRedirection();

    app.UseAuthentication();
    app.UseAuthorization();

    app.MapControllers();

    app.Run();



8. controller 설정


namespace TodoWeb.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class CustomerController(IMapper mapper, ITodoCustomerRepository todoCustomerRepository) : ControllerBase
    {
        [HttpPost("register")]
        public async Task<ActionResult<TodoCustomerDto>> Register(TodoCustomerDto todoCustomerDto)
        {
            if (todoCustomerDto == null) return BadRequest("Invalid User Info");

            var customer = mapper.Map<TodoCustomer>(todoCustomerDto);

            using var hmac = new HMACSHA512();

            customer.PasswordHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(todoCustomerDto.Password));
            customer.PasswordSalt = hmac.Key;

            var result = await todoCustomerRepository.RegisterUser(customer);

            if (result == null) return StatusCode(500, "Error registering user");

            return mapper.Map<TodoCustomerDto>(result);
        }

        [HttpPost("login")]
        public async Task<ActionResult<TodoCustomerDto>> LoginUser(TodoCustomerDto todoCustomerDto)
        {
            if (todoCustomerDto == null) return BadRequest("invalid user");

            var result = await todoCustomerRepository.LoginUser(todoCustomerDto);

            if (result == null) return Unauthorized("login failed");

            return result;
        }
    }

}


namespace TodoWeb.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
   public class TodoController(ITodoRepository todoService, IMapper mapper) : ControllerBase
    {
        [HttpPost("add")]
        public async Task<ActionResult<TodoItemDto>> AddTodoItem(TodoItemDto todoItemDto)
        {
            if (todoItemDto == null) return BadRequest("todoitem is null.");

            var todoItem = mapper.Map<TodoItem>(todoItemDto);

            var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier);
            if (userIdClaim == null) return Unauthorized(" user error");

            todoItem.TodoCustomerId = int.Parse(userIdClaim.Value);

            var result = await todoService.AddTodo(todoItem);

            if (result == null)
            {
                return BadRequest("todo 저장 실패.");
            }


            return Ok(mapper.Map<TodoItemDto>(todoItem));
        }

        [HttpGet("all")]
        public async Task<ActionResult<List<TodoItemDto>>> GetTodo()
        {
            var todos = await todoService.GetAll();

            return Ok(mapper.Map<List<TodoItemDto>>(todos));
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<TodoItemDto>> Get(int id)
        {
            var todo = await todoService.GetDodo(id);

            if (todo == null)
            {
                return NotFound("Todo not found");
            }

            return Ok(mapper.Map<TodoItemDto>(todo));
        }

        [HttpPut("update")]
        public async Task<ActionResult<TodoItemDto>> Update(TodoItemDto todoItemDto)
        {
            if (todoItemDto == null) return BadRequest("Invalid Todo Item");

            TodoItem todoItem = mapper.Map<TodoItem>(todoItemDto);

            var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier);
            if (userIdClaim == null) return Unauthorized("User Error");

            todoItem.TodoCustomerId = int.Parse(userIdClaim.Value);

            var result = await todoService.UpdateTodo(todoItem);

            return mapper.Map<TodoItemDto>(result);
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteTodoItemAsync(int id)
        {
            var isDeleted = await todoService.DeleteTodo(id);

            if (isDeleted) return Ok();
            else return StatusCode(StatusCodes.Status500InternalServerError, "Todo 삭제 실패");
        }    
    }
}

9. command

- dotnet ef migrations add init
- dotnet ef database update
- dotnet watch

댓글 쓰기

댓글 목록