[ตอนที่ 3] Social App Workshop ด้วย ASP.NET Core 3 กับ Angular 9 ระบบ Security สำหรับ API

Line
Facebook
Twitter
Google

สารบัญเนื้อหา

  1. สร้างโมเดลเพื่อจัดเก็บข้อมูล Users
  2. Repository คืออะไร
  3. สร้าง Interface เพื่อใช้ Repository
  4. สร้าง Auth Repository และ Method Register
  5. สร้าง Login Repository
  6. สร้างระบบลงทะเบียน Users
  7. ใช้งาน DTOs (Data Transfer Objects)
  8. สร้าง Method สำหรับ Login
  9. ใช้งาน Authentication Middleware

1. สร้างโมเดลเพื่อจัดเก็บข้อมูล Users

สร้างโมเดล User.cs ภายใต้ Models โค้ดตามนี้

namespace SocialApp_API.Models
{
    public class User
    {
        public int Id { get; set; }
        public string Username { get; set; }
        public byte[] PasswordHash { get; set; }
        public byte[] PasswordSalt { get; set; }
    }
}

และกำหนด Property ใน DataContext

public DbSet<User> Users { get; set; }

ทำการ Migration และ Update Database โดยใช้คำสั่ง

dotnet ef migrations add AddUserEntity
dotnet ef database update

2. Repository Pattern คืออะไร

Repository Pattern คือ โครงสร้างการออกแบบ Application โดยมีตัวกลางระหว่าง Data Source Layer กับ Business Layer ของ Application ซึ่งทำหน้าที่ในเรียกใช้ข้อมูลจาก Data Source หรือ Web Services และทำการ Map เพื่อส่งให้ Business Layer รวมถึงควบคุมการเปลี่ยนแปลงข้อมูลใน Data Source Layer จาก Business Layer ด้วย

ทำไมต้องใช้ Repository

  • ลดความซ้ำซ้อนในการ Query ข้อมูล
  • แยกส่วน Application กับส่วนของข้อมูลที่เราไม่ต้องการให้ Application เข้าไปเปลี่ยนแปลง
  • การ Query จะรวมอยู่ที่เดียว
  • รองรับระบบการ Test

3. สร้าง Interface เพื่อใช้ Repository

สร้าง Interface ชื่อ IAuthRepository.cs ภายใต้ Data โค้ดดังนี้

using SocialApp_API.Models;
using System.Threading.Tasks;

namespace SocialApp_API.Data
{
    public interface IAuthRepository
    {
        Task<User> Register(User user, string password);
        Task<User> Login(string username, string password);
        Task<bool> UserExist(string username);
    }
}

4. สร้าง Auth Repository และ Method Register

สร้าง Auth Repository ชื่อ AuthRepository.cs ภายใต้ Data แล้วทำการ Implement Interface ดังนี้

using Microsoft.EntityFrameworkCore;
using SocialApp_API.Models;
using System;
using System.Threading.Tasks;

namespace SocialApp_API.Data
{
    public class AuthRepository : IAuthRepository
    {
        private readonly DataContext _context;

        public AuthRepository(DataContext context)
        {
            _context = context;
        }

        public Task<User> Login(string username, string password)
        {
            throw new NotImplementedException();
        }

        public async Task<User> Register(User user, string password)
        {
            byte[] passwordHash, passwordSalt;
            CreatePasswordHash(password, out passwordHash, out passwordSalt);

            user.PasswordHash = passwordHash;
            user.PasswordSalt = passwordSalt;

            await _context.Users.AddAsync(user);
            await _context.SaveChangesAsync();

            return user;
        }

        private void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
        {
            using (var hmac = new System.Security.Cryptography.HMACSHA512())
            {
                passwordSalt = hmac.Key;
                passwordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
            }
        }

        public Task<bool> UserExist(string username)
        {
            throw new NotImplementedException();
        }
    }
}

5. สร้าง Login Repository

สร้าง Login Repository ใน Method Login ใน AuthRepository.cs ภายใต้ Data ดังนี้

using Microsoft.EntityFrameworkCore;
using SocialApp_API.Models;
using System;
using System.Threading.Tasks;

namespace SocialApp_API.Data
{
    public class AuthRepository : IAuthRepository
    {
        private readonly DataContext _context;

        public AuthRepository(DataContext context)
        {
            _context = context;
        }

        public async Task<User> Login(string username, string password)
        {
            var user = await _context.Users.FirstOrDefaultAsync(x => x.Username == username);

            if (user == null)
                return null;

            if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
                return null;

            return user;
        }

        private bool VerifyPasswordHash(string password, byte[] passwordHash, byte[] passwordSalt)
        {
            using (var hmac = new System.Security.Cryptography.HMACSHA512(passwordSalt))
            {
                var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
                for (int i = 0; i < computedHash.Length; i++)
                {
                    if (computedHash[i] != passwordHash[i]) return false;
                }
            }
            return true;
        }

