.NET 6.0中使用Identity框架實現JWT身分驗證與授權

.NET 6.0中使用Identity框架實現JWT身分驗證與授權

透過一個簡單的過程介紹使用 ASP.NET Core 6.0 Web API 上傳和下載多個檔案。

最後更新 2022/7/25 下午8:43
Sarathlal Saseendran
預計閱讀 12 分鐘
分類
.NET
標籤
.NET C# ASP.NET Core Web API 鑑權

原文作者:Sarathlal Saseendran

原文链接:https://www.c-sharpcorner.com/article/jwt-authentication-and-authorization-in-net-6-0-with-identity-framework/

翻譯:沙漠盡頭的狼(Google 翻譯加持)

介紹

微軟於 2021 年 11 月發佈了 .NET 6.0。我已經在 C# Corner 上寫了幾篇關於 JWT 身分驗證的文章。由於 .NET 6.0 進行了一些重大變更,因此我決定寫一篇關於使用 .NET 6.0 版本進行 JWT 身分驗證的文章。我們將使用微軟 Identity 框架來儲存使用者與角色資訊。

Authentication(身分驗證)是驗證使用者憑證的過程,而 Authorization(授權)是檢查使用者存取應用程式中特定模組權限的過程。在本文中,我們將了解如何透過實作 JWT 身分驗證來保護 ASP.NET Core Web API 應用程式。我們還會了解如何在 ASP.NET Core 中使用授權來提供對應用程式各種功能的存取。我們將使用者憑證儲存在 SQL Server 資料庫中(註:您可以使用 MySQL、PostgreSQL 等其他關聯式資料庫),並使用 EF Core 框架與 Identity 框架進行資料庫操作。

JSON Web Token (JWT) 是一個開放標準 (RFC 7519),它定義了一種緊湊且自包含的方式,使用 JSON 物件於各方之間安全地傳輸資訊。此資訊可以驗證並信任,因為它是數位簽章的。JWT 可以使用密鑰(使用 HMAC 演算法)或使用 RSAECDSA 的公鑰/私鑰對進行簽章。

在其緊湊的形式中,JSON Web Tokens 由三個部分組成,以點 (.) 分隔,它們是:

  • Header(標頭)
  • Payload(承載)
  • Signature(簽章)

因此,JWT 格式通常如下所示:

xxxx.yyyy.zzzz

有關 JSON Web Token 的更多詳細資訊,請參閱下面的連結。

https://jwt.io/introduction/

使用 Visual Studio 2022 建立 ASP.NET Core Web API

我們需要 Visual Studio 2022 來建立 .NET 6.0 應用程式。我們可以從 Visual Studio 2022 中選擇 ASP.NET Core Web API 範本。

我們可以為專案取一個合適的名稱並選擇 .NET 6.0 架構。

我們的新專案將在隨後建立。

我們必須將以下 4 個套件庫安裝到新專案中。

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools
  • Microsoft.AspNetCore.Identity.EntityFrameworkCore
  • Microsoft.AspNetCore.Authentication.JwtBearer

您可以使用 NuGet 套件管理員來安裝上述套件。

我們可以使用以下值變更 appsettings.json。它具有用於 JWT 身分驗證的資料庫連線詳細資訊及其他詳細資訊。

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "ConnStr": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=JWTAuthDB;Integrated Security=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
  },
  "JWT": {
    "ValidAudience": "http://localhost:4200",
    "ValidIssuer": "http://localhost:5000",
    "Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xzyr"
  }
}

我們可以建立一個新資料夾 Auth 並在 Auth 資料夾下建立 ApplicationDbContext 類別並加入以下程式碼。我們將在 Auth 資料夾下加入所有與身分驗證相關的類別。

ApplicationDbContext.cs

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace IdentityDemo.Auth
{
    public class ApplicationDbContext : IdentityDbContext<IdentityUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
        }
    }
}

建立一個靜態類別 UserRoles 並加入以下值。

UserRoles.cs

namespace IdentityDemo.Auth
{
    public static class UserRoles
    {
        public const string Admin = "Admin";
        public const string User = "User";
    }
}

我們加入了兩個常數值 AdminUser 作為角色。您可以根據需要加入任意數量的角色。

建立類別 RegisterModel,用於新使用者註冊。

RegisterModel.cs

using System.ComponentModel.DataAnnotations;

namespace IdentityDemo.Auth
{
    public class RegisterModel
    {
        [Required(ErrorMessage = "使用者名稱不能為空")] public string? Username { get; set; }

        [EmailAddress]
        [Required(ErrorMessage = "郵件不能為空")] public string? Email { get; set; }

        [Required(ErrorMessage = "密碼不能為空")] public string? Password { get; set; }
    }
}

建立類別 LoginModel 用於使用者登入。

LoginModel.cs

using System.ComponentModel.DataAnnotations;

