สารบัญเนื้อหา
- สร้างโมเดลเพื่อจัดเก็บข้อมูล Users
- Repository คืออะไร
- สร้าง Interface เพื่อใช้ Repository
- สร้าง Auth Repository และ Method Register
- สร้าง Login Repository
- สร้างระบบลงทะเบียน Users
- ใช้งาน DTOs (Data Transfer Objects)
- สร้าง Method สำหรับ Login
- ใช้งาน 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]
โปรดติดตามตอนต่อไป…