上星期五我們在練習完刪除一筆資料前,先出現對話框詢問使用者是否確定要刪除,如果確定就執行 ASP.NET Core Web API 的刪除動作。雖然在前端有防止使用者誤按的防呆機制,但是後端的 ASP.NET Core Web API 卻沒有受到保護,只要有心人士知道 Web API 的 URL 就可以任意地操控我們的資料。
所以今天起先安排一系列與資料保護有關的練習,完成後再往下發展另外的學習。
ASP.NET Core Identity
ASP.NET Core 本身就有一套 ASP.NET Core Identity 會員管理機制,我們實在不必動手再刻一個,就直接拿來用好了。
首先,請為 Demae.Core 專案安裝 Microsoft.AspNetCore.Identity.EntityFrameworkCore NuGet 套件:
IdentityUser
接著新增一個繼承 IdentityUser 命名為 AppUser (名稱可自訂,主要能代表是應用程式的使用者)的 Entity Class 雖然直接繼承,不必再新增任何屬性也可以有會員管理的功能,但是為了說明該 IdentityUser 可以被客制化,所以就暫時追加一個 NickName 屬性吧:
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
namespace Demae.Core.Entities
{
public class AppUser : IdentityUser
{
public string NickName { get; set; }
}
}
修改 DbContext
接著將原先繼承 DbContext 的 DemaeContext 改成繼承 IdentityDbContext 並加入剛剛所實作的 AppUser 到 DbSet 屬性 public DbSet
using Demae.Core.Entities;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace Demae.Core.Data
{
public class DemaeContext : IdentityDbContext
{
public DemaeContext(DbContextOptions<DemaeContext> options)
: base(options) { }
public DbSet<City> Cities { get; set; }
public DbSet<Area> Areas { get; set; }
public DbSet<Address> Addresses { get; set; }
public DbSet<AppUser> AppUsers { get; set; }
}
}
其實也可以寫成這樣:
namespace Demae.Core.Data
{
public class DemaeContext : IdentityDbContext<AppUser>
{
public DemaeContext(DbContextOptions<DemaeContext> options)
: base(options) { }
public DbSet<City> Cities { get; set; }
public DbSet<Area> Areas { get; set; }
public DbSet<Address> Addresses { get; set; }
}
}
Add-Migration 和 Update-Database
還記得先前曾經學習過的《資料庫移轉初體驗》中所提到的,每當 Entity Model 有所改變時就要重復執行一次 Add-Migration 和 Update-Database 的流程,當執行完後,打開對應資料庫,應該可以發現新增了 7 個資料表,目前只要知道,這新增的資料表是用來管理會員登入和權限用的就可以了,往後還會再進一步討論:
展開 AspNetUsers 資料表應該也會發現被客制化的 NickName 欄位:
在 ASP.NET Core Web API 設定 Identity
既然用來記錄使用者登入以及授權旳資料庫已經準備好了,接著就是在 Demae.Api 專案的 Startup.cs 中註冊使用,加入程式碼如下所示:
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
// 省略
services.AddIdentity<AppUser, IdentityRole>().AddEntityFrameworkStores<DemaeContext>();
// 省略
}
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
DemaeDbInitializer seeder)
{
// 省略
app.UseIdentity();
// 省略
}
授權
接著在需要經過授權才可執行的控制器上加入 [Authorize]
修飾詞,即可限制該控制器的所有動作方法都需要授權才可執行,程式碼修改如下:
namespace Demae.Api.Controllers
{
[Authorize]
[Produces("application/json")]
[Route("api/Addresses")]
public class AddressesController : Controller
{
// 省略
}
}
接著使用 Postman 測試一下,加入的授權是否有效。奇怪了,理論上應該傳回 401 未授權才對,怎麼是 404 找不到的狀態碼呢?
如果將該 URL 貼到網頁瀏覽器,應該會發現,原來因為需要授權才可執行,所以被導到登入頁面了,因為我們沒有實作登入頁面(Web API 也不需要)所以就出現無法找到此網頁的資訊。
因為 ASP.NET Core 原設計是 Web API 與網頁可發行在同一個網頁空間,所以內訂會被導到登入頁面,但是這對於只當作 Web API 使用的並不合適,所以需要做些修正,所以請在 Startup.cs 裡加入下述程式碼:
public void ConfigureServices(IServiceCollection services)
{
// 省略
services.Configure<IdentityOptions>(config =>
{
config.Cookies.ApplicationCookie.Events =
new CookieAuthenticationEvents()
{
OnRedirectToLogin = (ctx) =>
{
if (ctx.Request.Path.StartsWithSegments("/api") && ctx.Response.StatusCode == 200)
{
ctx.Response.StatusCode = 401;
}
return Task.CompletedTask;
},
OnRedirectToAccessDenied = (ctx) =>
{
if (ctx.Request.Path.StartsWithSegments("/api") && ctx.Response.StatusCode == 200)
{
ctx.Response.StatusCode = 403;
}
return Task.CompletedTask;
}
};
});
// 省略
}
上述程式碼擷取 URL 包含 /api 的,表示來自 Web API 的請求需要做判斷,一般會被拒絕存取會有兩種情況:
- 尚未登入
- 權限不夠
所以上述程式碼有兩項判斷。
接著再用 Postman 測試看看,果然如預期的,出現 401 未授權的狀態碼:
好吧!今天就先學到這裡,明天再來進行第二回合的使用者驗證吧!