สร้างโปรเจ็กส์แบบ mvc
[code]
> mkdir grocery
> cd grocery
> dotnet new mvc
[/code]
รันด้วย Visual Studio 2017 ได้ https://localhost:44330/
เพิ่ม package ที่ต้องใช้
[code]
dotnet add package JWT -v 3.0.0-beta4
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
[/code]
Models/GroceryItem.cs
1 2 3 4 5 6 7 8 |
namespace grocery.Models { public class GroceryItem { public long Id { get; set; } public string Description { get; set; } } } |
Models/Credentials.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System.ComponentModel.DataAnnotations; namespace grocery.Models { public class Credentials { [Required] [EmailAddress] [Display(Name = "Email")] public string Email { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } } } |
จริงๆไฟล์ JWTSettings.cs ควรอยู่ที่ root directory ไม่ใช่ใน Models
Models/JWTSettings.cs
1 2 3 4 5 6 7 8 9 |
namespace grocery.Models { public class JWTSettings { public string SecretKey { get; set; } public string Issuer { get; set; } public string Audience { get; set; } } } |
สร้างโฟลเดอร์ Data
Data/GroceryListContext.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using grocery.Models; using Microsoft.EntityFrameworkCore; namespace grocery.Data { public class GroceryListContext : DbContext { public GroceryListContext(DbContextOptions<GroceryListContext> options) : base(options) { } public DbSet<GroceryItem> GroceryList { get; set; } } } |
Data/UserDbContext.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace grocery.Data { public class UserDbContext : IdentityDbContext<IdentityUser> { public UserDbContext(DbContextOptions<UserDbContext> options) : base(options) { Database.EnsureCreated(); } } } |
บรรทัดที่ 12 : น่าจะเป็นการบังคับสร้างดาต้าเบส โดยไม่ต้องมาใช้คำสั่ง dotnet ef database update
Controllers/GroceryListController.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
using grocery.Data; using grocery.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.Linq; namespace grocery.Controllers { [Authorize] [Route("api/[controller]")] public class GroceryListController : Controller { private readonly GroceryListContext _context; public GroceryListController(GroceryListContext context) { _context = context; if (_context.GroceryList.Count() == 0) { _context.GroceryList.Add(new GroceryItem { Description = "Item1" }); _context.SaveChanges(); } } [HttpGet] public IEnumerable<GroceryItem> GetAll() { return _context.GroceryList.ToList(); } [HttpGet("{id}", Name = "GetGroceryItem")] public IActionResult GetById(long id) { var item = _context.GroceryList.FirstOrDefault(t => t.Id == id); if (item == null) { return NotFound(); } return new ObjectResult(item); } [HttpPost] public IActionResult Create([FromBody] GroceryItem item) { if (item == null) { return BadRequest(); } _context.GroceryList.Add(item); _context.SaveChanges(); return CreatedAtRoute("GetGroceryItem", new { id = item.Id }, item); } [HttpDelete("{id}")] public IActionResult Delete(long id) { var item = _context.GroceryList.First(t => t.Id == id); if (item == null) { return NotFound(); } _context.GroceryList.Remove(item); _context.SaveChanges(); return new NoContentResult(); } } } |
Controllers/AccountController.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
using grocery.Models; using JWT; using JWT.Algorithms; using JWT.Serializers; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace grocery.Controllers { [Route("api/[controller]")] public class AccountController : Controller { private readonly UserManager<IdentityUser> _userManager; private readonly SignInManager<IdentityUser> _signInManager; private readonly JWTSettings _options; public AccountController( UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, IOptions<JWTSettings> optionsAccessor) { _userManager = userManager; _signInManager = signInManager; _options = optionsAccessor.Value; } [HttpPost] public async Task<IActionResult> Register([FromBody] Credentials Credentials) { if (ModelState.IsValid) { var user = new IdentityUser { UserName = Credentials.Email, Email = Credentials.Email }; var result = await _userManager.CreateAsync(user, Credentials.Password); if (result.Succeeded) { await _signInManager.SignInAsync(user, isPersistent: false); return new JsonResult(new Dictionary<string, object> { { "access_token", GetAccessToken(Credentials.Email) }, { "id_token", GetIdToken(user) } }); } return Errors(result); } return Error("Unexpected error"); } private string GetIdToken(IdentityUser user) { var payload = new Dictionary<string, object> { { "id", user.Id }, { "sub", user.Email }, { "email", user.Email }, { "emailConfirmed", user.EmailConfirmed }, }; return GetToken(payload); } private string GetAccessToken(string Email) { var payload = new Dictionary<string, object> { { "sub", Email }, { "email", Email } }; return GetToken(payload); } private string GetToken(Dictionary<string, object> payload) { var secret = _options.SecretKey; payload.Add("iss", _options.Issuer); payload.Add("aud", _options.Audience); payload.Add("nbf", ConvertToUnixTimestamp(DateTime.Now)); payload.Add("iat", ConvertToUnixTimestamp(DateTime.Now)); payload.Add("exp", ConvertToUnixTimestamp(DateTime.Now.AddDays(7))); IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); IJsonSerializer serializer = new JsonNetSerializer(); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); return encoder.Encode(payload, secret); } private JsonResult Errors(IdentityResult result) { var items = result.Errors .Select(x => x.Description) .ToArray(); return new JsonResult(items) { StatusCode = 400 }; } private JsonResult Error(string message) { return new JsonResult(message) { StatusCode = 400 }; } private static double ConvertToUnixTimestamp(DateTime date) { DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); TimeSpan diff = date.ToUniversalTime() - origin; return Math.Floor(diff.TotalSeconds); } [HttpPost("sign-in")] public async Task<IActionResult> SignIn([FromBody] Credentials Credentials) { if (ModelState.IsValid) { var result = await _signInManager.PasswordSignInAsync(Credentials.Email, Credentials.Password, false, false); if (result.Succeeded) { var user = await _userManager.FindByEmailAsync(Credentials.Email); return new JsonResult(new Dictionary<string, object> { { "access_token", GetAccessToken(Credentials.Email) }, { "id_token", GetIdToken(user) } }); } return new JsonResult("Unable to sign in") { StatusCode = 401 }; } return Error("Unexpected error"); } } } |
บรรทัดที่ 84 : กำหนดให้ token หมดอายุใน 7 วัน
Controllers/BooksController.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; namespace grocery.Controllers { [Route("api/[controller]")] public class BooksController : Controller { [HttpGet, Authorize] public IEnumerable<Book> Get() { var currentUser = HttpContext.User; var resultBookList = new Book[] { new Book { Author = "Ray Bradbury",Title = "Fahrenheit 451" }, new Book { Author = "Gabriel García Márquez", Title = "One Hundred years of Solitude" }, new Book { Author = "George Orwell", Title = "1984" }, new Book { Author = "Anais Nin", Title = "Delta of Venus" } }; return resultBookList; } public class Book { public string Author { get; set; } public string Title { get; set; } public bool AgeRestriction { get; set; } } } } |
Startup.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
using grocery.Data; using grocery.Models; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using System.Text; using System.Threading.Tasks; namespace grocery { 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.AddEntityFramework() // .AddDbContext<UserDbContext>(opt => opt.UseInMemoryDatabase()); services.AddDbContext<UserDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection"))); services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<UserDbContext>(); services.Configure<CookiePolicyOptions>(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); // Configures GroceryListContext to use in-memory database //services.AddDbContext<GroceryListContext>(opt => opt.UseInMemoryDatabase()); services.AddDbContext<GroceryListContext>(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection"))); services.Configure<JWTSettings>(Configuration.GetSection("JWTSettings")); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = Configuration["Jwt:Issuer"], ValidAudience = Configuration["Jwt:Issuer"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])) }; }); services.ConfigureApplicationCookie(options => { options.Events.OnRedirectToLogin = context => { context.Response.StatusCode = 401; return Task.CompletedTask; }; }); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseCookiePolicy(); app.UseIdentity(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } } } |
บรรทัดที่ 58-71 : Validating JWTs with ASP.NET Core
appsettings.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*", "JWTSettings": { "SecretKey": "jwts-are-awesome", "Issuer": "dotnet_grocery_list", "Audience": "GroceryListAPI" }, "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=grocery;Trusted_Connection=True;MultipleActiveResultSets=true" } } |
SecretKey ใช้ sign โทเคน และ validate โทเคน
รัน F5
Register ผู้ใช้งาน
ใช้ Postman ทำการ POST ไปที่
https://localhost:44330/api/account
[code]
{
"email": "mr.phaisarn@gmail.com",
"password": "123456#User"
}
[/code]
ผลลัพธ์
[code]
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJtci5waGFpc2FybkBnbWFpbC5jb20iLCJlbWFpbCI6Im1yLnBoYWlzYXJuQGdtYWlsLmNvbSIsImlzcyI6ImRvdG5ldF9ncm9jZXJ5X2xpc3QiLCJhdWQiOiJHcm9jZXJ5TGlzdEFQSSIsIm5iZiI6MTUzOTcwNTE3NC4wLCJpYXQiOjE1Mzk3MDUxNzQuMCwiZXhwIjoxNTQwMzA5OTc0LjB9.KlW5XgthP9_fsbHfx_Vtcf-7PDxwt5TrjVLlO9aMVi8",
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjBkYjNmZmU4LTlkYWEtNGE4OC1iN2JkLTNhNjU2YTc0NDNjYSIsInN1YiI6Im1yLnBoYWlzYXJuQGdtYWlsLmNvbSIsImVtYWlsIjoibXIucGhhaXNhcm5AZ21haWwuY29tIiwiZW1haWxDb25maXJtZWQiOmZhbHNlLCJpc3MiOiJkb3RuZXRfZ3JvY2VyeV9saXN0IiwiYXVkIjoiR3JvY2VyeUxpc3RBUEkiLCJuYmYiOjE1Mzk3MDUxNzQuMCwiaWF0IjoxNTM5NzA1MTc0LjAsImV4cCI6MTU0MDMwOTk3NC4wfQ.Jb7AvFsj8JnhjVxc9Ztgf-EDs8Hkf1DWo9XmQsEgyD8"
}
[/code]
การ log-in เข้าใช้งาน
ใช้ Postman ทำการ POST ไปที่
https://localhost:44330/api/account/sign-in
[code]
{
"email": "mr.phaisarn@gmail.com",
"password": "123456#User"
}
[/code]
ผลลัพธ์
[code]
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJtci5waGFpc2FybkBnbWFpbC5jb20iLCJlbWFpbCI6Im1yLnBoYWlzYXJuQGdtYWlsLmNvbSIsImlzcyI6ImRvdG5ldF9ncm9jZXJ5X2xpc3QiLCJhdWQiOiJHcm9jZXJ5TGlzdEFQSSIsIm5iZiI6MTUzOTcwNTI2NS4wLCJpYXQiOjE1Mzk3MDUyNjUuMCwiZXhwIjoxNTQwMzEwMDY1LjB9.Ujy0vd05kFiI_8yZd-INpAleUdG1ruSFS5iZfMq6O8E",
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjBkYjNmZmU4LTlkYWEtNGE4OC1iN2JkLTNhNjU2YTc0NDNjYSIsInN1YiI6Im1yLnBoYWlzYXJuQGdtYWlsLmNvbSIsImVtYWlsIjoibXIucGhhaXNhcm5AZ21haWwuY29tIiwiZW1haWxDb25maXJtZWQiOmZhbHNlLCJpc3MiOiJkb3RuZXRfZ3JvY2VyeV9saXN0IiwiYXVkIjoiR3JvY2VyeUxpc3RBUEkiLCJuYmYiOjE1Mzk3MDUyNjUuMCwiaWF0IjoxNTM5NzA1MjY1LjAsImV4cCI6MTU0MDMxMDA2NS4wfQ.h0WFsop4kFwweSAsxmOEAnGjrJNVpkWNfXOUlcwwJF8"
}
[/code]
นำ access_token มาใช้ขอ รายการหนังสือ
ใช้ Postman ทำการ GET ไปที่
https://localhost:44330/api/books
และส่ง access_token ไปด้วย
Error Message
If we start the application now, and issue an HTTP GET request to any endpoint of the GroceryListController class, we will get a 404 (Not Found) response from the server. You would probably expect a 401 (Unauthorized) answer, but 404 was sent back because when a user is not logged in they are redirect to a login web page. This web page is not provided by default by ASP.NET Core, and therefore the request ends up being answered with a 404 response.
Although we have secured our precious endpoints, we are not ready yet. Even if we send the access_token in a HTTP request, we are still going to get a 401 answer, because we have not configured our application to validate JWTs. Let’s tackle this issue now.
Link