        public async Task<User> Register(User user, string password)
        {
            byte[] passwordHash, passwordSalt;
            CreatePasswordHash(password, out passwordHash, out passwordSalt);

            user.PasswordHash = passwordHash;
            user.PasswordSalt = passwordSalt;

            await _context.Users.AddAsync(user);
            await _context.SaveChangesAsync();

            return user;
        }

        private void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
        {
            using (var hmac = new System.Security.Cryptography.HMACSHA512())
            {
                passwordSalt = hmac.Key;
                passwordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
            }
        }

        public async Task<bool> UserExist(string username)
        {
            if (await _context.Users.AnyAsync(x => x.Username == username))
                return true;

            return false;
        }
    }
}

ทำการ Register Repository ใน Startup.cs ในส่วนของ ConfigureServices

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<DataContext>(options => options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
    services.AddControllers();
    services.AddCors();
    services.AddScoped<IAuthRepository, AuthRepository>();
}

6. สร้างระบบลงทะเบียน Users

สร้าง AuthController.cs ภายใต้ Controllers

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SocialApp_API.Data;
using SocialApp_API.Models;

namespace SocialApp_API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthController : ControllerBase
    {
        private readonly IAuthRepository _repo;

        public AuthController(IAuthRepository repo)
        {
            _repo = repo;
        }

        [HttpPost("register")]
        public async Task<IActionResult> Register(string username, string password)
        {
            username = username.ToLower();

            if (await _repo.UserExist(username))
                return BadRequest("Username already exists");

            var userToCreate = new User
            {
                Username = username
            };

            var createdUser = await _repo.Register(userToCreate, password);

            return StatusCode(201);
        }
    }
}

7. ใช้งาน DTOs (Data Transfer Objects)

สร้างโฟลเดอร์ Dtos เพื่อเก็บ DTOs แล้วสร้างไฟล์ UserForRegisterDto.cs ภายใต้ Dtos แล้วเขียนโค้ดดังนี้

namespace SocialApp_API.Dtos
{
    public class UserForRegisterDto
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }
}

ปรับปรุง Method Register ใน AuthController ดังนี้

[HttpPost("register")]
public async Task<IActionResult> Register(UserForRegisterDto userForRegisterDto)
{
    userForRegisterDto.Username = userForRegisterDto.Username.ToLower();

    if (await _repo.UserExist(userForRegisterDto.Username))
        return BadRequest("Username already exists");

    var userToCreate = new User
    {
        Username = userForRegisterDto.Username
    };

    var createdUser = await _repo.Register(userToCreate, userForRegisterDto.Password);

    return StatusCode(201);
}

ทดสอบรัน และส่งข้อมูลด้วย Http Post ผ่าน Postman ไปที่ http://localhost:5000/api/auth/register ด้วยข้อมูลแบบ JSON ดังนี้

{
    "username": "sam",
    "password": "password"
}

ถ้าทำถูกต้องต้องได้รับโค้ด 201 (Created) กลับมา

เปิด Database เพื่อตรวจสอบว่าข้อมูลบันทึกสำเร็จจริงหรือไม่

ทดลองส่งข้อมูลเดิมซ้ำไปอีกครั้ง

ถ้าทำถูกต้องต้องได้รับข้อความ Error ว่า Username already exists

ทดลองส่งข้อมูลว่าง ๆ เข้าไป เช่น

{
    "username": "",
    "password": "password"
}
{
    "username": "",
    "password": ""
}

จะพบขว่าข้อมูลบันทึกสำเร็จ ซึ่งไม่ควรจะเป็นเช่นนั้น เราจึงต้องทำการ Validate ข้อมูลก่อน โดยปรับปรุงโค้ดในไฟล์ UserForRegisterDto.cs ภายใต้ Dtos ดังนี้

using System.ComponentModel.DataAnnotations;

namespace SocialApp_API.Dtos
{
    public class UserForRegisterDto
    {
        [Required]
        public string Username { get; set; }
        [Required]
        [StringLength(8, MinimumLength = 4,ErrorMessage = "You must specify password between 4 and 8 charectors")]
        public string Password { get; set; }
    }
}

8. สร้าง Method สำหรับ Login

ทำการติดตั้ง NuGet Package เพิ่มเติม เพื่อใช้งาน Tokens ในการ Login

Install-Package Microsoft.IdentityModel.Tokens
Install-Package System.IdentityModel.Tokens.Jwt

สร้างไฟล์ UserForLoginDto.cs ภายใต้ Dtos แล้วเขียนโค้ดดังนี้

namespace SocialApp_API.Dtos
{
    public class UserForLoginDto
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }
}

เพิ่ม Method Login ใน AuthController ดังนี้

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using SocialApp_API.Data;
using SocialApp_API.Dtos;
using SocialApp_API.Models;

