Asp.Net Core – MVC Bảo mật và định danh (Phần I)

0 Comments

 

Bảo mật và xác định danh tính trong Asp.Net Core

Ở bài trước chúng ta đã cùng nhau hoàn thiện ứng dụng nhưng từng đó là chưa đủ, với một ứng dụng web vấn đề bảo mật luôn được đặt lên hàng đầu.

Bảo mật là mối quan tâm chính của bất kỳ ứng dụng Web hoặc API hiện đại nào. Bảo mật giữ cho dữ liệu người dùng hay khách hàng tránh xa khỏi những kẻ tấn công. Đây là một vấn đề rất rộng, liên quan tới một số thứ như:
– Vệ sinh dữ liệu đầu vào ngăn chặn những cuộc tấn công SQL.
– Ngăn chặn các cuộc tấn công tên miền chéo (CSRF) trong các form.
– Sử dụng HTTPS (mã hóa kết nối) để dữ liệu không bị chặn khi truyền tải trên mạng.
– Cung cấp cho người dùng cách đăng nhập an toàn bằng mật khẩu hoặc thông tin đăng nhập khác.
– Thiết kế đặt lại mật khẩu, khôi phục tài khoản và luồng xác thực đa yếu tố.
Asp.Net Core giúp cho tất cả điều này trở nên dễ dàng hơn. Hai phần mềm đầu tiên ( ngăn chặn những cuộc tấn công SQL và những cuộc tấn công tên miền chéo) đã được tích hợp sẵn và bạn có thể thêm một vài dòng mã để kích hoạt hỗ trợ HTTPS. Ở phần này chúng ta chủ yếu quan tâm tới phần định danh của bảo mật. Xử lý tài khoản người dùng, xác thực (đăng nhập) người dùng của bạn một cách an toàn và đưa ra quyết định ủy quyền khi họ được xác thực.
MVC và mẫu xác thực cá nhân bạn đã xử dụng để xây dựng dự án bao gồm một số lớp được xây dựng dựa trên định danh Asp.Net Core. Một hệ thống xác thực và nhận dạng là một phần của Asp.Net Core.

Định danh trong Asp.Net Core là gì?
Asp.Net Core Identity là một hệ thống nhận dạng đi kèm với Asp.Net Core. Giống như mọi thứ khác trong hệ sinh thái Asp.Net Core, nó là một tập các gói NuGet có thể được cài đặt trong bất kỳ hệ thống nào (và đã được bao gồm nếu bạn sử dụng mẫu mặc định).

Asp.Net Core quan tâm tới việc lưu trữ các tài khoản người dùng, mã hóa và lưu trữ mật khẩu, và quản lý vai trò của người dùng. Nó hỗ trợ đăng nhập email/password, xác thực đa yếu tố, đăng nhập với các nhà cung cấp khác như Google, Facebook, Twitter. Cũng giống như kết nối vơi các bằng các giao thức như Oauth 2.0 và OpenID Connect.

Yêu cầu xác thực:
Thường thì bạn sẽ muốn yêu cầu người dùng đang nhập trước khi họ có thể truy cập vào một số phần nhất định trong ứng dụng của bạn, ví dụ: sẽ hiển thị trang chủ cho mọi người (dù bạn đã đăng nhập hay chưa), nhưng chỉ hiển thị danh sách công việc khi bạn đã đăng nhập.

Bạn có thể sử dụng [Authorize] trong Asp.Net Core để yêu cầu người dùng đang nhập cho một hành động cụ thể hay một controller. Để yêu cầu cho tất cả các hành động của Todocontroller thêm thuộc tính bên trên dòng đầu tiên của controller:

Controllers/TodoController.cs
[Authorize]
public class TodoController : Controller
{
           // …
}

Thêm khai báo using ở đầu tệp:
using Microsoft.AspNetCore.Authorization;
Bây giờ hãy chạy thử ứng dụng của mình lên và truy cập vào đường dẫn /Todo mà chưa đang nhập, bạn sẽ được chuyển tới trang đăng nhập một cách tự động.

Sử dụng định danh trong ứng dụng:
Bản thân các mục công việc của chúng ta hiện tại vẫ được chia sẻ cho tất cả người dùng, vì các thực thể công việc không được gắn với một người dùng cụ thể. Bây giờ, thuộc tính [authorize] đảm bảo rằng bạn phải đăng nhập để xem các công việc, bạn có thể lọc các công việc dựa trên người đăng nhập trong truy vấn cơ sở dữ liệu.

Đầu tiên hãy thêm một Dependency Injection UserManager<ApplicationUser> vào TodoController:
Controllers/TodoController.cs

[Authorize]
public class TodoController : Controller
{
           private readonly ITodoItemService _todoItemService;
           private readonly UserManager<ApplicationUser> _userManager;

           public TodoController(ITodoItemService todoItemService,
           UserManager<ApplicationUser> userManager)
           {
                   _todoItemService = todoItemService;
                   _userManager = userManager;
           }

           // …
}
Bạn cần thêm câu lệnh using ở đầu tệp:
using Microsoft.AspNetCore.Identity;

Lớp UserManager là một phần của Asp.Net Core Identity. Bạn có thể sử dụng nó để lấy người dùng hiện tại trong phương thức Index:
public async Task<IActionResult> Index()
{
         var currentUser = await _userManager.GetUserAsync(User);
         if (currentUser == null) return Challenge();

         var items = await _todoItemService
               .GetIncompleteItemsAsync(currentUser);

         var model = new TodoViewModel()
         {
                 Items = items
         };

         return View(model);
}
Đoạn mã mới ở đầu của phương thức sử dụng UserManager để tìm kiếm người dùng hiện tại từ thuộc tính người dùng có sẵn trong phương thức:
var currentUser = await _userManager.GetUserAsync(User);