namespace IdentityDemo.Auth
{
    public class LoginModel
    {
        [Required(ErrorMessage = "使用者名稱不能為空")] public string? Username { get; set; }

        [Required(ErrorMessage = "密碼不能為空")] public string? Password { get; set; }
    }
}

我們可以建立一個 Response 類別,用於在使用者註冊與使用者登入後回傳回應值。如果請求失敗,它也會回傳錯誤訊息。

Response.cs

namespace IdentityDemo.Auth
{
    public class Response
    {
        public string? Status { get; set; }
        public string? Message { get; set; }
    }
}

我們可以在 Controllers 資料夾中建立一個 API 控制器 AuthenticateController 並加入以下程式碼。

AuthenticateController.cs

using IdentityDemo.Auth;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace IdentityDemo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthenticateController : ControllerBase
    {
        private readonly UserManager<IdentityUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;
        private readonly IConfiguration _configuration;

        public AuthenticateController(
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager,
            IConfiguration configuration)
        {
            _userManager = userManager;
            _roleManager = roleManager;
            _configuration = configuration;
        }

        [HttpPost]
        [Route("login")]
        public async Task<IActionResult> Login([FromBody] LoginModel model)
        {
            var user = await _userManager.FindByNameAsync(model.Username);
            if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))
            {
                var userRoles = await _userManager.GetRolesAsync(user);

                var authClaims = new List<Claim>
                {
                    new Claim(ClaimTypes.Name, user.UserName),
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                };

                foreach (var userRole in userRoles)
                {
                    authClaims.Add(new Claim(ClaimTypes.Role, userRole));
                }

                var token = GetToken(authClaims);

                return Ok(new
                {
                    token = new JwtSecurityTokenHandler().WriteToken(token),
                    expiration = token.ValidTo
                });
            }

            return Unauthorized();
        }

        [HttpPost]
        [Route("register")]
        public async Task<IActionResult> Register([FromBody] RegisterModel model)
        {
            var userExists = await _userManager.FindByNameAsync(model.Username);
            if (userExists != null)
                return StatusCode(StatusCodes.Status500InternalServerError,
                    new Response { Status = "Error", Message = "使用者已經存在!" });

            IdentityUser user = new()
            {
                Email = model.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = model.Username
            };
            var result = await _userManager.CreateAsync(user, model.Password);
            if (!result.Succeeded)
                return StatusCode(StatusCodes.Status500InternalServerError,
                    new Response
                    {
                        Status = "Error", Message = "建立使用者失敗!請檢查後再重試。"
                    });

            return Ok(new Response { Status = "Success", Message = "新增使用者成功!" });
        }

        [HttpPost]
        [Route("register-admin")]
        public async Task<IActionResult> RegisterAdmin([FromBody] RegisterModel model)
        {
            var userExists = await _userManager.FindByNameAsync(model.Username);
            if (userExists != null)
                return StatusCode(StatusCodes.Status500InternalServerError,
                    new Response { Status = "Error", Message = "使用者已經存在!" });

            IdentityUser user = new()
            {
                Email = model.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = model.Username
            };
            var result = await _userManager.CreateAsync(user, model.Password);
            if (!result.Succeeded)
                return StatusCode(StatusCodes.Status500InternalServerError,
                    new Response
                    {
                        Status = "Error", Message = "建立使用者失敗!請檢查後再重試。"
                    });

            if (!await _roleManager.RoleExistsAsync(UserRoles.Admin))
                await _roleManager.CreateAsync(new IdentityRole(UserRoles.Admin));
            if (!await _roleManager.RoleExistsAsync(UserRoles.User))
                await _roleManager.CreateAsync(new IdentityRole(UserRoles.User));

            if (await _roleManager.RoleExistsAsync(UserRoles.Admin))
            {
                await _userManager.AddToRoleAsync(user, UserRoles.Admin);
            }

            if (await _roleManager.RoleExistsAsync(UserRoles.User))
            {
                await _userManager.AddToRoleAsync(user, UserRoles.User);
            }

            return Ok(new Response { Status = "Success", Message = "新增使用者成功!" });
        }

        private JwtSecurityToken GetToken(List<Claim> authClaims)
        {
            var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));

            var token = new JwtSecurityToken(
                issuer: _configuration["JWT:ValidIssuer"],
                audience: _configuration["JWT:ValidAudience"],
                expires: DateTime.Now.AddHours(3),
                claims: authClaims,
                signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
            );

            return token;
        }
    }
}

我們在控制器類別中加入了三個方法 loginregisterregister-admin``。registerregister-admin 幾乎相同,但 register-admin 方法將用於建立具有 Admin 角色的使用者。在 login 方法中,我們在成功登入後回傳了一個 JWT token

.NET 6.0 中,微軟刪除了 Startup 類別(註:您可以恢復繼續使用這種方式),只保留了 Program 類別。我們必須在 Program 類別中定義所有相依性注入及其他設定。

