使用者驗證與授權 - ASP.NET Core Identity :防止未授權的操作

上星期五我們在練習完刪除一筆資料前,先出現對話框詢問使用者是否確定要刪除,如果確定就執行 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 AppUsers { get; set; } 

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 的請求需要做判斷,一般會被拒絕存取會有兩種情況:

  1.  尚未登入
  2.  權限不夠

所以上述程式碼有兩項判斷。

接著再用 Postman 測試看看,果然如預期的,出現 401 未授權的狀態碼:

好吧!今天就先學到這裡,明天再來進行第二回合的使用者驗證吧!