Nếu có người đăng nhập, thuộc tính người dùng chứa một đối tượng với một số thông tin của người dùng. UserManager sử dụng thông tin đó để tra cứu chi tiết của người dùng trong cơ sở dữ liệu thông qua phương thức getUserAsync().
Giá trị CurrentUser không bao giờ null vì thuộc tính [Authorize] được đặt trên controller. Bạn có thể sử dụng phương thức Challenge() để buộc người dùng đăng nhập lại nếu thông tin của họ bị thiếu:
if (currentUser == null) return Challenge();
Vì hiện tại bạn đang truyền tham số ApplicationUser cho GetIncompleteItemsAsync(), bạn sẽ cần để cập nhật interface ItodoItemService:
Services/ITodoItemService.cs
public interface ITodoItemService
{
         Task<TodoItem[]> GetIncompleteItemsAsync(
          ApplicationUser user);

         // …
}
Vì bạn đã thay đổi Interface ItodoItemService, bạn cũng cần cập nhật trong phương thức triển khai TodoItemService:
Services/TodoItemService
public async Task<TodoItem[]> GetIncompleteItemsAsync(
ApplicationUser user)

Bước tiếp theo là cập nhật truy vấn cơ sở dữ liệu và thêm điều kiện chỉ hiển thị các công việc được tạo bởi người dùng hiện tại. Trước khi bạn có thể làm điều đó bạn cần thêm một thuộc tính mới vào cơ sở dữ liệu.

Security concept: pixelated Locks icon on digital background, 3d render

Cập nhật cơ sở dữ liệu:
Bạn cần thêm một thuộc tính mới vào model TodoItem để mỗi mục có thể ghi nhớ người sở hữu nó:
Models/TodoItem.cs
public string UserId { get; set; }

Sau khi cập nhật model được sử dụng bởi database context. Tạo một migration mới :
dotnet ef migrations add AddItemUserId
Câu lệnh trên tạo ra một migration mới có tên AddItemUserId, nó sẽ thêm một cột mới vào bảng Items. Chạy câu lệnh sau để cập nhật chúng vào cơ sở dữ liệu:
dotnet ef database update.

Cập nhật lớp dịch vụ:
Với cơ sở dữ liệu và database context được cập nhật, bây giờ bạn có thể cập nhật phương thức GetcompleteItemAsync() trong TodoItemService và thêm một mệnh để khác vào câu lệnh where:
Services/TodoItemService.cs
public async Task<TodoItem[]> GetIncompleteItemsAsync(
ApplicationUser user)
{
          return await _context.Items
                 .Where(x => x.IsDone == false && x.UserId == user.Id)
                 .ToArrayAsync();
}
Nếu bạn chạy ứng dụng và đăng ký hoặc đăng nhập, bạn sẽ thấy một danh sách các công việc trống vì bạn chưa cập nhật phương thức AddItem.

Cập nhật hành động AddItem và MarkDone:
Bạn sẽ cần sử dụng UserMangaer để có được người dùng hiện tại trong các phương thức AddItem và MarkDone giống như bạn đã làm trong phương thức Index.
Controllers/TodoController.cs
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddItem(TodoItem newItem)
{
          if (!ModelState.IsValid)
          {
                   return RedirectToAction(“Index”);
          }

          var currentUser = await _userManager.GetUserAsync(User);
          if (currentUser == null) return Challenge();

          var successful = await _todoItemService
          .AddItemAsync(newItem, currentUser);

          if (!successful)
          {
                   return BadRequest(“Could not add item.”);
          }

          return RedirectToAction(“Index”);
}

[ValidateAntiForgeryToken]
public async Task<IActionResult> MarkDone(Guid id)
{
         if (id == Guid.Empty)
         {
                 return RedirectToAction(“Index”);
         }

         var currentUser = await _userManager.GetUserAsync(User);
         if (currentUser == null) return Challenge();

         var successful = await _todoItemService
         .MarkDoneAsync(id, currentUser);

         if (!successful)
         {
                  return BadRequest(“Could not mark item as done.”);
         }

         return RedirectToAction(“Index”);
}
Cả hai phuong thức Service hiện tại đều có tham số đầu vào ApplicationUser. Cập nhật chúng trong Interface ItodoItem:
Task<bool> AddItemAsync(TodoItem newItem, ApplicationUser user);

Task<bool> MarkDoneAsync(Guid id, ApplicationUser user);

Và cuối cùng là cập nhật phương thức triển khai TodoItemService. Trong phương thức AddItemAsync, đặt thuộc tính UserId khi bạn tạo TodoItem mới:
public async Task<bool> AddItemAsync(
TodoItem newItem, ApplicationUser user)
{
           newItem.Id = Guid.NewGuid();
           newItem.IsDone = false;
           newItem.DueAt = DateTimeOffset.Now.AddDays(3);
           newItem.UserId = user.Id;

           // …
}
Mệnh đề where trong phương thức MarkDoneAsync cũng cần kiểm tra Id người dùng, vì vậy người lạ sẽ không thể cập nhật công việc của người khác bằng cách đoán Id:
public async Task<bool> MarkDoneAsync(
Guid id, ApplicationUser user)
{
        var item = await _context.Items
        .Where(x => x.Id == id && x.UserId == user.Id)
        .SingleOrDefaultAsync();

         // …
}
Mọi thứ đã hoàn thành, hãy thử sử dụng ứng dụng với hai tài khoản người dùng khác nhau. Các mục công việc được giữ riêng cho mỗi tài khoản. Ở bài sau chúng ta sẽ tiếp tục với chức năng phân quyền cho ứng dụng.

Categories:

Leave a Reply

Your email address will not be published. Required fields are marked *