namespace SocialApp_API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthController : ControllerBase
    {
        private readonly IAuthRepository _repo;
        private readonly IConfiguration _config;

        public AuthController(IAuthRepository repo, IConfiguration config)
        {
            _repo = repo;
            _config = config;
        }

        [HttpPost("register")]
        public async Task<IActionResult> Register(UserForRegisterDto userForRegisterDto)
        {
            userForRegisterDto.Username = userForRegisterDto.Username.ToLower();

            if (await _repo.UserExist(userForRegisterDto.Username))
                return BadRequest("Username already exists");

            var userToCreate = new User
            {
                Username = userForRegisterDto.Username
            };

            var createdUser = await _repo.Register(userToCreate, userForRegisterDto.Password);

            return StatusCode(201);
        }

        [HttpPost("login")]
        public async Task<IActionResult> Login(UserForLoginDto userForLoginDto)
        {
            var userFromRepo = await _repo.Login(userForLoginDto.Username.ToLower(), userForLoginDto.Password);

            if (userFromRepo == null)
                return Unauthorized();

            var claims = new[]
            {
                new Claim(ClaimTypes.NameIdentifier, userFromRepo.Id.ToString()),
                new Claim(ClaimTypes.Name, userFromRepo.Username)
            };

            var key = new SymmetricSecurityKey(Encoding.UTF8
                .GetBytes(_config.GetSection("AppSettings:Token").Value));

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

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

            var tokenHandler = new JwtSecurityTokenHandler();

            var token = tokenHandler.CreateToken(tokenDescriptor);

            return Ok(new
            {
                token = tokenHandler.WriteToken(token)
            });
        }
    }
}

เพิ่ม ค่า Token ใน appsetttings.json

{
  "AppSettings": {
    "Token": "Super Secret Key"
  },
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=socialapp.db"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

Build และทดสอบรัน และส่งข้อมูลด้วย Http Post ผ่าน Postman ไปที่ http://localhost:5000/api/auth/login ด้วยข้อมูลแบบ JSON ดังนี้

{
    "username": "sam",
    "password": "password"
}

ถ้าทำถูกต้อง จะต้องได้ token กลับมา แบบนี้

{
    "token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiIxIiwidW5pcXVlX25hbWUiOiJzYW0iLCJuYmYiOjE1OTI5MzE3NjksImV4cCI6MTU5MzAxODE2OSwiaWF0IjoxNTkyOTMxNzY5fQ.eDhRlAfj0jtbO6_vi8C2mfU-uhOHyJR6UbdjG_8Jrnqe0UjsIjw3Yy1hggyfQb6WH7BJnIm7YPPpTENVcXG7Yw"
}

9. ใช้งาน Authentication Middleware

ก่อนการใช้งาน Authentication Middleware ต้องติดตั้ง Package เพิ่มเติม คือ

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer

กำหนดให้ ValueController เป็น Controller ที่ต้อง Login ก่อน โดยใส่ [Authorize] ไว้บนสุด เพื่อให้มีผลทุก Method แล้วไปกำหนด Method ที่ต้องการให้เข้าถึงได้เป็นการเฉพาะราย Method ที่ต้องการให้เข้าถึงได้แบบไม่ต้อง Login โดยใส่ [AllowAnonymous]

เพิ่ม Authentication Services ใน ConfigureServices ใน Startup.cs ดังนี้

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<DataContext>(options => options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
    services.AddControllers();
    services.AddCors();
    services.AddScoped<IAuthRepository, AuthRepository>();
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII
                    .GetBytes(Configuration.GetSection("AppSettings:Token").Value)),
                ValidateIssuer = false,
                ValidateAudience = false
            };
        });
}

เพิ่ม Middleware app.UseAuthentication(); ก่อน app.UseAuthorization();

app.UseAuthentication();

Build และทดสอบรัน แล้วลองเรียกข้อมูลจาก http://localhost:5000/api/values เพื่อดูความแตกต่าง

ถ้าทำถูกต้อง ต้องไม่สามารถเรียกข้อมูลได้จนกว่าจะใส่ Header กำหนดค่า Authentication เป็น Bearer [token]

โปรดติดตามตอนต่อไป…

Line
Facebook
Twitter
Google
[ตอนที่ 16] Social App Workshop ด้วย ASP.NET Core 3 กับ Angular 9 การทำ Sorting
[ตอนที่ 15] Social App Workshop ด้วย ASP.NET Core 3 กับ Angular 9 การทำ Filtering
[ตอนที่ 14] Social App Workshop ด้วย ASP.NET Core 3 กับ Angular 9 การใช้งาน Paging
No Preview
[ตอนที่ 13] Social App Workshop ด้วย ASP.NET Core 3 กับ Angular 9 การกำหนดรูปแบบการแสดงผลวัน-เวลา
เตรียม Atom สำหรับ React Native #3
เตรียม Visual Studio Code สำหรับ React Native #2
การติดตั้ง React Native บน macOs #1
การกำหนดค่า TF_MIN_GPU_MULTIPROCESSOR_COUNT เพื่อให้ TensorFlow ใช้งาน GPU ทุกตัว
ติดตั้ง Ubuntu 17.04 ใช้งานร่วมกับ Windows 10
การติดตั้ง TensorFlow & Caffe บน Ubuntu 16.04