Program.cs

using IdentityDemo.Auth;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
ConfigurationManager configuration = builder.Configuration;

// Add services to the container.

// For Entity Framework
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("ConnStr")));

// For Identity
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

// Adding Authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})

// Adding Jwt Bearer
.AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.RequireHttpsMetadata = false;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidAudience = configuration["JWT:ValidAudience"],
        ValidIssuer = configuration["JWT:ValidIssuer"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Secret"]))
    };
});

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// Authentication & Authorization
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

我們必須在執行應用程式之前建立所需的資料庫與資料表。由於我們使用的是實體框架 (EF Core),我們可以使用下面的資料庫遷移命令和套件管理器主控台來建立一個遷移指令碼:

add-migration Initial

使用以下命令建立資料庫與資料表:

update-database

如果您使用 SQL Server 物件總管檢查資料庫,您可以看到在資料庫內部建立了以下資料表:

在資料庫遷移過程中,為 UserRoleClaims 建立了 7 張資料表。這是用於 Identity 框架。

ASP.NET Core Identity 是一個會員系統,允許您向應用程式加入登入功能。使用者可以建立帳戶並使用 使用者名稱和密碼登入,也可以使用 外部登入提供者,例如 Facebook、Google、Microsoft Account、Twitter 等。

您可以將 ASP.NET Core Identity 設定為使用 SQL Server 資料庫來儲存使用者名稱、密碼和設定檔資料。或者,你可以使用自己的持久化儲存將資料儲存在另一個其他持久化儲存中,例如 Azure 資料表儲存。

我們可以在 WeatherForecast 控制器中加入 Authorize 屬性。

我們可以執行應用程式並嘗試從 Postman 工具存取 WeatherForecastController 中的 get 方法。

我們收到了 401 未經授權的錯誤。因為,我們已經為整個控制器加入了 Authorize 屬性。我們必須透過請求標頭提供一個有效的 token 來存取這個控制器和控制器內的方法。

我們可以在 AuthenticateController 中使用註冊方法建立一個新使用者。

我們以原始 JSON 格式提供了輸入資料。

我們可以使用上述使用者憑證登入並取得有效的 JWT token

使用上述憑證成功登入後,我們收到了一個 token

我們可以使用 https://jwt.io 站點解碼 token 並檢視宣告和其他資訊。

我們可以在授權頁籤中將上述 token 值作為 Bearer token 傳遞,並再次呼叫 WeatherForecastControllerget 方法。

這一次,我們成功地接收到了來自控制器的值。

我們可以使用基於角色的授權來變更 WeatherForecastController

現在,只有具有管理員 (Admin) 角色的使用者才能存取這個控制器和方法。

我們可以嘗試在 Postman 工具中再次使用相同的 token 存取 WeatherForecastController。

我們現在收到了 403 拒絕錯誤而不是 401。即使我們傳遞了一個有效的 token,我們也沒有足夠的權限來存取控制器。要存取這個控制器,使用者必須具有 Admin 角色權限。目前使用者是普通使用者,沒有任何 Admin 角色權限。

我們可以建立一個具有 Admin 角色權限的新使用者。我們已經在 AuthenticateController 中有一個方法 register-admin 用於相同目的。

我們可以使用這些新的使用者憑證登入並獲得一個新的 token。如果您對 token 進行解碼,您可以看到角色已加入到 token 中。

我們可以使用這個 token 而不是舊 token 來存取 WeatherForecastController

現在我們已經成功地從 WeatherForecastController 中取得了資料。

結論

在這篇文章中,我們了解了如何在 .NET 6.0 ASP.NET Core Web API 應用程式中建立 JSON Web Token,並使用此 token 進行身分驗證和授權。我們建立了兩個使用者,一個沒有任何角色,一個有 Admin 角色。我們在控制器層級應用了身分驗證和授權,並看到了這兩個使用者的不同行為。

繼續探索

延伸閱讀

更多文章
同分類 / 同標籤 2024/1/19

基於 .NET 的 FluentValidation 驗證教學

FluentValidation 是一個基於 .NET 開發的驗證框架,開源免費,而且優雅,支援鏈式操作,易於理解,功能完善,還可與 MVC5、WebApi2 和 ASP.NET CORE 深度整合,組件內提供十幾種常用驗證器,可擴展性好,支援自訂驗證器,支援本地化多語言。

繼續閱讀
同分類 / 同標籤 2024/6/20

CodeWF.EventBus:輕量級事件匯流排,讓通訊更流暢

CodeWF.EventBus,一款靈活的事件匯流排庫,實現模組間解耦通訊。支援多種.NET專案類型,如WPF、WinForms、ASP.NET Core等。採用簡潔設計,輕鬆實現命令的發布與訂閱、請求與回應。透過有序的事件處理,確保事件得到妥善處理。簡化您的程式碼,提升系統可維護性。

繼續閱讀