JWT là viết tắt của JSON Web Token và nó là một cơ chế ủy quyền chứ không phải xác thực. Vì vậy, hãy tìm hiểu sự khác biệt giữa hai điều đó.
Xác thực là cơ chế cho phép xác minh rằng người dùng chính xác là người mà họ tuyên bố. Đó là một quá trình đăng nhập trong đó người dùng cung cấp tên người dùng và mật khẩu và hệ thống sẽ xác minh chúng. Vì vậy, xác thực trả lời câu hỏi: người dùng là ai?
Ủy quyền là cơ chế cho phép xác minh quyền truy cập nào mà người dùng có đối với một tài nguyên nhất định. Đó là một quá trình cấp cho người dùng một số vai trò và một tập hợp các quyền mà một vai trò cụ thể có. Vì vậy, ủy quyền trả lời câu hỏi đó: người dùng có những quyền gì trong hệ thống?
Điều quan trọng là phải hiểu rằng Xác thực luôn được đặt lên hàng đầu và Ủy quyền là thứ hai. Nói cách khác, bạn không thể nhận được sự cho phép trước khi xác minh danh tính của mình. Nhưng các phương pháp ủy quyền phổ biến nhất là gì? Có hai cách tiếp cận chính để xử lý ủy quyền cho ứng dụng web.
Cách tiếp cận truyền thống trên web dành cho người dùng được ủy quyền là phiên phía máy chủ dựa trên cookie. Quá trình bắt đầu khi người dùng đăng nhập và máy chủ xác thực anh ta. Sau đó, máy chủ tạo một phiên có ID phiên và lưu nó ở đâu đó trong bộ nhớ của máy chủ. Máy chủ gửi lại ID phiên cho máy khách và máy khách lưu ID phiên trong cookie. Đối với mọi yêu cầu, máy khách sẽ gửi ID phiên như một phần của yêu cầu và máy chủ xác minh ID phiên trong bộ nhớ của nó cũng như các quyền của người dùng liên quan đến phiên này.
Một cách tiếp cận phổ biến khác là sử dụng mã thông báo để ủy quyền. Quá trình bắt đầu tương tự khi người dùng nhập thông tin đăng nhập, mật khẩu và máy khách gửi yêu cầu đăng nhập đến máy chủ. Thay vì tạo phiên, máy chủ tạo mã thông báo được ký bằng mã thông báo bí mật. Sau đó, máy chủ gửi lại mã thông báo cho máy khách và máy khách phải lưu trữ mã thông báo đó trong bộ nhớ cục bộ. Tương tự như cách tiếp cận dựa trên phiên, máy khách phải gửi mã thông báo đến máy chủ cho mọi yêu cầu. Tuy nhiên, máy chủ không lưu trữ bất kỳ thông tin bổ sung nào về phiên người dùng. Máy chủ phải xác thực rằng mã thông báo không thay đổi kể từ khi nó được tạo và ký bằng khóa bí mật.
Phương pháp ủy quyền dựa trên phiên có thể dễ bị tấn công được gọi là Giả mạo yêu cầu chéo trang web (CSRF). Đó là một kiểu tấn công khi kẻ tấn công trỏ đến một trang web mà chúng đăng nhập để thực hiện các hành động mà chúng không có ý định thực hiện, chẳng hạn như gửi khoản thanh toán hoặc thay đổi mật khẩu.
Một điều nữa là khi sử dụng phương pháp ủy quyền dựa trên phiên sẽ tạo ra một phiên có trạng thái giữa máy khách và máy chủ. Vấn đề là nếu một máy khách muốn truy cập các máy chủ khác nhau trong phạm vi của cùng một ứng dụng thì các máy chủ đó phải chia sẻ trạng thái phiên. Trong trường hợp khác, máy khách sẽ cần được cấp phép trên mỗi máy chủ vì phiên sẽ khác nhau.
Mặt khác, phương pháp ủy quyền dựa trên mã thông báo không yêu cầu lưu trữ dữ liệu phiên ở phía máy chủ và có thể đơn giản hóa việc ủy quyền giữa nhiều máy chủ.
Tuy nhiên, mã thông báo vẫn có thể bị kẻ tấn công đánh cắp và cũng khó có thể vô hiệu hóa mã thông báo. Chúng ta sẽ xem chi tiết hơn và cách xử lý tình trạng vô hiệu trong bài viết này.
Mã thông báo Web JSON (JWT) là một tiêu chuẩn mở xác định một cách nhỏ gọn và khép kín để truyền thông tin một cách an toàn giữa các bên dưới dạng đối tượng JSON. Thông tin này có thể được xác minh và tin cậy vì nó được ký điện tử. JWT có thể được ký bằng bí mật (với thuật toán HMAC ) hoặc cặp khóa công khai/riêng bằng cách sử dụng RSA hoặc ECDSA .
Mã thông báo Web JSON bao gồm ba phần được phân tách bằng dấu chấm .
{ "alg": "HS256", "typ": "JWT" }
Tiêu đề thường bao gồm hai phần: loại mã thông báo và thuật toán ký đang được sử dụng.
{ "sub": "1234567890", "name": "John Doe", "admin": true }
Tải trọng chứa các xác nhận quyền sở hữu, là các tuyên bố về người dùng. Sau đó, tải trọng được mã hóa Base64Url để tạo thành phần thứ hai của Mã thông báo Web JSON. Bạn có thể tìm thấy mô tả về các trường tiêu chuẩn được sử dụng làm xác nhận quyền sở hữu tại đây .
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Để tạo phần chữ ký, bạn phải lấy tiêu đề được mã hóa, tải trọng được mã hóa, bí mật và thuật toán được chỉ định trong tiêu đề và ký tên vào đó.
Mã thông báo thường trông giống như sau:
xxxxx.yyyyy.zzzzz
Bạn có thể điều hướng đến jwt.io và gỡ lỗi mã thông báo mẫu hoặc mã thông báo của riêng bạn. Chỉ cần dán mã thông báo của bạn vào trường Mã hóa và chọn Thuật toán của chữ ký mã thông báo.
Bây giờ chúng ta đã có kiến thức lý thuyết về cách hoạt động của JWT, chúng ta có thể áp dụng nó vào dự án thực tế. Giả sử chúng ta có một API đơn giản thể hiện các hoạt động CRUD cho thực thể cà phê. Chúng tôi sẽ tạo một dự án API ASP.NET Core đại diện cho Coffee API. Sau đó, chúng tôi sẽ tạo một dự án API ASP.NET Core khác đại diện cho API nhận dạng có thể tạo JWT. Trong cuộc sống thực, bạn có thể sẽ sử dụng Máy chủ nhận dạng hoặc Okta hoặc Auth0 cho mục đích Xác thực/Ủy quyền. Tuy nhiên, chúng tôi sẽ tạo API nhận dạng của riêng mình để trình bày cách tạo JWT. Khi Identity API hoàn tất, chúng ta có thể gọi bộ điều khiển của nó và tạo JWT dựa trên dữ liệu của người dùng. Ngoài ra, chúng tôi có thể bảo vệ Coffee API bằng cấu hình ủy quyền yêu cầu chuyển JWT theo từng yêu cầu.
Đầu tiên, chúng ta sẽ tạo một dự án API ASP.NET Core đơn giản đại diện cho Coffee API. Đây là cấu trúc của dự án này:
Hãy bắt đầu với Coffee.cs
trong thư mục Model
. Nó là một thực thể đơn giản với các thuộc tính Id
và Name
.
namespace Hackernoon.Coffee.API.Model; public class Coffee { public int Id { get; set; } public string Name { get; set; } }
Chúng tôi cần lưu trữ các thực thể của mình trong khi làm việc với API. Vì vậy, hãy giới thiệu một bộ lưu trữ trong bộ nhớ đơn giản. Nó nằm trong tệp Storage.cs
trong thư mục Data
.
namespace Hackernoon.Coffee.API.Data; public static class Storage { private static readonly List<Model.Coffee> Data = new(); public static List<Model.Coffee> GetAll() { return Data; } public static bool Create(Model.Coffee model) { if (Data.Any(c => c.Id == model.Id || c.Name == model.Name)) return false; Data.Add(new Model.Coffee { Id = model.Id, Name = model.Name }); return true; } public static bool Delete(int id) { if (Data.All(c => c.Id != id)) return false; Data.Remove(Storage.Data.First(c => c.Id == id)); return true; } public static bool Update(Model.Coffee model) { if (Data.All(c => c.Id != model.Id)) return false; Data.First(c => c.Id == model.Id).Name = model.Name; return true; } }
Chúng tôi cần một lớp đại diện cho các yêu cầu đối với Coffee API. Vì vậy, hãy tạo CoffeeRequest.cs
trong thư mục Contracts
.
namespace Hackernoon.Coffee.API.Contracts; public class CoffeeRequest { public int Id { get; set; } public string Name { get; set; } }
Khi hoàn tất, chúng ta có thể triển khai CoffeeController.cs
trong thư mục Controller
đại diện cho các hoạt động CRUD cho thực thể cà phê.
using Hackernoon.Coffee.API.Contracts; using Hackernoon.Coffee.API.Data; using Microsoft.AspNetCore.Mvc; namespace Hackernoon.Coffee.API.Controllers; [Route("coffee")] [ApiController] public class CoffeeController : ControllerBase { [HttpGet] public IList<Model.Coffee> GetAll() { return Storage.GetAll(); } [HttpPost] public IActionResult Create([FromBody]CoffeeRequest request) { var model = new Model.Coffee { Id = request.Id, Name = request.Name }; if (!Storage.Create(model)) return new BadRequestResult(); return new OkResult(); } [HttpDelete] public IActionResult Delete(int id) { if (!Storage.Delete(id)) return new BadRequestResult(); return new OkResult(); } [HttpPut] public IActionResult Update([FromBody] CoffeeRequest request) { var model = new Model.Coffee() { Id = request.Id, Name = request.Name }; if (!Storage.Update(model)) return new BadRequestResult(); return new OkResult(); } }
Coffee API đã hoàn tất, chúng ta có thể chạy dự án và xem Swagger UI như sau:
Hãy tạo một dự án API ASP.NET Core khác đại diện cho API nhận dạng. Đây là cấu trúc của dự án này:
Hãy bắt đầu với TokenGenerationRequest.cs
trong thư mục Contracts
, thể hiện yêu cầu tạo JWT mới với các thuộc tính Email
và Password
.
namespace Hackernoon.Identity.API.Contracts; public class TokenGenerationRequest { public string Email { get; set; } public string Password { get; set; } }
Chúng ta chỉ cần triển khai TokenController.cs
đại diện cho logic của việc tạo JWT. Nhưng trước khi chúng tôi thực hiện điều đó, gói Microsoft.AspNetCore.Authentication.JwtBearer
NuGet cần phải được cài đặt.
using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Hackernoon.Identity.API.Contracts; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; namespace Hackernoon.Identity.API.Controllers; [Route("token")] public class TokenController : ControllerBase { private const string SecretKey = "VerySecretAndLongKey-NeedMoreSymbolsHere-123"; private const string Issuer = "IdentityServerIssuer"; private const string Audience = "IdentityServerClient"; private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(20); [HttpPost] public string Create([FromBody]TokenGenerationRequest request) { var claims = new List<Claim> {new Claim(ClaimTypes.Email, request.Email) }; var jwt = new JwtSecurityToken( issuer: Issuer, audience: Audience, claims: claims, expires: DateTime.UtcNow.Add(Lifetime), signingCredentials: CreateSigningCredentials()); return new JwtSecurityTokenHandler().WriteToken(jwt); } private static SigningCredentials CreateSigningCredentials() { return new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey)), SecurityAlgorithms.HmacSha256); } }
Lưu ý rằng các hằng số nhạy cảm như SecretKey
, Issuer
và Audience
phải được đặt ở đâu đó trong cấu hình. Chúng được mã hóa cứng chỉ để đơn giản hóa dự án thử nghiệm này. Trường Lifetime
được đặt thành 20 phút, có nghĩa là mã thông báo sẽ có hiệu lực trong thời gian đó. Bạn cũng có thể cấu hình tham số này.
Bây giờ chúng ta có thể chạy dự án và xem Swagger UI như sau:
Hãy thực hiện cuộc gọi đến điểm cuối /token
và tạo JWT mới. Hãy thử tải trọng sau:
{ "email": "[email protected]", "password": "password" }
API nhận dạng sẽ tạo JWT tương ứng:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJqb2huLmRvZUBnbWFpbC5jb20iLCJJc0dvdXJtZXQiOiJmYWxzZSIsImV4cCI6MTcwNzc4Mzk4MCwiaXNzIjoiSWRlbnRpdHlTZXJ2ZXJJc3N1ZXIiLCJhdWQiOiJJZGVudGl0eVNlcnZlckNsaWVudCJ9.4odXsbWak1C0uK3Ux-n7f58icYQQwlHjM54OjgMCVPM
Giờ đây, khi API nhận dạng đã sẵn sàng và cung cấp mã thông báo cho chúng tôi, chúng tôi có thể bảo vệ API cà phê bằng ủy quyền. Một lần nữa gói Microsoft.AspNetCore.Authentication.JwtBearer
NuGet cần được cài đặt.
Chúng ta cần đăng ký các dịch vụ cần thiết bằng dịch vụ xác thực. Thêm mã sau vào tệp Program.cs
ngay sau khi tạo trình tạo.
var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = "IdentityServerIssuer", ValidateAudience = true, ValidAudience = "IdentityServerClient", ValidateLifetime = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes("VerySecretAndLongKey-NeedMoreSymbolsHere-123")), ValidateIssuerSigningKey = true, }; }); builder.Services.AddAuthorization();
Điều quan trọng cần nhớ là thứ tự trong phần mềm trung gian là quan trọng. Chúng tôi kích hoạt xác thực bằng cách gọi phương thức AddAuthentication()
và chỉ định JwtBearerDefaults.AuthenticationScheme
làm lược đồ xác thực. Nó là một hằng số chứa giá trị Bearer
.
namespace Microsoft.AspNetCore.Authentication.JwtBearer { /// <summary>Default values used by bearer authentication.</summary> public static class JwtBearerDefaults { /// <summary> /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions /// </summary> public const string AuthenticationScheme = "Bearer"; } }
Chúng ta cần chỉ định TokenValidationParameters
mô tả tham số nào của JWT sẽ được xác thực trong quá trình ủy quyền. Chúng tôi cũng chỉ định IssuerSigningKey
tương tự như signingCredentials
trong Identity API để xác minh chữ ký JWT. Kiểm tra thêm chi tiết về TokenValidationParameters
tại đây .
Đoạn mã tiếp theo bổ sung phần mềm trung gian vào trình tạo để kích hoạt khả năng xác thực và ủy quyền. Nó nên được thêm vào giữa các phương thức UseHttpsRedirection()
và MapControllers()
.
app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();
Bây giờ, chúng ta có thể sử dụng thuộc tính Authorize
trên bộ điều khiển hoặc các hành động của nó. Bằng cách áp dụng mã này, giờ đây tất cả các hành động trong CoffeeController
đều được bảo vệ bằng cơ chế ủy quyền và JWT phải được gửi như một phần của yêu cầu.
[Route("coffee")] [ApiController] [Authorize] public class CoffeeController : ControllerBase { ..
Nếu chúng tôi thực hiện lệnh gọi tới bất kỳ điểm cuối nào của API Cà phê, chúng tôi có thể gỡ lỗi HttpContext.User
và thấy rằng nó đã được điền và có Identity
với các xác nhận quyền sở hữu mà chúng tôi đã chỉ định trong JWT. Điều quan trọng là phải hiểu cách ASP.NET Core xử lý Ủy quyền một cách sâu sắc.
Chúng tôi đã làm rất tốt việc bảo vệ Coffee API bằng sự ủy quyền. Nhưng nếu bạn chạy dự án Coffee API và mở Swagger UI, bạn sẽ không thể gửi JWT như một phần của yêu cầu. Để khắc phục điều đó, chúng ta cần cập nhật tệp Program.cs
bằng đoạn mã sau:
builder.Services.AddSwaggerGen(option => { option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Please enter a valid token", Name = "Authorization", Type = SecuritySchemeType.Http, BearerFormat = "JWT", Scheme = "Bearer" }); option.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type=ReferenceType.SecurityScheme, Id="Bearer" } }, new string[]{} } }); });
Sau đó, chúng ta sẽ thấy nút Ủy quyền ở góc trên cùng bên phải:
Khi click vào nút Authorize bạn sẽ có thể vào JWT như sau:
Bạn không thể giới hạn bản thân trong việc sử dụng Swagger UI và có thể thực hiện kiểm tra API thông qua công cụ Postman. Trước tiên, hãy gọi điểm cuối /token
của API nhận dạng. Chúng ta cần chỉ định tiêu đề Content-Type
với giá trị application/json
trong phần Tiêu đề vì chúng ta sẽ sử dụng JSON làm tải trọng.
Sau đó, chúng ta có thể gọi điểm cuối /token
và nhận JWT mới.
Bây giờ, chúng ta có thể sao chép JWT và sử dụng nó để gọi Coffee API. Chúng ta cần chỉ định tiêu đề Content-Type
tương tự như Identity API nếu muốn kiểm tra, tạo và cập nhật điểm cuối. Tiêu đề Authorization
cũng phải được đặt bằng giá trị Bearer [your JWT value]
. Sau đó, chỉ cần nhấn nút Gửi và xem kết quả.
Như bạn còn nhớ, phần tải trọng của JWT là một tập hợp các xác nhận quyền sở hữu với các giá trị chính xác là các cặp khóa-giá trị. Ủy quyền dựa trên vai trò cho phép bạn phân biệt quyền truy cập vào tài nguyên ứng dụng tùy thuộc vào vai trò của người dùng.
Nếu chúng tôi cập nhật phương thức Create()
trong tệp TokenController.cs
trong Identity API bằng mã bổ sung xác nhận quyền sở hữu mới cho vai trò; chúng tôi có thể xử lý xác thực dựa trên vai trò trong API Cà phê. ClaimTypes.Role
là tên được xác định trước của yêu cầu vai trò.
var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim(ClaimTypes.Role, "Barista") };
Cập nhật thuộc tính Authorize
trong tệp CoffeeController.cs
chỉ định tên vai trò:
[Authorize(Roles = "Barista")]
Giờ đây, tất cả người dùng thực hiện lệnh gọi tới Coffee API đều phải có quyền xác nhận vai trò với giá trị Barista
. Nếu không, họ sẽ nhận được mã trạng thái 403 Forbidden
.
Thuộc tính Authorize
có thể dễ dàng xử lý xác thực dựa trên vai trò. Nhưng điều gì sẽ xảy ra nếu điều đó là chưa đủ và chúng tôi muốn phân biệt quyền truy cập dựa trên một số thuộc tính người dùng như tuổi hoặc bất kỳ thuộc tính nào khác? Bạn có thể đã đoán rằng bạn có thể thêm các xác nhận quyền sở hữu của mình vào JWT và sử dụng chúng để xây dựng logic ủy quyền. Bản thân ủy quyền dựa trên vai trò là trường hợp đặc biệt của ủy quyền dựa trên yêu cầu, giống như vai trò là cùng một đối tượng yêu cầu thuộc loại được xác định trước.
Hãy cập nhật phương thức Create()
trong tệp TokenController.cs
trong Identity API với mã thêm xác nhận quyền sở hữu mới IsGourmet
.
var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim("IsGourmet", "true") };
Trong tệp Program.cs trong Coffee API, chúng ta cần tạo một chính sách xác minh xác nhận quyền sở hữu và có thể được sử dụng trong thuộc tính Authorize
. Đoạn mã sau phải được thêm ngay sau lệnh gọi phương thức AddAuthentication()
.
builder.Services.AddAuthorization(opts => { opts.AddPolicy("OnlyForGourmet", policy => { policy.RequireClaim("IsGourmet", "true"); }); });
Cập nhật thuộc tính Authorize
trong tệp CoffeeController.cs
chỉ định tên chính sách:
[Authorize(Policy = "OnlyForGourmet")]
Chúc mừng! Bạn đã nỗ lực rất nhiều trong việc học JWT trong .NET. Bây giờ, bạn phải hiểu rõ về các nguyên tắc JWT và lý do tại sao việc sử dụng nó để thực hiện ủy quyền trong các ứng dụng .NET lại quan trọng. Nhưng chúng tôi mới chỉ bắt đầu trong lĩnh vực xác thực và ủy quyền trong các ứng dụng ASP.NET Core.
Tôi khuyên bạn nên xem tài liệu của Microsoft về các chủ đề chúng ta đã thảo luận trong bài viết này. Ngoài ra còn có rất nhiều khả năng tích hợp sẵn để ủy quyền và quản lý vai trò trong nền tảng .NET. Một bổ sung hay cho bài viết này có thể là tài liệu của Microsoft về ủy quyền.