สร้างโปรเจ็กส์แบบ ASP.NET Core Web API
ติดตั้ง Package Microsoft.IdentityModel.Tokens 6.15.0
PM> Install-Package Microsoft.IdentityModel.Tokens -Version 6.15.0
ติดตั้ง Package System.IdentityModel.Tokens.Jwt 6.15.0
PM> Install-Package System.IdentityModel.Tokens.Jwt -Version 6.15.0
สร้างคลาส Entities\User.cs
using System.Text.Json.Serialization;
namespace WebApi5.Entities
{
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
[JsonIgnore]
public string Password { get; set; }
}
}
สร้างคลาส Models\AuthenticateRequest.cs
using System.ComponentModel.DataAnnotations;
namespace WebApi5.Models
{
public class AuthenticateRequest
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
}
}
สร้างคลาส Models\AuthenticateResponse.cs
using WebApi5.Entities;
namespace WebApi5.Models
{
public class AuthenticateResponse
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public string Token { get; set; }
public AuthenticateResponse(User user, string token)
{
Id = user.Id;
FirstName = user.FirstName;
LastName = user.LastName;
Username = user.Username;
Token = token;
}
}
}
สร้างคลาส Helpers\AppSettings.cs
namespace WebApi5.Helpers
{
public class AppSettings
{
public string Secret { get; set; }
}
}
สร้างคลาส Helpers\AuthorizeAttribute.cs
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using WebApi5.Entities;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeAttribute : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
var user = (User)context.HttpContext.Items["User"];
if (user == null)
{
// not logged in
context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized };
}
}
}
สร้างคลาส Services\UserService.cs
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using WebApi5.Entities;
using WebApi5.Helpers;
using WebApi5.Models;
namespace WebApi5.Services
{
public interface IUserService
{
AuthenticateResponse Authenticate(AuthenticateRequest model);
IEnumerable<User> GetAll();
User GetById(int id);
}
public class UserService : IUserService
{
// users hardcoded for simplicity, store in a db with hashed passwords in production applications
private List<User> _users = new List<User>
{
new User { Id = 1, FirstName = "Test", LastName = "User", Username = "test", Password = "test" }
};
private readonly AppSettings _appSettings;
public UserService(IOptions<AppSettings> appSettings)
{
_appSettings = appSettings.Value;
}
public AuthenticateResponse Authenticate(AuthenticateRequest model)
{
var user = _users.SingleOrDefault(x => x.Username == model.Username && x.Password == model.Password);
// return null if user not found
if (user == null) return null;
// authentication successful so generate jwt token
var token = generateJwtToken(user);
return new AuthenticateResponse(user, token);
}
public IEnumerable<User> GetAll()
{
return _users;
}
public User GetById(int id)
{
return _users.FirstOrDefault(x => x.Id == id);
}
// helper methods
private string generateJwtToken(User user)
{
// generate token that is valid for 7 days
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }),
//Expires = DateTime.UtcNow.AddDays(7),
Expires = DateTime.UtcNow.AddMinutes(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
}
สร้างคลาส Helpers\JwtMiddleware.cs
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WebApi5.Services;
namespace WebApi5.Helpers
{
public class JwtMiddleware
{
private readonly RequestDelegate _next;
private readonly AppSettings _appSettings;
public JwtMiddleware(RequestDelegate next, IOptions<AppSettings> appSettings)
{
_next = next;
_appSettings = appSettings.Value;
}
public async Task Invoke(HttpContext context, IUserService userService)
{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
if (token != null)
attachUserToContext(context, userService, token);
await _next(context);
}
private void attachUserToContext(HttpContext context, IUserService userService, string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);
// attach user to context on successful jwt validation
context.Items["User"] = userService.GetById(userId);
}
catch
{
// do nothing if jwt validation fails
// user is not attached to context so request won't have access to secure routes
}
}
}
}
แก้ไขไฟล์ appsettings.json
{
"AppSettings": {
"Secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
แก้ไขไฟล์ Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using WebApi5.Helpers;
using WebApi5.Services;
namespace WebApi5
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApi5", Version = "v1" });
});
// configure strongly typed settings object
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
// configure DI for application services
services.AddScoped<IUserService, UserService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApi5 v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseMiddleware<JwtMiddleware>();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
สร้าง API Controller Controllers\UsersController.cs
using Microsoft.AspNetCore.Mvc;
using WebApi5.Models;
using WebApi5.Services;
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
namespace WebApi5.Controllers
{
[Route("[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private IUserService _userService;
public UsersController(IUserService userService)
{
_userService = userService;
}
[HttpPost("authenticate")]
public IActionResult Authenticate(AuthenticateRequest model)
{
var response = _userService.Authenticate(model);
if (response == null)
return BadRequest(new { message = "Username or password is incorrect" });
return Ok(response);
}
[Authorize]
[HttpGet]
public IActionResult GetAll()
{
var users = _userService.GetAll();
return Ok(users);
}
}
}
ทดลองเรียกใช้งาน https://localhost:7078/Users จะได้
{
"message": "Unauthorized"
}
ต้องไปเรียก https://localhost:7078/Users/authenticate เพื่อขอ token มาก่อน
Request
{
"username": "test",
"password": "test"
}
Response
{
"id": 1,
"firstName": "Test",
"lastName": "User",
"username": "test",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYmYiOjE2Mzk1OTM0OTYsImV4cCI6MTYzOTU5MzU1NiwiaWF0IjoxNjM5NTkzNDk2fQ.ingwuhAl4TB7n7lOMILt-hWpN07ggjwQu4VJF6Lky2U"
}
ถ้า username หรือ password ผิดจะได้
{
"message": "Username or password is incorrect"
}
ทีนี้เรียก https://localhost:7078/Users พร้อมให้ค่า Bearer Token จะเรียกได้ละ (token มีอายุ 1 นาที ถ้าเกิน 1 นาทีก็จะ Unauthorized
)
[
{
"id": 1,
"firstName": "Test",
"lastName": "User",
"username": "test"
}
]